Prechádzať zdrojové kódy

客户端首页热门推荐代码提交

wangzhijun 2 dní pred
rodič
commit
923a6de3bd
16 zmenil súbory, kde vykonal 522 pridanie a 67 odobranie
  1. 37 0
      nightFragrance-massage/src/main/java/com/ylx/home/hot/controller/HomeHotController.java
  2. 25 0
      nightFragrance-massage/src/main/java/com/ylx/home/hot/domain/dto/HotRecommendDTO.java
  3. 62 0
      nightFragrance-massage/src/main/java/com/ylx/home/hot/domain/vo/HotRecommendVO.java
  4. 10 0
      nightFragrance-massage/src/main/java/com/ylx/home/hot/service/HomeHotRecommendService.java
  5. 193 0
      nightFragrance-massage/src/main/java/com/ylx/home/hot/service/impl/HomeHotRecommendServiceImpl.java
  6. 7 0
      nightFragrance-massage/src/main/java/com/ylx/massage/mapper/MaTechnicianMapper.java
  7. 7 0
      nightFragrance-massage/src/main/java/com/ylx/massage/service/IMaTechnicianService.java
  8. 30 3
      nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/MaTechnicianServiceImpl.java
  9. 26 0
      nightFragrance-massage/src/main/java/com/ylx/merchant/domain/vo/MerchantWithAddressVO.java
  10. 15 0
      nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java
  11. 3 64
      nightFragrance-massage/src/main/java/com/ylx/project/domain/Project.java
  12. 6 0
      nightFragrance-massage/src/main/java/com/ylx/project/mapper/ProjectMapper.java
  13. 4 0
      nightFragrance-massage/src/main/java/com/ylx/project/service/ProjectService.java
  14. 21 0
      nightFragrance-massage/src/main/java/com/ylx/project/service/impl/ProjectServiceImpl.java
  15. 45 0
      nightFragrance-massage/src/main/resources/mapper/massage/MaTechnicianMapper.xml
  16. 31 0
      nightFragrance-massage/src/main/resources/mapper/project/ProjectMapper.xml

+ 37 - 0
nightFragrance-massage/src/main/java/com/ylx/home/hot/controller/HomeHotController.java

@@ -0,0 +1,37 @@
+package com.ylx.home.hot.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.common.core.domain.R;
+import com.ylx.home.hot.domain.dto.HotRecommendDTO;
+import com.ylx.home.hot.domain.vo.HotRecommendVO;
+import com.ylx.home.hot.service.HomeHotRecommendService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+@RestController
+@RequestMapping("/home/hot")
+@Api(tags = {"用户端首页热门推荐"})
+@Slf4j
+@PreAuthorize("@customerAuth.isCustomer()")
+public class HomeHotController {
+
+    @Resource
+    private HomeHotRecommendService homeHotRecommendService;
+
+    @PostMapping("/recommend")
+    @ApiOperation("用户端热门推荐分页接口")
+    public R<Page<HotRecommendVO>> hotRecommend(@Validated @RequestBody HotRecommendDTO dto) {
+        Page<HotRecommendVO> hotRecommendPage = this.homeHotRecommendService.getHotRecommendPage(dto);
+        return R.ok(hotRecommendPage);
+    }
+
+}

+ 25 - 0
nightFragrance-massage/src/main/java/com/ylx/home/hot/domain/dto/HotRecommendDTO.java

@@ -0,0 +1,25 @@
+package com.ylx.home.hot.domain.dto;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.home.hot.domain.vo.HotRecommendVO;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+@ApiModel("首页热门推荐DTO")
+public class HotRecommendDTO extends Page<HotRecommendVO> {
+
+    @ApiModelProperty("用户经度")
+    @NotNull(message = "用户经度不能为空")
+    private BigDecimal longitude;
+
+    @ApiModelProperty("用户纬度")
+    @NotNull(message = "用户纬度不能为空")
+    private BigDecimal latitude;
+}

+ 62 - 0
nightFragrance-massage/src/main/java/com/ylx/home/hot/domain/vo/HotRecommendVO.java

@@ -0,0 +1,62 @@
+package com.ylx.home.hot.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 首页热门推荐混合卡片VO
+ * cardType=1:技师商户卡片(左图原型)
+ * cardType=2:服务项目卡片(右图原型)
+ */
+@Data
+@ApiModel("热门推荐卡片VO")
+public class HotRecommendVO {
+
+    // ===================== 通用字段(两种卡片都有) =====================
+    @ApiModelProperty(value = "卡片类型:1=技师商户,2=服务项目")
+    private Integer cardType;
+
+    @ApiModelProperty(value = "封面图", notes = "商户=技师头像/形象照,项目=服务封面")
+    private String coverImg;
+
+    // ===================== 【cardType=1 商户专属字段】原型左侧卡片 =====================
+    @ApiModelProperty(value = "商户ID", notes = "cardType=1生效")
+    private Integer techId;
+
+    @ApiModelProperty(value = "商户名称/昵称")
+    private String teNickName;
+
+    @ApiModelProperty(value = "技师评分")
+    private Integer nStar;
+
+    @ApiModelProperty(value = "已服务订单总数")
+    private Integer serviceTotal;
+
+    @ApiModelProperty(value = "距离用户距离")
+    private String distance;
+
+    @ApiModelProperty(value = "服务标签集合")
+    private List<String> tagList;
+
+    // ===================== 【cardType=2 项目专属字段】原型右侧卡片 =====================
+    @ApiModelProperty(value = "项目ID", notes = "cardType=2生效")
+    private Integer projectId;
+
+    @ApiModelProperty(value = "项目标题")
+    private String projectTitle;
+
+    @ApiModelProperty(value = "项目描述亮点")
+    private String highlight;
+
+    @ApiModelProperty(value = "服务时长文案")
+    private String durationText;
+
+    @ApiModelProperty(value = "起步标价")
+    private BigDecimal priceMin;
+
+    @ApiModelProperty(value = "总已售下单量")
+    private Long salesCount;
+}

+ 10 - 0
nightFragrance-massage/src/main/java/com/ylx/home/hot/service/HomeHotRecommendService.java

@@ -0,0 +1,10 @@
+package com.ylx.home.hot.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ylx.home.hot.domain.dto.HotRecommendDTO;
+import com.ylx.home.hot.domain.vo.HotRecommendVO;
+
+public interface HomeHotRecommendService {
+
+    Page<HotRecommendVO> getHotRecommendPage(HotRecommendDTO dto);
+}

+ 193 - 0
nightFragrance-massage/src/main/java/com/ylx/home/hot/service/impl/HomeHotRecommendServiceImpl.java

@@ -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;
+    }
+
+}

+ 7 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/mapper/MaTechnicianMapper.java

@@ -20,6 +20,7 @@ import com.ylx.merchant.domain.dto.MerchantListDTO;
 import com.ylx.merchant.domain.dto.MerchantProjectDTO;
 import com.ylx.merchant.domain.vo.MerchantDetailVO;
 import com.ylx.merchant.domain.vo.MerchantListVO;
+import com.ylx.merchant.domain.vo.MerchantWithAddressVO;
 import com.ylx.project.domain.bookMerchant.vo.ProjectInfoVO;
 import org.apache.ibatis.annotations.Param;
 import org.mapstruct.Mapper;
@@ -228,5 +229,11 @@ public interface MaTechnicianMapper extends BaseMapper<MaTechnician> {
     MerchantDetailVO getDetailById(Integer id);
 
     MerchantDetailVO getDetail(@Param("dto") MerchantDetailDTO dto);
+
+    Long countValidTech();
+
+    List<MerchantWithAddressVO> selectTechWithAddressByIds(@Param("ids") List<Integer> ids);
+
+    List<MerchantWithAddressVO> listValidTechWithAddress(@Param("offset") long offset, @Param("fetchCount") long fetchCount);
 }
 

+ 7 - 0
nightFragrance-massage/src/main/java/com/ylx/massage/service/IMaTechnicianService.java

@@ -29,6 +29,7 @@ import com.ylx.merchant.domain.dto.MerchantListDTO;
 import com.ylx.merchant.domain.dto.MerchantProjectDTO;
 import com.ylx.merchant.domain.vo.MerchantDetailVO;
 import com.ylx.merchant.domain.vo.MerchantListVO;
+import com.ylx.merchant.domain.vo.MerchantWithAddressVO;
 import com.ylx.project.domain.Project;
 import com.ylx.project.domain.bookMerchant.vo.ProjectInfoVO;
 import org.springframework.stereotype.Service;
@@ -335,4 +336,10 @@ public interface IMaTechnicianService extends IService<MaTechnician> {
     Page<ProjectInfoVO> getByMerchantProject(MerchantProjectDTO dto);
 
     MerchantDetailVO getDetail(MerchantDetailDTO dto);
+
+    Long countValidTech();
+
+    List<MerchantWithAddressVO> selectTechWithAddressByIds(List<Integer> techIds);
+
+    List<MerchantWithAddressVO> listValidTechWithAddress(long offset, long fetchCount);
 }

+ 30 - 3
nightFragrance-massage/src/main/java/com/ylx/massage/service/impl/MaTechnicianServiceImpl.java

@@ -9,6 +9,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ylx.address.domain.TAddress;
 import com.ylx.address.mapper.TAddressMapper;
+import com.ylx.address.service.TAddressService;
 import com.ylx.attendanceconfig.domain.AttendanceRule;
 import com.ylx.attendanceconfig.mapper.AttendanceRuleMapper;
 import com.ylx.collect.service.CollectService;
@@ -25,18 +26,19 @@ import com.ylx.massage.domain.vo.*;
 import com.ylx.massage.enums.*;
 import com.ylx.massage.mapper.*;
 import com.ylx.massage.service.IMaTechnicianService;
-import com.ylx.address.service.TAddressService;
 import com.ylx.merchant.domain.dto.MerchantDetailDTO;
 import com.ylx.merchant.domain.dto.MerchantListDTO;
 import com.ylx.merchant.domain.dto.MerchantProjectDTO;
 import com.ylx.merchant.domain.vo.MerchantDetailVO;
 import com.ylx.merchant.domain.vo.MerchantListVO;
+import com.ylx.merchant.domain.vo.MerchantWithAddressVO;
 import com.ylx.order.domain.TOrder;
 import com.ylx.order.mapper.TOrderMapper;
 import com.ylx.project.domain.Project;
 import com.ylx.project.domain.bookMerchant.vo.ProjectInfoVO;
 import com.ylx.project.mapper.ProjectMapper;
 import org.springframework.beans.BeanUtils;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -124,7 +126,8 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
     private TAddressService addressService;
     @Resource
     private CollectService collectService;
-
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
 
     /**
      * 商户入驻申请注册
@@ -415,6 +418,7 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
 
     /**
      * 检查审核通过时,是否填写了身份证到期日期、健康证到期日期、从业资格证到期日期
+     *
      * @param dto
      */
     private void checkProfileAuditExpirationDates(MerchantProfileAuditDTO dto) {
@@ -1033,7 +1037,7 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
     /**
      * 查询商户服务项目列表
      *
-     * @param merchantId      商户id
+     * @param merchantId  商户id
      * @param auditStatus 审核状态
      * @return List<MaProject>
      */
@@ -1243,6 +1247,14 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
         if (rows <= 0) {
             throw new ServiceException("商户状态变更失败");
         }
+
+
+        // ========== 新增:商户状态变更时移除热门ZSet ==========
+        Integer newMerchantStatus = maTechnician.getMerchantStatus();
+        // 商户管理状态(0-正常, 1-限制接单, 2-冻结, 3-注销)
+        if (ObjectUtil.notEqual(0, newMerchantStatus)) {
+            redisTemplate.opsForZSet().remove("hot:merchant:rank", String.valueOf(id));
+        }
         return rows;
     }
 
@@ -1857,6 +1869,7 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
 
     /**
      * 构建合同文件VO
+     *
      * @param record
      * @return ContractFileVO.Contract
      */
@@ -1944,6 +1957,20 @@ public class MaTechnicianServiceImpl extends ServiceImpl<MaTechnicianMapper, MaT
         return detail;
     }
 
+    @Override
+    public List<MerchantWithAddressVO> listValidTechWithAddress(long offset, long fetchCount) {
+        return this.baseMapper.listValidTechWithAddress(offset, fetchCount);
+    }
+
+    @Override
+    public Long countValidTech() {
+        return this.baseMapper.countValidTech();
+    }
+
+    @Override
+    public List<MerchantWithAddressVO> selectTechWithAddressByIds(List<Integer> techIds) {
+        return this.baseMapper.selectTechWithAddressByIds(techIds);
+    }
 
     /**
      * 申请开通新服务

+ 26 - 0
nightFragrance-massage/src/main/java/com/ylx/merchant/domain/vo/MerchantWithAddressVO.java

@@ -0,0 +1,26 @@
+package com.ylx.merchant.domain.vo;
+
+import io.swagger.annotations.ApiModel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("商户关联地址vo")
+public class MerchantWithAddressVO implements Serializable {
+    private static final long serialVersionUID = -6201707352375823578L;
+
+    // 商户基础字段
+    private Integer id;
+    private String teNickName;
+    private String teAvatar;
+    private String avatar;
+    private Integer nStar;
+    private Integer nNum;
+    private String teProject;
+
+    // 关联地址经纬度
+    private BigDecimal latitude;
+    private BigDecimal longitude;
+}

+ 15 - 0
nightFragrance-massage/src/main/java/com/ylx/order/service/impl/TOrderServiceImpl.java

@@ -50,6 +50,7 @@ import com.ylx.shopingfundsdetail.service.ShoppingFundsDetailService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -126,6 +127,9 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
     @Resource
     private IMaProjectFareSettingService maProjectFareSettingService;
 
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
+
     @Override
     public TOrder addOrder(TOrder order) {
         return null;
@@ -1136,6 +1140,17 @@ public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> impleme
                 .eq(MaTechnician::getId, merchantId);
 
         maTechnicianMapper.update(null, techUpdate);
+        // Redis商户ZSet score +1
+        redisTemplate.opsForZSet().incrementScore("hot:merchant:rank", String.valueOf(order.getMerchantId()), 1);
+
+        // 项目表 projectUpdate 字段原子性自增 1
+        LambdaUpdateWrapper<Project> projectUpdate = Wrappers.lambdaUpdate(Project.class)
+                .setSql(" = sales_completed + 1")
+                .eq(Project::getId, order.getProjectId());
+
+        this.projectService.update(null, projectUpdate);
+        // Redis项目ZSet score +1
+        redisTemplate.opsForZSet().incrementScore("hot:project:rank", String.valueOf(order.getProjectId()), 1);
     }
 
     @Override

+ 3 - 64
nightFragrance-massage/src/main/java/com/ylx/project/domain/Project.java

@@ -12,134 +12,73 @@ import java.math.BigDecimal;
 @Data
 @TableName("project")
 public class Project extends BaseEntity {
-    /**
-     * 序列化版本号。
-     */
+
     private static final long serialVersionUID = -7825952120084009270L;
 
-    /**
-     * 主键ID。
-     */
     @ApiModelProperty("id")
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
-    /**
-     * 服务标签(1:按摩 2:陪玩)
-     */
     @ApiModelProperty("服务标签(1:按摩 2:陪玩)")
     private Integer type;
 
-    /**
-     * 服务类目ID
-     */
     @ApiModelProperty("服务类目ID")
     private Integer categoryId;
 
-    /**
-     * 标题。
-     */
     @ApiModelProperty("标题")
     private String title;
 
-
-    /**
-     * 封面图。
-     */
     @ApiModelProperty("封面图")
     private String cover;
 
-    /**
-     * 标注价格(现价)。
-     */
     @ApiModelProperty("标注价格(现价)")
     private BigDecimal price;
 
-    /**
-     * 市场参考价起始值。
-     */
     @ApiModelProperty("市场参考价(起)")
     private BigDecimal priceMin;
 
-    /**
-     * 市场参考价结束值。
-     */
     @ApiModelProperty("市场参考价(止)")
     private BigDecimal priceMax;
 
-    /**
-     * 商户分佣比例。
-     */
     @ApiModelProperty("商户分佣比例")
     private BigDecimal merchantShareRatio;
 
-    /**
-     * 标准时长。
-     */
     @ApiModelProperty("标时")
     private Integer standardDuration;
 
-    /**
-     * 计量单位,来源于字典数据。
-     */
     @ApiModelProperty("计量单位(字典数据)")
     private Integer unitType;
 
-    /**
-     * 状态:0=上架,1=下架。
-     */
     @ApiModelProperty("状态: 0=上架, 1=下架")
     private Integer status;
 
-    /**
-     * 是否推荐:0=否,1=是。
-     */
     @ApiModelProperty("是否推荐:0=否,1=是")
     private Integer isRecommended;
 
-    /**
-     * 价格是否自定义:0=否,1=是。
-     */
     @ApiModelProperty("价格是否自定义:0=否,1=是")
     private Integer isPriceCustom;
 
-    /**
-     * 项目亮点,关联亮点字典表的ID集合。
-     */
     @ApiModelProperty("项目亮点:关联亮点字典表的ID集合")
     private String highlightIds;
 
-    /**
-     * 项目亮点
-     */
     @ApiModelProperty("项目亮点前端展示")
     private String highlight;
 
-    /**
-     * 适用人群。
-     */
     @ApiModelProperty("适用人群")
     private String targetAudience;
 
-    /**
-     * 前端展示排序。
-     */
     @ApiModelProperty("前端展示排序")
     private Long sortOrder;
 
-    /**
-     * 是否删除:0=否,1=是。
-     */
     @ApiModelProperty("是否删除0否1是")
     @TableField("is_delete")
     @TableLogic
     private Integer isDelete;
 
-    /**
-     * 项目详情,富文本内容。
-     */
     @ApiModelProperty("项目详情 富文本")
     private String detail;
 
+    @ApiModelProperty("已完成有效订单总数(冗余,用于热门排序)")
+    private Long salesCompleted;
 
 }

+ 6 - 0
nightFragrance-massage/src/main/java/com/ylx/project/mapper/ProjectMapper.java

@@ -12,6 +12,8 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import java.util.Date;
+import java.util.List;
+
 @Mapper
 public interface ProjectMapper extends BaseMapper<Project> {
 
@@ -25,4 +27,8 @@ public interface ProjectMapper extends BaseMapper<Project> {
     default int deleteProjectById(Long id) {
         return deleteById(id);
     }
+
+    List<Project> selectProjectPage(@Param("offset") long offset, @Param("fetchCount") long fetchCount);
+
+    Long countValidProject();
 }

+ 4 - 0
nightFragrance-massage/src/main/java/com/ylx/project/service/ProjectService.java

@@ -40,4 +40,8 @@ public interface ProjectService extends IService<Project> {
     BookProjectDetailVO getBookingProjectDetail(BookMerchantDTO dto);
 
     List<ProjectBaseVo> listProjects();
+
+    List<Project> selectProjectPage(long offset, long fetchCount);
+
+    Long countValidProject();
 }

+ 21 - 0
nightFragrance-massage/src/main/java/com/ylx/project/service/impl/ProjectServiceImpl.java

@@ -38,12 +38,14 @@ import com.ylx.servicecategory.service.ServiceCategoryService;
 import com.ylx.system.service.ISysDictDataService;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
 import java.math.BigDecimal;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
 
@@ -57,6 +59,8 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
     private ISysDictDataService sysDictDataService;
     @Resource
     private IMaProjectService maProjectService;
+    @Resource
+    private RedisTemplate<String, String> redisTemplate;
 
     @Override
     public Page<ProjectPageVo> list(Page<Project> page, ProjectSearchDTO dto) {
@@ -113,6 +117,13 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
         if (!updateResult) {
             throw new ServiceException("更新项目失败");
         }
+
+        // ========== 新增:下架时移除热门ZSet ==========
+        Integer newStatus = entity.getStatus();
+        // 两种场景需要移除:1.本次编辑从上架改为下架;2.本身已是下架(兜底防脏数据)
+        if (ObjectUtil.equals(ProjectStatusEnum.OFF_SHELF.getCode(), newStatus)) {
+            redisTemplate.opsForZSet().remove("hot:project:rank", String.valueOf(id));
+        }
     }
 
     @Override
@@ -240,6 +251,16 @@ public class ProjectServiceImpl extends ServiceImpl<ProjectMapper, Project> impl
         return BeanUtil.copyToList(projects, ProjectBaseVo.class);
     }
 
+    @Override
+    public List<Project> selectProjectPage(long offset, long fetchCount) {
+        return this.baseMapper.selectProjectPage(offset, fetchCount);
+    }
+
+    @Override
+    public Long countValidProject() {
+        return this.baseMapper.countValidProject();
+    }
+
     private ProjectPageVo convertToVo(Project entity) {
         ProjectPageVo vo = new ProjectPageVo();
         // 属性拷贝(推荐)

+ 45 - 0
nightFragrance-massage/src/main/resources/mapper/massage/MaTechnicianMapper.xml

@@ -682,4 +682,49 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             t.id
     </select>
 
+    <!--计数有效可接单技师-->
+    <select id="countValidTech" resultType="java.lang.Long">
+        SELECT COUNT(id)
+        FROM ma_technician
+        WHERE
+            is_delete = 0
+        AND merchant_status = 0
+        AND audit_status = 2
+        AND service_state != 2
+    </select>
+
+    <!--关联地址表获取经纬度-->
+    <select id="selectTechWithAddressByIds" resultType="com.ylx.merchant.domain.vo.MerchantWithAddressVO">
+        SELECT
+            t.id, t.te_nick_name, t.te_avatar, t.avatar, t.n_star, t.n_num, t.te_project,
+            a.latitude, a.longitude
+        FROM ma_technician t
+        LEFT JOIN t_address a ON t.id = a.merchant_id
+        AND a.user_type = 2 AND a.type = 1 AND a.is_default = 1 AND a.is_delete = 0
+        WHERE t.is_delete = 0 AND t.id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
+    <select id="listValidTechWithAddress" resultType="com.ylx.merchant.domain.vo.MerchantWithAddressVO">
+        SELECT
+            t.id, t.te_nick_name, t.te_avatar, t.avatar, t.n_star, t.n_num, t.te_project,
+            a.latitude, a.longitude
+        FROM ma_technician t
+         -- 关联商户默认真实地址
+        LEFT JOIN t_address a ON t.id = a.merchant_id
+            AND a.user_type = 2
+            AND a.type = 1
+            AND a.is_default = 1
+            AND a.is_delete = 0
+        WHERE
+          t.is_delete = 0
+          AND t.merchant_status = 0
+          AND t.audit_status = 2
+          AND t.service_state != 2
+        ORDER BY t.n_num DESC
+            LIMIT #{offset}, #{size}
+    </select>
+
 </mapper>

+ 31 - 0
nightFragrance-massage/src/main/resources/mapper/project/ProjectMapper.xml

@@ -168,5 +168,36 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         p.create_time DESC
     </select>
 
+    <select id="selectProjectPage" resultType="com.ylx.project.domain.Project">
+        SELECT
+            id,
+            title,
+            cover,
+            price_min,
+            price,
+            standard_duration,
+            unit_type,
+            highlight,
+            sales_completed
+        FROM
+            project
+        WHERE
+            is_delete=0
+        AND status=0
+        ORDER BY sales_completed DESC
+        LIMIT #{offset}, #{size}
+    </select>
+
+    <!--计数上架有效项目-->
+    <select id="countValidProject" resultType="java.lang.Long">
+        SELECT
+            COUNT(id)
+        FROM
+            project
+        WHERE
+            is_delete = 0
+        AND status = 0
+    </select>
+
 
 </mapper>