소스 검색

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_java

caoliqin 1 주 전
부모
커밋
8c0a28739d
47개의 변경된 파일2017개의 추가작업 그리고 21개의 파일을 삭제
  1. 3 1
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductScrmController.java
  2. 73 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductTagScrmController.java
  3. 83 0
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreUserEndCategoryScrmController.java
  4. 17 0
      fs-cid-workflow/src/main/resources/application.yml
  5. 122 0
      fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java
  6. 2 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  7. 1 0
      fs-service/src/main/java/com/fs/course/domain/FsCourseRealLink.java
  8. 33 7
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  9. 20 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductTagRelationScrm.java
  10. 35 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductTagScrm.java
  11. 20 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductUserEndCategory.java
  12. 43 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreUserEndCategoryScrm.java
  13. 11 1
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java
  14. 28 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductTagRelationScrmMapper.java
  15. 29 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductTagScrmMapper.java
  16. 27 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductUserEndCategoryMapper.java
  17. 30 0
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreUserEndCategoryScrmMapper.java
  18. 5 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java
  19. 12 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java
  20. 31 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductTagScrmService.java
  21. 39 0
      fs-service/src/main/java/com/fs/hisStore/service/IFsStoreUserEndCategoryScrmService.java
  22. 60 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  23. 79 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductTagScrmServiceImpl.java
  24. 163 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java
  25. 23 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductTagListVO.java
  26. 17 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductTagNameVO.java
  27. 22 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreTagProductVO.java
  28. 29 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java
  29. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java
  30. 5 0
      fs-service/src/main/java/com/fs/live/service/ILiveDataService.java
  31. 24 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java
  32. 18 0
      fs-service/src/main/java/com/fs/live/vo/LiveAppSimpleVO.java
  33. 15 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopCourseFinishTempSetting.java
  34. 14 0
      fs-service/src/main/java/com/fs/qw/vo/QwSopTempSetting.java
  35. 78 1
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  36. 424 0
      fs-service/src/main/java/com/fs/utils/ShortCodeGeneratorUtils.java
  37. 1 1
      fs-service/src/main/resources/application-config-druid-bjzm-test.yml
  38. 3 3
      fs-service/src/main/resources/application-config-druid-bjzm.yml
  39. 41 2
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  40. 34 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductTagRelationScrmMapper.xml
  41. 65 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductTagScrmMapper.xml
  42. 34 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductUserEndCategoryMapper.xml
  43. 75 0
      fs-service/src/main/resources/mapper/hisStore/FsStoreUserEndCategoryScrmMapper.xml
  44. 8 0
      fs-service/src/main/resources/mapper/live/LiveDataMapper.xml
  45. 14 4
      fs-user-app/src/main/java/com/fs/app/controller/IndexController.java
  46. 83 1
      fs-user-app/src/main/java/com/fs/app/controller/store/IndexScrmController.java
  47. 16 0
      fs-wx-task/src/main/resources/application.yml

+ 3 - 1
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductScrmController.java

@@ -153,7 +153,9 @@ public class FsStoreProductScrmController extends BaseController
     {
         FsStoreProductScrm product=fsStoreProductService.selectFsStoreProductById(productId);
         List<FsStoreProductAttrScrm> attrs=attrService.selectFsStoreProductAttrByProductId(productId);
-        return R.ok().put("data",product).put("attrs", attrs);
+        List<Long> userEndCategoryIds = fsStoreProductService.selectUserEndCategoryIdsByProductId(productId);
+        List<Long> tagIds = fsStoreProductService.selectTagIdsByProductId(productId);
+        return R.ok().put("data",product).put("attrs", attrs).put("userEndCategoryIds", userEndCategoryIds).put("tagIds", tagIds);
     }
 
     @PreAuthorize("@ss.hasPermi('store:storeProduct:add')")

+ 73 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreProductTagScrmController.java

@@ -0,0 +1,73 @@
+package com.fs.hisStore.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.FsStoreProductTagScrm;
+import com.fs.hisStore.service.IFsStoreProductTagScrmService;
+import com.fs.hisStore.vo.FsStoreProductTagListVO;
+import com.fs.hisStore.vo.FsStoreTagProductVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 商品标签管理
+ * 支持:标签名称模糊搜索、新增/编辑/删除、调整排序、一页10条分页、查看关联商品(弹窗:商品ID、名称、主图、售价、状态)
+ */
+@RestController
+@RequestMapping("/store/store/storeProductTag")
+public class FsStoreProductTagScrmController extends BaseController {
+
+    @Autowired
+    private IFsStoreProductTagScrmService productTagService;
+
+    /** 分页列表,一页10条 */
+    @PreAuthorize("@ss.hasPermi('store:storeProductTag:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreProductTagScrm query) {
+        startPage();
+        List<FsStoreProductTagListVO> list = productTagService.selectListVOPage(query);
+        return getDataTable(list);
+    }
+
+    /** 关联商品:点击商品数量弹窗展示(商品ID、名称、主图、售价、状态),分页 */
+    @PreAuthorize("@ss.hasPermi('store:storeProductTag:query')")
+    @GetMapping("/products/{tagId}")
+    public TableDataInfo listProductsByTagId(@PathVariable Long tagId) {
+        startPage();
+        List<FsStoreTagProductVO> list = productTagService.selectProductsByTagId(tagId);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductTag:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(productTagService.selectById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductTag:add')")
+    @Log(title = "商品标签", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreProductTagScrm entity) {
+        return toAjax(productTagService.insert(entity));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductTag:edit')")
+    @Log(title = "商品标签", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreProductTagScrm entity) {
+        return toAjax(productTagService.update(entity));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:storeProductTag:remove')")
+    @Log(title = "商品标签", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(productTagService.deleteByIds(ids));
+    }
+}

+ 83 - 0
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreUserEndCategoryScrmController.java

@@ -0,0 +1,83 @@
+package com.fs.hisStore.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.service.IFsStoreUserEndCategoryScrmService;
+import com.fs.hisStore.vo.FsStoreUserEndCategoryProductVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 商品用户分端类管理(金刚区/瀑布流)
+ * 支持:分类名称模糊搜索、新增/编辑/删除、排序(用户端最多展示8个,超出不展示)、金刚区需icon/瀑布流不需、状态显示隐藏、一页10条分页、排序相同按创建时间倒序
+ */
+@RestController
+@RequestMapping("/store/store/userEndCategory")
+public class FsStoreUserEndCategoryScrmController extends BaseController {
+
+    @Autowired
+    private IFsStoreUserEndCategoryScrmService userEndCategoryService;
+
+    /** 分页列表,一页10条,排序值相同按创建时间倒序 */
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(FsStoreUserEndCategoryScrm query) {
+        startPage();
+        List<FsStoreUserEndCategoryScrm> list = userEndCategoryService.selectListPage(query);
+        return getDataTable(list);
+    }
+
+    /** 添加/编辑商品时拉取用户端分类(金刚区、瀑布流各最多8条由前端截取) */
+    @GetMapping("/listForProduct")
+    public R listForProduct(@RequestParam(required = false) Long storeId) {
+        List<FsStoreUserEndCategoryScrm> list = userEndCategoryService.selectListForProduct(storeId);
+        return R.ok().put("data", list);
+    }
+
+    /** 按用户端分类ID分页查询关联商品(去重商品ID分页;返回商品ID、名称、售价、原价、销量及产品标签列表) */
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:query')")
+    @GetMapping("/products")
+    public TableDataInfo listProductsByCategoryId(
+            @RequestParam Long id,
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize) {
+        startPage();
+        List<FsStoreUserEndCategoryProductVO> list = userEndCategoryService.listProductsByCategoryId(id);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:query')")
+    @GetMapping("/{id}")
+    public AjaxResult getInfo(@PathVariable Long id) {
+        return AjaxResult.success(userEndCategoryService.selectById(id));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:add')")
+    @Log(title = "用户分端类", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody FsStoreUserEndCategoryScrm entity) {
+        return toAjax(userEndCategoryService.insert(entity));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:edit')")
+    @Log(title = "用户分端类", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody FsStoreUserEndCategoryScrm entity) {
+        return toAjax(userEndCategoryService.update(entity));
+    }
+
+    @PreAuthorize("@ss.hasPermi('store:userEndCategory:remove')")
+    @Log(title = "用户分端类", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public AjaxResult remove(@PathVariable Long[] ids) {
+        return toAjax(userEndCategoryService.deleteByIds(ids));
+    }
+}

+ 17 - 0
fs-cid-workflow/src/main/resources/application.yml

@@ -0,0 +1,17 @@
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 7201
+logging:
+  level:
+    org: INFO
+    com: DEBUG
+# Spring配置
+spring:
+  profiles:
+    active: dev
+#    active: druid-hcl
+#    active: druid-sxjz
+#    active: druid-hdt
+#    active: druid-myhk-test
+cid-group-no: 1

+ 122 - 0
fs-qw-task/src/main/java/com/fs/app/taskService/impl/SopLogsTaskServiceImpl.java

@@ -52,6 +52,7 @@ import com.fs.sop.vo.SopUserLogsVo;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
@@ -1883,6 +1884,25 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         log.error("浏览器看课模板解析失败:" + e);
                     }
 
+                    break;
+                case "21"://短信看课
+                    if (sopLogs.getFsUserId() != null && !Long.valueOf(0L).equals(sopLogs.getFsUserId())) {
+                        addWatchLogIfNeeded(sopLogs, videoId, courseId, sendTime, qwUserId, companyUserId, companyId, externalId, logVo,2);
+                        sortLink = generateSmsShortLink(setting, logVo, sendTime, courseId, videoId,
+                                qwUserId, companyUserId, companyId, externalId, isOfficial, sopLogs.getFsUserId());
+
+                        if (StringUtils.isNotBlank(sortLink)) {
+                            if(StringUtils.isNotBlank(setting.getSmsTemplateContent()) && setting.getSmsTemplateContent().contains("${sms.courseUrl}")){
+                                setting.setValue(setting.getSmsTemplateContent()
+                                        .replaceAll("【(.*?)】", "【" + cachedCourseConfig.getSmsDomain() + "】")
+                                        .replace("${sms.courseUrl}", sortLink));
+                            }else{
+                                log.error("生成看课短链时检测到短信模板选择错误,跳过设置 URL。");
+                            }
+                        } else {
+                            log.error("生成看课短链失败,跳过设置 URL。");
+                        }
+                    }
                     break;
                 default:
                     break;
@@ -2300,6 +2320,108 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         return sortLink.replaceAll("^[\\s\\u2005]+", "");
     }
 
+
+    private String generateSmsShortLink(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
+                                        Long courseId, Long videoId, String qwUserId,
+                                        String companyUserId, String companyId, String externalId, String isOfficial, Long fsUserId) {
+        // 获取缓存的配置
+        CourseConfig config;
+        synchronized (configLock) {
+            config = cachedCourseConfig;
+        }
+
+        if (config == null) {
+            log.error("CourseConfig is not loaded.");
+            return "";
+        }
+
+        // 手动创建 FsCourseLink 对象,避免使用 BeanUtils.copyProperties
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setUNo(UUID.randomUUID().toString());
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(logVo.getCorpId());
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(Long.parseLong(externalId));
+
+        if (StringUtil.strIsNullOrEmpty(isOfficial)) {
+            link.setLinkType(0);
+        } else {
+//            link.setLinkType(isOfficial.equals("1") ? 5 : 0);
+            if (isOfficial.equals("1")) {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    link.setLinkType(0);
+                } else {
+                    link.setLinkType(5);
+                }
+            } else if (isOfficial.equals("0")) {
+                link.setLinkType(0);
+            } else {
+                link.setLinkType(0);
+            }
+        }
+
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        courseMap.setCompanyId(link.getCompanyId());
+        courseMap.setQwUserId(link.getQwUserId());
+        courseMap.setCompanyUserId(link.getCompanyUserId());
+        courseMap.setVideoId(link.getVideoId());
+        courseMap.setCorpId(link.getCorpId());
+        courseMap.setCourseId(link.getCourseId());
+        courseMap.setQwExternalId(link.getQwExternalId());
+        courseMap.setUNo(link.getUNo());
+
+        if (StringUtil.strIsNullOrEmpty(isOfficial)) {
+            courseMap.setLinkType(0);
+        } else {
+            if (isOfficial.equals("1")) {
+                if (fsUserId == null || Long.valueOf(0L).equals(fsUserId)) {
+                    courseMap.setLinkType(0);
+                } else {
+                    courseMap.setLinkType(5);
+                }
+            } else if (isOfficial.equals("0")) {
+                courseMap.setLinkType(0);
+            } else {
+                courseMap.setLinkType(0);
+            }
+        }
+
+        String courseJson = JSON.toJSONString(courseMap);
+        String realLinkFull = REAL_LINK_PREFIX + courseJson;
+        link.setRealLink(realLinkFull);
+
+        String randomString = ShortCodeGeneratorUtils.generate8();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+
+        link.setCreateTime(sendTime);
+
+        Integer expireDays = (setting.getExpiresDays() == null || setting.getExpiresDays() == 0)
+                ? config.getVideoLinkExpireDate()
+                : setting.getExpiresDays();
+
+        // 使用 Java 8 时间 API 计算过期时间
+        LocalDateTime sendDateTime = sendTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
+        LocalDateTime expireDateTime = sendDateTime.plusDays(expireDays - 1);
+        expireDateTime = expireDateTime.toLocalDate().atTime(23, 59, 59);
+        Date updateTime = Date.from(expireDateTime.atZone(ZoneId.systemDefault()).toInstant());
+        link.setUpdateTime(updateTime);
+        if(StringUtils.isEmpty(config.getSmsDomainName())){
+            log.error("检测到未配置看课短信链接域名");
+            return null;
+        }
+        String sortLink = config.getSmsDomainName()+ "/" + link.getLink();
+        enqueueCourseLink(link);
+        return sortLink;
+    }
+
     private QwCreateLinkByAppVO createLinkByApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
                                                 Long courseId, Long videoId, String qwUserId,
                                                 String companyUserId, String companyId, String externalId, String corpId, String qwUserName) {

+ 2 - 0
fs-service/src/main/java/com/fs/course/config/CourseConfig.java

@@ -22,6 +22,8 @@ public class CourseConfig implements Serializable {
     private String realLinkH5DomainName;//H5通用看课域名
     private String realLinkH5LiveName;//H5通用直播域名
     private String authDomainName;//网页授权域名
+    private String smsDomainName;//短信推送域名
+    private String smsDomain;//短信推送域名
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名
     private String courseDomainName;//链接域名

+ 1 - 0
fs-service/src/main/java/com/fs/course/domain/FsCourseRealLink.java

@@ -43,6 +43,7 @@ public class FsCourseRealLink implements Serializable
     private Long id;
 
     private String chatId;
+    private String uNo;
 
     private Long projectId;//项目ID
 

+ 33 - 7
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -65,6 +65,7 @@ import com.fs.his.service.IFsUserWxService;
 import com.fs.his.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import com.fs.im.service.OpenIMService;
 import com.fs.qw.domain.*;
 import com.fs.qw.domain.QwCompany;
@@ -170,6 +171,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     private ISopUserLogsInfoService iSopUserLogsInfoService;
     @Autowired
     private FsCourseLinkMapper fsCourseLinkMapper;
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
 
 
     @Autowired
@@ -4560,15 +4563,38 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 throw new RuntimeException(e);
             }
             if (jsonNode.isArray()) {
-                List<FsStoreProductScrm> fsPackageListVOS = new ArrayList<>();
+                // 第一步:收集所有 productId
+                List<Long> productIds = new ArrayList<>();
+                Map<Long, JsonNode> productIdToJsonNodeMap = new HashMap<>();
                 for (JsonNode node : jsonNode) {
+                    Long productId = node.path("productId").asLong();
+                    if (productId != null && productId > 0) {
+                        productIds.add(productId);
+                        productIdToJsonNodeMap.put(productId, node);
+                    }
+                }
+
+                // 第二步:批量从数据库查询满足条件的产品(已上架、未删除、审核通过)
+                List<FsStoreProductScrm> validProducts = new ArrayList<>();
+                if (!productIds.isEmpty()) {
+                    // 使用 getStoreProductInProductIdsForApp 查询,该方法已过滤 is_del=0 和 is_show=1
+                    // 但还需要在内存中过滤 is_audit='1'
+                    validProducts = fsStoreProductScrmMapper.getStoreProductInProductIdsForApp(productIds);
+                }
+                // 第四步:根据查询结果构建 VO 对象
+                List<FsStoreProductScrm> fsPackageListVOS = new ArrayList<>();
+                for (FsStoreProductScrm validProduct : validProducts) {
                     FsStoreProductScrm fsStoreProductScrm = new FsStoreProductScrm();
-                    fsStoreProductScrm.setProductId(node.path("productId").asLong());
-                    fsStoreProductScrm.setImages(node.path("image").asText());
-                    fsStoreProductScrm.setImgUrl(node.path("imgUrl").asText());
-                    fsStoreProductScrm.setBarCode(node.path("barCode").asText());
-                    fsStoreProductScrm.setPrice(new BigDecimal(node.path("price").asText()));
-                    fsStoreProductScrm.setProductName(node.path("productName").asText());
+                    JsonNode originalNode = productIdToJsonNodeMap.get(validProduct.getProductId());
+
+                    fsStoreProductScrm.setProductId(validProduct.getProductId());
+                    fsStoreProductScrm.setImages(validProduct.getImages() != null ? validProduct.getImages() :
+                            (originalNode != null ? originalNode.path("image").asText() : ""));
+                    fsStoreProductScrm.setImgUrl(validProduct.getImgUrl() != null ? validProduct.getImgUrl() :
+                            (originalNode != null ? originalNode.path("imgUrl").asText() : ""));
+                    fsStoreProductScrm.setBarCode(validProduct.getBarCode());
+                    fsStoreProductScrm.setPrice(validProduct.getPrice());
+                    fsStoreProductScrm.setProductName(validProduct.getProductName());
                     fsPackageListVOS.add(fsStoreProductScrm);
                 }
                 vo.setFsStoreProductScrms(fsPackageListVOS);

+ 20 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductTagRelationScrm.java

@@ -0,0 +1,20 @@
+package com.fs.hisStore.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商品与标签关联表 fs_store_product_tag_relation(一个商品最多3个标签)
+ */
+@Data
+public class FsStoreProductTagRelationScrm implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 标签ID */
+    private Long tagId;
+}

+ 35 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductTagScrm.java

@@ -0,0 +1,35 @@
+package com.fs.hisStore.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 商品标签对象 fs_store_product_tag
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FsStoreProductTagScrm extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 店铺ID */
+    @Excel(name = "店铺ID")
+    private Long storeId;
+
+    /** 标签名称,最多4个字 */
+    @Excel(name = "标签名称")
+    private String tagName;
+
+    /** 状态:1-显示 0-隐藏 */
+    @Excel(name = "状态", readConverterExp = "1=显示,0=隐藏")
+    private Integer status;
+
+    /** 排序值 */
+    @Excel(name = "排序")
+    private Long sort;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductUserEndCategory.java

@@ -0,0 +1,20 @@
+package com.fs.hisStore.domain;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商品与用户分端类关联表 fs_store_product_user_end_category
+ */
+@Data
+public class FsStoreProductUserEndCategory implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 商品ID */
+    private Long productId;
+
+    /** 用户分端类ID */
+    private Long userEndCategoryId;
+}

+ 43 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreUserEndCategoryScrm.java

@@ -0,0 +1,43 @@
+package com.fs.hisStore.domain;
+
+import com.fs.common.annotation.Excel;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 商品用户分端类(金刚区/瀑布流)对象 fs_store_user_end_category
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class FsStoreUserEndCategoryScrm extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** 店铺ID */
+    @Excel(name = "店铺ID")
+    private Long storeId;
+
+    /** 分类名称 */
+    @Excel(name = "分类名称")
+    private String categoryName;
+
+    /** 分类位置:1-金刚区 2-瀑布流 */
+    @Excel(name = "分类位置", readConverterExp = "1=金刚区,2=瀑布流")
+    private Integer position;
+
+    /** 分类icon(金刚区必填,瀑布流不填) */
+    @Excel(name = "分类icon")
+    private String icon;
+
+    /** 状态:1-显示 0-隐藏 */
+    @Excel(name = "状态", readConverterExp = "1=显示,0=隐藏")
+    private Integer status;
+
+    /** 排序值,越小越靠前;用户端各位置最多展示前8个 */
+    @Excel(name = "排序")
+    private Long sort;
+}

+ 11 - 1
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductScrmMapper.java

@@ -284,7 +284,11 @@ public interface FsStoreProductScrmMapper
 
     List<FsStoreProductListQueryVO> selectFsStoreProductNewQuery(Map<String, Object> params);
 
+    List<FsStoreProductListQueryVO> selectFsStoreProductNewQueryPage(Map<String, Object> params);
+
     List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(Map<String, Object> params);
+
+    List<FsStoreProductListQueryVO> selectFsStoreProductHotQueryPage(Map<String, Object> params);
     @Select("select p.* from fs_store_product_scrm p " +
             //新增审核状态及所属店铺审核状态
             "<if test='config.isAudit == \"1\" '>" +
@@ -412,7 +416,13 @@ public interface FsStoreProductScrmMapper
 
     List<FsStoreProductScrm> bulkCopyFsStoreProductByIds(Long[] productIds);
 
-    List<FsStoreProductScrm> getStoreProductInProductIds(List<Long> productIds);
+    List<FsStoreProductScrm> getStoreProductInProductIds(@Param("productIds") List<Long> productIds);
+
+    /** 按商品ID列表查询,仅返回上线且未删除的商品(is_del=0, is_show=1),用于 App 列表 */
+    List<FsStoreProductScrm> getStoreProductInProductIdsForApp(@Param("productIds") List<Long> productIds);
+
+    /** 用于首页商品列表「全部」:分页查上架商品ID(is_del=0, is_show=1),配合 PageHelper */
+    List<Long> selectProductIdsForApp();
 
     @Select({"<script> " +
             "SELECT distinct fsp.* FROM fs_store_product_scrm fsp " +

+ 28 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductTagRelationScrmMapper.java

@@ -0,0 +1,28 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.domain.FsStoreProductTagRelationScrm;
+import com.fs.hisStore.vo.FsStoreProductTagNameVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 商品-标签关联 Mapper(一个商品最多3个标签)
+ */
+public interface FsStoreProductTagRelationScrmMapper {
+
+    int insertBatch(@Param("productId") Long productId, @Param("tagIds") List<Long> tagIds);
+
+    int deleteByProductId(Long productId);
+
+    List<Long> selectTagIdsByProductId(Long productId);
+
+    /** 统计某标签关联的商品数量 */
+    int countByTagId(Long tagId);
+
+    /** 按标签ID删除关联(删除标签时调用) */
+    int deleteByTagIds(@Param("tagIds") Long[] tagIds);
+
+    /** 按商品ID列表批量查询商品对应的标签名称(关联 fs_store_product_tag_scrm) */
+    List<FsStoreProductTagNameVO> selectProductTagNamesByProductIds(@Param("productIds") List<Long> productIds);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductTagScrmMapper.java

@@ -0,0 +1,29 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.domain.FsStoreProductTagScrm;
+import com.fs.hisStore.vo.FsStoreProductTagListVO;
+import com.fs.hisStore.vo.FsStoreTagProductVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 商品标签 Mapper
+ */
+public interface FsStoreProductTagScrmMapper {
+
+    FsStoreProductTagScrm selectById(Long id);
+
+    List<FsStoreProductTagListVO> selectListVO(FsStoreProductTagScrm query);
+
+    int insert(FsStoreProductTagScrm entity);
+
+    int update(FsStoreProductTagScrm entity);
+
+    int deleteById(Long id);
+
+    int deleteByIds(Long[] ids);
+
+    /** 查询标签下关联商品(分页:商品ID、名称、主图、售价、状态) */
+    List<FsStoreTagProductVO> selectProductsByTagId(@Param("tagId") Long tagId);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreProductUserEndCategoryMapper.java

@@ -0,0 +1,27 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.domain.FsStoreProductUserEndCategory;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 商品-用户分端类关联 Mapper
+ */
+public interface FsStoreProductUserEndCategoryMapper {
+
+    int insertBatch(@Param("productId") Long productId, @Param("categoryIds") List<Long> categoryIds);
+
+    int deleteByProductId(Long productId);
+
+    List<Long> selectCategoryIdsByProductId(Long productId);
+
+    /** 按用户端分类ID删除关联(删除分类时调用) */
+    int deleteByCategoryIds(@Param("categoryIds") Long[] categoryIds);
+
+    /** 按用户端分类ID查询去重后的商品ID列表(用于分页,配合 PageHelper) */
+    List<Long> selectDistinctProductIdsByCategoryId(@Param("categoryId") Long categoryId);
+
+    /** 关联表内全部去重商品ID(不按分类,配合 PageHelper 用于「全部」) */
+    List<Long> selectDistinctProductIds();
+}

+ 30 - 0
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreUserEndCategoryScrmMapper.java

@@ -0,0 +1,30 @@
+package com.fs.hisStore.mapper;
+
+import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 商品用户分端类 Mapper
+ */
+public interface FsStoreUserEndCategoryScrmMapper {
+
+    FsStoreUserEndCategoryScrm selectById(Long id);
+
+    List<FsStoreUserEndCategoryScrm> selectList(FsStoreUserEndCategoryScrm query);
+
+    int insert(FsStoreUserEndCategoryScrm entity);
+
+    int update(FsStoreUserEndCategoryScrm entity);
+
+    int deleteById(Long id);
+
+    int deleteByIds(Long[] ids);
+
+    /** 用于添加商品时选择:按 position 分组,各取前8条(status=1),排序:sort asc, create_time desc */
+    List<FsStoreUserEndCategoryScrm> selectListForProduct(@Param("storeId") Long storeId);
+
+    /** 用户端展示:按位置取前8条,status=1,排序 sort asc, create_time desc */
+    List<FsStoreUserEndCategoryScrm> selectTop8ByPosition(@Param("storeId") Long storeId, @Param("position") Integer position);
+}

+ 5 - 0
fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java

@@ -302,4 +302,9 @@ public class FsStoreProductAddEditParam implements Serializable
     /** 所属小程序app_id,多个用逗号隔开 */
     private String appIds;
 
+    /** 用户端分类ID列表(金刚区/瀑布流多选) */
+    private List<Long> userEndCategoryIds;
+
+    /** 商品标签ID列表(最多3个) */
+    private List<Long> tagIds;
 }

+ 12 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductScrmService.java

@@ -101,6 +101,12 @@ public interface IFsStoreProductScrmService
 
     List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(int count, String appId);
 
+    /** 绿色有机分页(与 selectFsStoreProductNewQuery 条件一致,支持分页) */
+    List<FsStoreProductListQueryVO> selectFsStoreProductNewQueryPage(int pageNum, int pageSize, String appId);
+
+    /** 上新推荐分页(与 selectFsStoreProductHotQuery 条件一致,支持分页) */
+    List<FsStoreProductListQueryVO> selectFsStoreProductHotQueryPage(int pageNum, int pageSize, String appId);
+
     List<FsStoreProductListQueryVO> selectFsStoreProductGoodQuery(int count);
 
     List<FsStoreProductListQueryVO> selectFsStoreProductTuiListQuery(BaseQueryParam param);
@@ -148,4 +154,10 @@ public interface IFsStoreProductScrmService
     R copyStoreProduct(Long productId);
 
     R updateCache(Long productId);
+
+    /** 查询商品关联的用户端分类ID列表 */
+    List<Long> selectUserEndCategoryIdsByProductId(Long productId);
+
+    /** 查询商品关联的标签ID列表 */
+    List<Long> selectTagIdsByProductId(Long productId);
 }

+ 31 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreProductTagScrmService.java

@@ -0,0 +1,31 @@
+package com.fs.hisStore.service;
+
+import com.fs.hisStore.domain.FsStoreProductTagScrm;
+import com.fs.hisStore.vo.FsStoreProductTagListVO;
+import com.fs.hisStore.vo.FsStoreTagProductVO;
+
+import java.util.List;
+
+/**
+ * 商品标签 Service 接口
+ */
+public interface IFsStoreProductTagScrmService {
+
+    FsStoreProductTagScrm selectById(Long id);
+
+    List<FsStoreProductTagListVO> selectListVO(FsStoreProductTagScrm query);
+
+    /** 分页列表 */
+    List<FsStoreProductTagListVO> selectListVOPage(FsStoreProductTagScrm query);
+
+    /** 标签下关联商品(分页:商品ID、名称、主图、售价、状态) */
+    List<FsStoreTagProductVO> selectProductsByTagId(Long tagId);
+
+    int insert(FsStoreProductTagScrm entity);
+
+    int update(FsStoreProductTagScrm entity);
+
+    int deleteById(Long id);
+
+    int deleteByIds(Long[] ids);
+}

+ 39 - 0
fs-service/src/main/java/com/fs/hisStore/service/IFsStoreUserEndCategoryScrmService.java

@@ -0,0 +1,39 @@
+package com.fs.hisStore.service;
+
+import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.vo.FsStoreUserEndCategoryProductVO;
+
+import java.util.List;
+
+/**
+ * 商品用户分端类 Service 接口
+ */
+public interface IFsStoreUserEndCategoryScrmService {
+
+    FsStoreUserEndCategoryScrm selectById(Long id);
+
+    List<FsStoreUserEndCategoryScrm> selectList(FsStoreUserEndCategoryScrm query);
+
+    /** 分页列表:一页10条,排序值相同按创建时间倒序 */
+    List<FsStoreUserEndCategoryScrm> selectListPage(FsStoreUserEndCategoryScrm query);
+
+    /** 添加商品时选择:按金刚区/瀑布流分组,各最多8条(仅展示用由前端截取) */
+    List<FsStoreUserEndCategoryScrm> selectListForProduct(Long storeId);
+
+    /** 用户端首页:按位置(1金刚区 2瀑布流)取前8条,status=1,排序 sort asc, create_time desc */
+    List<FsStoreUserEndCategoryScrm> selectTop8ByPosition(Long storeId, Integer position);
+
+    /** 按用户端分类ID分页查询关联商品(去重商品ID分页,再查商品简表+标签并组装) */
+    List<FsStoreUserEndCategoryProductVO> listProductsByCategoryId(Long categoryId);
+
+    /** 首页商品列表:id 为空查全部(分页商品ID后查简表+标签),id 不为空按用户端分类查;返回 list+total */
+    java.util.Map<String, Object> listProductsForApp(Long id, Integer pageNum, Integer pageSize);
+
+    int insert(FsStoreUserEndCategoryScrm entity);
+
+    int update(FsStoreUserEndCategoryScrm entity);
+
+    int deleteById(Long id);
+
+    int deleteByIds(Long[] ids);
+}

+ 60 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java

@@ -127,6 +127,11 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     @Autowired
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
 
+    @Autowired
+    private FsStoreProductUserEndCategoryMapper fsStoreProductUserEndCategoryMapper;
+    @Autowired
+    private FsStoreProductTagRelationScrmMapper fsStoreProductTagRelationScrmMapper;
+
     @Autowired
     @Lazy
     private ILiveService liveService;
@@ -252,6 +257,14 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     {
         storeAuditLogUtil.addBatchAuditArray(productIds, "", "");
         log.info("批量删除商品:{}", productIds);
+        if (productIds != null) {
+            for (Long pid : productIds) {
+                if (pid != null) {
+                    fsStoreProductUserEndCategoryMapper.deleteByProductId(pid);
+                    fsStoreProductTagRelationScrmMapper.deleteByProductId(pid);
+                }
+            }
+        }
         int result = fsStoreProductMapper.deleteFsStoreProductByIds(productIds);
 
         // 清除缓存
@@ -512,6 +525,10 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     @Override
     public int deleteFsStoreProductById(Long productId)
     {
+        if (productId != null) {
+            fsStoreProductUserEndCategoryMapper.deleteByProductId(productId);
+            fsStoreProductTagRelationScrmMapper.deleteByProductId(productId);
+        }
         int result = fsStoreProductMapper.deleteFsStoreProductById(productId);
         // 清除缓存
         clearProductDetailCache(productId);
@@ -759,6 +776,19 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         } else {
             addProductAttr(product.getProductId(),param.getItems(),param.getValues());
         }
+        // 保存用户端分类关联
+        fsStoreProductUserEndCategoryMapper.deleteByProductId(product.getProductId());
+        if (param.getUserEndCategoryIds() != null && !param.getUserEndCategoryIds().isEmpty()) {
+            fsStoreProductUserEndCategoryMapper.insertBatch(product.getProductId(), param.getUserEndCategoryIds());
+        }
+        // 保存商品标签关联(最多3个)
+        fsStoreProductTagRelationScrmMapper.deleteByProductId(product.getProductId());
+        if (param.getTagIds() != null && !param.getTagIds().isEmpty()) {
+            if (param.getTagIds().size() > 3) {
+                throw new ServiceException("商品标签最多选择3个");
+            }
+            fsStoreProductTagRelationScrmMapper.insertBatch(product.getProductId(), param.getTagIds());
+        }
         // 数据修改缓存
         if (product.getProductId() != null) {
             FsStoreProductScrm cacheProduct = fsStoreProductMapper.selectFsStoreProductById(product.getProductId());
@@ -1141,6 +1171,24 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         return fsStoreProductMapper.selectFsStoreProductHotQuery(map);
     }
 
+    @Override
+    public List<FsStoreProductListQueryVO> selectFsStoreProductNewQueryPage(int pageNum, int pageSize, String appId) {
+        HashMap<String, Object> map = new HashMap<>();
+        map.put("config", medicalMallConfig);
+        map.put("appId", appId);
+        com.github.pagehelper.PageHelper.startPage(pageNum, pageSize);
+        return fsStoreProductMapper.selectFsStoreProductNewQueryPage(map);
+    }
+
+    @Override
+    public List<FsStoreProductListQueryVO> selectFsStoreProductHotQueryPage(int pageNum, int pageSize, String appId) {
+        HashMap<String, Object> map = new HashMap<>();
+        map.put("config", medicalMallConfig);
+        map.put("appId", appId);
+        com.github.pagehelper.PageHelper.startPage(pageNum, pageSize);
+        return fsStoreProductMapper.selectFsStoreProductHotQueryPage(map);
+    }
+
     @Override
     public List<FsStoreProductListQueryVO> selectFsStoreProductGoodQuery(int count) {
         return fsStoreProductMapper.selectFsStoreProductGoodQuery(count,medicalMallConfig);
@@ -1636,4 +1684,16 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         }
         return R.ok();
     }
+
+    @Override
+    public List<Long> selectUserEndCategoryIdsByProductId(Long productId) {
+        if (productId == null) return Collections.emptyList();
+        return fsStoreProductUserEndCategoryMapper.selectCategoryIdsByProductId(productId);
+    }
+
+    @Override
+    public List<Long> selectTagIdsByProductId(Long productId) {
+        if (productId == null) return Collections.emptyList();
+        return fsStoreProductTagRelationScrmMapper.selectTagIdsByProductId(productId);
+    }
 }

+ 79 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductTagScrmServiceImpl.java

@@ -0,0 +1,79 @@
+package com.fs.hisStore.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.hisStore.domain.FsStoreProductTagScrm;
+import com.fs.hisStore.mapper.FsStoreProductTagRelationScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductTagScrmMapper;
+import com.fs.hisStore.service.IFsStoreProductTagScrmService;
+import com.fs.hisStore.vo.FsStoreProductTagListVO;
+import com.fs.hisStore.vo.FsStoreTagProductVO;
+import com.fs.common.exception.ServiceException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 商品标签 Service 实现
+ */
+@Service
+public class FsStoreProductTagScrmServiceImpl implements IFsStoreProductTagScrmService {
+
+    @Autowired
+    private FsStoreProductTagScrmMapper mapper;
+
+    @Autowired
+    private FsStoreProductTagRelationScrmMapper productTagRelationMapper;
+
+    @Override
+    public FsStoreProductTagScrm selectById(Long id) {
+        return mapper.selectById(id);
+    }
+
+    @Override
+    public List<FsStoreProductTagListVO> selectListVO(FsStoreProductTagScrm query) {
+        return mapper.selectListVO(query);
+    }
+
+    @Override
+    public List<FsStoreProductTagListVO> selectListVOPage(FsStoreProductTagScrm query) {
+        return mapper.selectListVO(query);
+    }
+
+    @Override
+    public List<FsStoreTagProductVO> selectProductsByTagId(Long tagId) {
+        return mapper.selectProductsByTagId(tagId);
+    }
+
+    @Override
+    public int insert(FsStoreProductTagScrm entity) {
+        if (entity.getTagName() != null && entity.getTagName().length() > 4) {
+            throw new ServiceException("标签名称最多4个字");
+        }
+        entity.setCreateTime(DateUtils.getNowDate());
+        entity.setUpdateTime(DateUtils.getNowDate());
+        return mapper.insert(entity);
+    }
+
+    @Override
+    public int update(FsStoreProductTagScrm entity) {
+        if (entity.getTagName() != null && entity.getTagName().length() > 4) {
+            throw new ServiceException("标签名称最多4个字");
+        }
+        entity.setUpdateTime(DateUtils.getNowDate());
+        return mapper.update(entity);
+    }
+
+    @Override
+    public int deleteById(Long id) {
+        productTagRelationMapper.deleteByTagIds(new Long[]{id});
+        return mapper.deleteById(id);
+    }
+
+    @Override
+    public int deleteByIds(Long[] ids) {
+        if (ids == null || ids.length == 0) return 0;
+        productTagRelationMapper.deleteByTagIds(ids);
+        return mapper.deleteByIds(ids);
+    }
+}

+ 163 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreUserEndCategoryScrmServiceImpl.java

@@ -0,0 +1,163 @@
+package com.fs.hisStore.service.impl;
+
+import com.fs.common.utils.DateUtils;
+import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductTagRelationScrmMapper;
+import com.fs.hisStore.mapper.FsStoreProductUserEndCategoryMapper;
+import com.fs.hisStore.mapper.FsStoreUserEndCategoryScrmMapper;
+import com.fs.hisStore.service.IFsStoreUserEndCategoryScrmService;
+import com.fs.hisStore.vo.FsStoreProductTagNameVO;
+import com.fs.hisStore.vo.FsStoreUserEndCategoryProductVO;
+import com.github.pagehelper.Page;
+import com.github.pagehelper.PageHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 商品用户分端类 Service 实现
+ */
+@Service
+public class FsStoreUserEndCategoryScrmServiceImpl implements IFsStoreUserEndCategoryScrmService {
+
+    @Autowired
+    private FsStoreUserEndCategoryScrmMapper mapper;
+
+    @Autowired
+    private FsStoreProductUserEndCategoryMapper productUserEndCategoryMapper;
+
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
+
+    @Autowired
+    private FsStoreProductTagRelationScrmMapper productTagRelationMapper;
+
+    @Override
+    public List<FsStoreUserEndCategoryProductVO> listProductsByCategoryId(Long categoryId) {
+        if (categoryId == null) return new ArrayList<>();
+        List<Long> productIds = productUserEndCategoryMapper.selectDistinctProductIdsByCategoryId(categoryId);
+        if (productIds == null || productIds.isEmpty()) return new ArrayList<>();
+        List<FsStoreProductScrm> products = fsStoreProductScrmMapper.getStoreProductInProductIds(productIds);
+        Map<Long, FsStoreProductScrm> productMap = products.stream().collect(Collectors.toMap(FsStoreProductScrm::getProductId, p -> p, (a, b) -> a));
+        List<FsStoreProductTagNameVO> tagNames = productTagRelationMapper.selectProductTagNamesByProductIds(productIds);
+        Map<Long, List<String>> tagMap = new LinkedHashMap<>();
+        for (FsStoreProductTagNameVO tn : tagNames) {
+            tagMap.computeIfAbsent(tn.getProductId(), k -> new ArrayList<>()).add(tn.getTagName());
+        }
+        List<FsStoreUserEndCategoryProductVO> result = new ArrayList<>();
+        for (Long pid : productIds) {
+            FsStoreProductScrm p = productMap.get(pid);
+            if (p == null) continue;
+            FsStoreUserEndCategoryProductVO vo = new FsStoreUserEndCategoryProductVO();
+            vo.setProductId(p.getProductId());
+            vo.setProductName(p.getProductName());
+            vo.setImage(p.getImage());
+            vo.setPrice(p.getPrice());
+            vo.setOtPrice(p.getOtPrice());
+            vo.setSales(p.getSales());
+            vo.setTagList(tagMap.getOrDefault(pid, new ArrayList<>()));
+            result.add(vo);
+        }
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> listProductsForApp(Long id, Integer pageNum, Integer pageSize) {
+        Map<String, Object> out = new HashMap<>();
+        out.put("list", new ArrayList<FsStoreUserEndCategoryProductVO>());
+        out.put("total", 0L);
+        if (pageNum == null || pageSize == null || pageSize <= 0) return out;
+        PageHelper.startPage(pageNum, pageSize);
+        List<Long> productIds = (id != null && id != 0L)
+                ? productUserEndCategoryMapper.selectDistinctProductIdsByCategoryId(id)
+                : productUserEndCategoryMapper.selectDistinctProductIds();
+        long total = productIds instanceof Page ? ((Page<?>) productIds).getTotal() : (productIds != null ? productIds.size() : 0);
+        if (productIds == null || productIds.isEmpty()) {
+            out.put("total", total);
+            return out;
+        }
+        List<FsStoreProductScrm> products = fsStoreProductScrmMapper.getStoreProductInProductIdsForApp(productIds);
+        Map<Long, FsStoreProductScrm> productMap = products.stream().collect(Collectors.toMap(FsStoreProductScrm::getProductId, p -> p, (a, b) -> a));
+        List<FsStoreProductTagNameVO> tagNames = productTagRelationMapper.selectProductTagNamesByProductIds(productIds);
+        Map<Long, List<String>> tagMap = new LinkedHashMap<>();
+        for (FsStoreProductTagNameVO tn : tagNames) {
+            tagMap.computeIfAbsent(tn.getProductId(), k -> new ArrayList<>()).add(tn.getTagName());
+        }
+        List<FsStoreUserEndCategoryProductVO> result = new ArrayList<>();
+        for (Long pid : productIds) {
+            FsStoreProductScrm p = productMap.get(pid);
+            if (p == null) continue;
+            FsStoreUserEndCategoryProductVO vo = new FsStoreUserEndCategoryProductVO();
+            vo.setProductId(p.getProductId());
+            vo.setProductName(p.getProductName());
+            vo.setImage(p.getImage());
+            vo.setPrice(p.getPrice());
+            vo.setOtPrice(p.getOtPrice());
+            vo.setSales(p.getSales());
+            vo.setTagList(tagMap.getOrDefault(pid, new ArrayList<>()));
+            result.add(vo);
+        }
+        out.put("list", result);
+        out.put("total", total);
+        return out;
+    }
+
+    @Override
+    public FsStoreUserEndCategoryScrm selectById(Long id) {
+        return mapper.selectById(id);
+    }
+
+    @Override
+    public List<FsStoreUserEndCategoryScrm> selectList(FsStoreUserEndCategoryScrm query) {
+        return mapper.selectList(query);
+    }
+
+    @Override
+    public List<FsStoreUserEndCategoryScrm> selectListPage(FsStoreUserEndCategoryScrm query) {
+        return mapper.selectList(query);
+    }
+
+    @Override
+    public List<FsStoreUserEndCategoryScrm> selectListForProduct(Long storeId) {
+        return mapper.selectListForProduct(storeId);
+    }
+
+    @Override
+    public List<FsStoreUserEndCategoryScrm> selectTop8ByPosition(Long storeId, Integer position) {
+        return mapper.selectTop8ByPosition(storeId, position);
+    }
+
+    @Override
+    public int insert(FsStoreUserEndCategoryScrm entity) {
+        entity.setCreateTime(DateUtils.getNowDate());
+        entity.setUpdateTime(DateUtils.getNowDate());
+        return mapper.insert(entity);
+    }
+
+    @Override
+    public int update(FsStoreUserEndCategoryScrm entity) {
+        entity.setUpdateTime(DateUtils.getNowDate());
+        return mapper.update(entity);
+    }
+
+    @Override
+    public int deleteById(Long id) {
+        productUserEndCategoryMapper.deleteByCategoryIds(new Long[]{id});
+        return mapper.deleteById(id);
+    }
+
+    @Override
+    public int deleteByIds(Long[] ids) {
+        if (ids == null || ids.length == 0) return 0;
+        productUserEndCategoryMapper.deleteByCategoryIds(ids);
+        return mapper.deleteByIds(ids);
+    }
+}

+ 23 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductTagListVO.java

@@ -0,0 +1,23 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商品标签列表VO(含关联商品数量)
+ */
+@Data
+public class FsStoreProductTagListVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long id;
+    private Long storeId;
+    private String tagName;
+    private Integer status;
+    private Long sort;
+    private java.util.Date createTime;
+    /** 关联商品数量 */
+    private Integer productCount;
+}

+ 17 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductTagNameVO.java

@@ -0,0 +1,17 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 商品-标签名称(用于按商品ID批量查标签)
+ */
+@Data
+public class FsStoreProductTagNameVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long productId;
+    private String tagName;
+}

+ 22 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreTagProductVO.java

@@ -0,0 +1,22 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 标签关联商品项(弹窗展示:商品ID、名称、主图、售价、状态)
+ */
+@Data
+public class FsStoreTagProductVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long productId;
+    private String productName;
+    private String image;
+    private BigDecimal price;
+    /** 状态:0-未上架 1-上架 */
+    private Integer isShow;
+}

+ 29 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreUserEndCategoryProductVO.java

@@ -0,0 +1,29 @@
+package com.fs.hisStore.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 用户端分类下的商品项:商品ID、名称、售价、原价、销量、产品标签列表
+ */
+@Data
+public class FsStoreUserEndCategoryProductVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long productId;
+    private String productName;
+    /** 主图(App 列表用) */
+    private String image;
+    /** 实际销售价格 */
+    private BigDecimal price;
+    /** 原价 */
+    private BigDecimal otPrice;
+    /** 销量 */
+    private Long sales;
+    /** 产品标签名称列表 */
+    private List<String> tagList;
+}

+ 8 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveDataMapper.java

@@ -10,6 +10,7 @@ import com.fs.live.vo.LiveDataDetailVo;
 import com.fs.live.vo.LiveDataListVo;
 import com.fs.live.vo.LiveDataStatisticsVo;
 import com.fs.live.vo.LiveUserDetailVo;
+import com.fs.live.vo.LiveAppSimpleVO;
 import com.fs.live.vo.RecentLiveDataVo;
 import com.fs.live.vo.TrendDataVO;
 import org.apache.ibatis.annotations.Param;
@@ -238,4 +239,11 @@ public interface LiveDataMapper {
     @DataSource(DataSourceType.SLAVE)
     List<LiveDataCompanyVO> selectCompanyEmployeeCountByLiveIds(@Param("liveIds") List<Long> liveIds,
                                                                 @Param("companyIds") List<Long> companyIds);
+
+    /**
+     * 查询正在直播的直播间(仅 liveId、liveType、封面、标题),用于首页推荐
+     * status=2 表示直播中
+     */
+    @DataSource(DataSourceType.SLAVE)
+    List<LiveAppSimpleVO> selectLivingLivesForApp();
 }

+ 5 - 0
fs-service/src/main/java/com/fs/live/service/ILiveDataService.java

@@ -171,6 +171,11 @@ public interface ILiveDataService {
 
     List<LiveDataListVo> exportLiveData(LiveDataParam param);
 
+    /**
+     * 首页推荐:正在直播的直播间列表,仅返回 liveId/liveType/封面/标题;当 liveType=2 时 liveFlag 从 Redis 查询
+     */
+    List<LiveAppSimpleVO> listLivingLivesForApp();
+
     /**
      * 查询分公司直播数据统计列表
      * @param param 查询参数

+ 24 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveDataServiceImpl.java

@@ -16,6 +16,7 @@ import com.fs.live.service.ILiveDataService;
 import com.fs.live.service.ILiveUserFavoriteService;
 import com.fs.live.service.ILiveUserFollowService;
 import com.fs.live.service.ILiveUserLikeService;
+import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.*;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
@@ -78,6 +79,8 @@ public class LiveDataServiceImpl implements ILiveDataService {
     @Autowired
     private ILiveUserFavoriteService liveUserFavoriteService;
     @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+    @Autowired
     private LiveDataMapper baseMapper;
     @Autowired
     private LiveUserFirstEntryMapper liveUserFirstEntryMapper;
@@ -571,6 +574,27 @@ public class LiveDataServiceImpl implements ILiveDataService {
         return baseMapper.getAllLiveDatas();
     }
 
+    @Override
+    public List<LiveAppSimpleVO> listLivingLivesForApp() {
+        List<LiveAppSimpleVO> list = liveDataMapper.selectLivingLivesForApp();
+        if (list == null || list.isEmpty()) {
+            return list;
+        }
+        for (LiveAppSimpleVO vo : list) {
+            if (vo.getLiveType() != null && vo.getLiveType() == 2 && vo.getLiveId() != null) {
+                try {
+                    Map<String, Integer> flagMap = liveWatchUserService.getLiveFlagWithCache(vo.getLiveId());
+                    if (flagMap != null && flagMap.containsKey("liveFlag")) {
+                        vo.setLiveFlag(flagMap.get("liveFlag"));
+                    }
+                } catch (Exception e) {
+                    log.warn("getLiveFlagWithCache fail liveId={}", vo.getLiveId(), e);
+                }
+            }
+        }
+        return list;
+    }
+
     @Override
     public void updateBatchById(List<LiveData> liveDatas) {
         baseMapper.updateBatchLiveData(liveDatas);

+ 18 - 0
fs-service/src/main/java/com/fs/live/vo/LiveAppSimpleVO.java

@@ -0,0 +1,18 @@
+package com.fs.live.vo;
+
+import lombok.Data;
+
+/**
+ * 首页推荐-正在直播的直播间简要信息(liveId、liveType、封面、标题、liveFlag当liveType=2时从Redis取)
+ */
+@Data
+public class LiveAppSimpleVO {
+    private Long liveId;
+    private Integer liveType;
+    /** 直播间封面图片 */
+    private String liveImgUrl;
+    /** 直播间标题 */
+    private String liveName;
+    /** 当 liveType=2 时由 Redis 查询填充 */
+    private Integer liveFlag;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/vo/QwSopCourseFinishTempSetting.java

@@ -139,6 +139,21 @@ public class QwSopCourseFinishTempSetting implements Serializable,Cloneable{
          * 群id
          */
         private String chatId;
+        // 短信CODE
+        private String smsTemplateCode;
+
+        /**
+         * 短信模板id
+         */
+        private Integer smsTemplateId;
+        /**
+         * 短信模板标题
+         */
+        private String smsTemplateTitle;
+        /**
+         * 短信模板内容
+         */
+        private String smsTemplateContent;
         @Override
         public Setting clone() {
             try {

+ 14 - 0
fs-service/src/main/java/com/fs/qw/vo/QwSopTempSetting.java

@@ -161,7 +161,21 @@ public class QwSopTempSetting implements Serializable{
              * 业务id
              */
             private String businessId;
+            // 短信CODE
+            private String smsTemplateCode;
 
+            /**
+             * 短信模板id
+             */
+            private Integer smsTemplateId;
+            /**
+             * 短信模板标题
+             */
+            private String smsTemplateTitle;
+            /**
+             * 短信模板内容
+             */
+            private String smsTemplateContent;
 
             @Override
             public Setting clone() {

+ 78 - 1
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -65,6 +65,7 @@ import com.fs.sop.vo.SopUserLogsVo;
 import com.fs.system.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.fs.voice.utils.StringUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -1545,6 +1546,25 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 log.error("浏览器看课模板解析失败:" + e);
                             }
 
+                            break;
+                        case "21":
+                            if (sopLogs.getFsUserId() != null && !Long.valueOf(0L).equals(sopLogs.getFsUserId())) {
+                                addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(), item.getFsUserId(), qwUserId, companyUserId, companyId,
+                                        item.getExternalId(), item.getStartTime(), createTime,2);
+                                String link = createSmsShortLink(st, param.getCorpId(), createTime, param.getCourseId(), param.getVideoId(),
+                                        String.valueOf(qwUser.getId()), companyUserId, companyId, item.getExternalId(), config);
+                                if (StringUtils.isNotBlank(link)) {
+                                    if(StringUtils.isNotBlank(st.getSmsTemplateContent()) && st.getSmsTemplateContent().contains("${sms.courseUrl}")){
+                                        st.setValue(st.getSmsTemplateContent()
+                                                .replaceAll("【(.*?)】", "【" + config.getSmsDomain() + "】")
+                                                .replace("${sms.courseUrl}", link));
+                                    }else{
+                                        log.error("生成看课短链时检测到短信模板选择错误,跳过设置 URL。");
+                                    }
+                                } else {
+                                    log.error("生成看课短链失败,跳过设置 URL。");
+                                }
+                            }
                             break;
                         //群公告(仅用于一键群发,个人不应该有群公告)
                         case "11":
@@ -2240,7 +2260,26 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         log.error("赋值-小程序封面地址失败-" + e);
                     }
                     break;
-
+                case "21":
+                    if (sopLogs.getFsUserId() != null && !Long.valueOf(0L).equals(sopLogs.getFsUserId())) {
+                        addWatchLogIfNeeded(item.getSopId(), param.getVideoId(), param.getCourseId(), item.getFsUserId(), String.valueOf(qwUser.getId()), companyUserId, companyId,
+                                item.getExternalId(), item.getStartTime(), dataTime,2);
+                        String link = createSmsShortLink(st, param.getCorpId(), dataTime, param.getCourseId(), param.getVideoId(),
+                                String.valueOf(qwUser.getId()), companyUserId, companyId, item.getExternalId(), config);
+                        if (StringUtils.isNotBlank(link)) {
+                            if(StringUtils.isNotBlank(st.getSmsTemplateContent()) && st.getSmsTemplateContent().contains("${sms.courseUrl}")){
+//                                st.setValue(st.getSmsTemplateContent().replace("${sms.courseUrl}", link));
+                                st.setValue(st.getSmsTemplateContent()
+                                        .replaceAll("【(.*?)】", "【" + config.getSmsDomain() + "】")
+                                        .replace("${sms.courseUrl}", link));
+                            }else{
+                                log.error("生成看课短链时检测到短信模板选择错误,跳过设置 URL。");
+                            }
+                        } else {
+                            log.error("生成看课短链失败,跳过设置 URL。");
+                        }
+                    }
+                    break;
                 default:
                     break;
 
@@ -2584,6 +2623,44 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
     }
 
+    private String createSmsShortLink(QwSopCourseFinishTempSetting.Setting setting, String corpId, Date sendTime,
+                                      Integer courseId, Integer videoId, String qwUserId,
+                                      String companyUserId, String companyId, Long externalId, CourseConfig config) {
+
+        FsCourseLink link = new FsCourseLink();
+        link.setCompanyId(Long.parseLong(companyId));
+        link.setQwUserId(Long.valueOf(qwUserId));
+        link.setCompanyUserId(Long.parseLong(companyUserId));
+        link.setVideoId(videoId.longValue());
+        link.setCorpId(corpId);
+        link.setCourseId(courseId.longValue());
+        link.setQwExternalId(externalId);
+        link.setLinkType(0); //正常链接
+        link.setUNo(UUID.randomUUID().toString());
+        String randomString = ShortCodeGeneratorUtils.generate8();
+        if (StringUtil.strIsNullOrEmpty(randomString)) {
+            link.setLink(UUID.randomUUID().toString().replace("-", ""));
+        } else {
+            link.setLink(randomString);
+        }
+        link.setCreateTime(sendTime);;
+        Date updateTime = createUpdateTime(setting, sendTime, config);
+        link.setUpdateTime(updateTime);
+
+
+        FsCourseRealLink courseMap = new FsCourseRealLink();
+        BeanUtils.copyProperties(link, courseMap);
+
+        String realLinkFull = registeredRealLink + JSON.toJSONString(courseMap);
+        link.setRealLink(realLinkFull);
+        fsCourseLinkMapper.insertFsCourseLink(link);
+        if(StringUtils.isEmpty(config.getSmsDomainName())){
+            log.error("检测到未配置看课短信链接域名");
+            return null;
+        }
+        return config.getSmsDomainName() + "/" + link.getLink();
+    }
+
     private QwCreateLinkByAppVO createLinkByApp(QwSopCourseFinishTempSetting.Setting setting, String corpId,
                                                 Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                                 String companyUserId, String companyId, Long externalId,

+ 424 - 0
fs-service/src/main/java/com/fs/utils/ShortCodeGeneratorUtils.java

@@ -0,0 +1,424 @@
+package com.fs.utils;
+
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+/**
+ * 短码生成工具类 - 高并发、高唯一性短码生成器
+ * 修复版本:解决数组越界和线程获取问题
+ *
+ * @author ToolKit
+ * @version 1.0.2
+ */
+public final class ShortCodeGeneratorUtils {
+
+    // 私有构造器,防止实例化
+    private ShortCodeGeneratorUtils() {
+        throw new AssertionError("Cannot instantiate utility class");
+    }
+
+    /**
+     * 字符集定义枚举
+     */
+    public enum CharsetType {
+
+        /** URL安全字符集(推荐) */
+        URL_SAFE("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
+
+        private final String chars;
+
+        CharsetType(String chars) {
+            this.chars = chars;
+        }
+
+        public String getChars() {
+            return chars;
+        }
+
+        public char[] getCharArray() {
+            return chars.toCharArray();
+        }
+
+        public int size() {
+            return chars.length();
+        }
+    }
+
+    // 默认配置
+    private static final CharsetType DEFAULT_CHARSET = CharsetType.URL_SAFE;
+
+    // 单例实例(延迟加载)
+    private static class InstanceHolder {
+        static final ShortCodeGenerator INSTANCE = new ShortCodeGenerator(DEFAULT_CHARSET);
+    }
+
+    /**
+     * 获取默认生成器实例
+     */
+    public static ShortCodeGenerator getInstance() {
+        return InstanceHolder.INSTANCE;
+    }
+
+    /**
+     * 获取指定字符集的生成器实例
+     */
+    public static ShortCodeGenerator getInstance(CharsetType charsetType) {
+        return new ShortCodeGenerator(charsetType);
+    }
+
+    /**
+     * 快速生成8位短码(使用默认配置)
+     */
+    public static String generate8() {
+        return getInstance().generate8CharCode();
+    }
+
+    /**
+     * 快速生成6位短码(使用默认配置)
+     */
+    public static String generate6() {
+        return getInstance().generate6CharCode();
+    }
+
+    /**
+     * 快速生成指定长度短码(使用默认配置)
+     */
+    public static String generate(int length) {
+        return getInstance().generateCode(length);
+    }
+
+    /**
+     * 短码生成器核心实现 - 完全修复版本
+     */
+    public static class ShortCodeGenerator {
+
+        private final char[] charset;
+        private final int charsetSize;
+        private final AtomicLong counter = new AtomicLong(0);
+        private volatile long lastTimestamp = 0;
+        private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+        private final SecureRandom secureRandom;
+        private static final long PROCESS_ID = getProcessId();
+
+        /**
+         * 使用默认字符集创建生成器
+         */
+        public ShortCodeGenerator() {
+            this(DEFAULT_CHARSET);
+        }
+
+        /**
+         * 使用指定字符集创建生成器
+         */
+        public ShortCodeGenerator(CharsetType charsetType) {
+            this.charset = charsetType.getCharArray();
+            this.charsetSize = charsetType.size();
+            this.secureRandom = new SecureRandom();
+            // 初始化安全随机数生成器
+            secureRandom.nextBytes(new byte[16]);
+        }
+
+        /**
+         * 生成8位短码
+         */
+        public String generate8CharCode() {
+            return generateCode(8);
+        }
+
+        /**
+         * 生成6位短码
+         */
+        public String generate6CharCode() {
+            return generateCode(6);
+        }
+
+        /**
+         * 生成指定长度短码
+         */
+        public String generateCode(int length) {
+            validateLength(length);
+
+            try {
+                // 组合多种生成策略降低碰撞概率
+                String codeByTime = generateByTimestamp(length);
+                String codeByHash = generateByHash(length);
+
+                return mixAndFinalize(codeByTime, codeByHash, length);
+            } catch (Exception e) {
+                // 如果任何方法失败,使用安全的降级方案
+                return generateSafeFallbackCode(length);
+            }
+        }
+
+        /**
+         * 批量生成短码
+         */
+        public String[] generateBatch(int count, int length) {
+            validateLength(length);
+
+            String[] codes = new String[count];
+
+            for (int i = 0; i < count; i++) {
+                codes[i] = generateCode(length);
+            }
+
+            return codes;
+        }
+
+        /**
+         * 获取字符集大小
+         */
+        public int getCharsetSize() {
+            return charsetSize;
+        }
+
+        /**
+         * 获取字符集
+         */
+        public String getCharsetString() {
+            return new String(charset);
+        }
+
+        /**
+         * 验证长度参数
+         */
+        private void validateLength(int length) {
+            if (length < 4 || length > 16) {
+                throw new IllegalArgumentException("Length must be between 4 and 16");
+            }
+        }
+
+        /**
+         * 安全的降级生成方案
+         */
+        private String generateSafeFallbackCode(int length) {
+            char[] result = new char[length];
+            ThreadLocalRandom random = ThreadLocalRandom.current();
+
+            for (int i = 0; i < length; i++) {
+                // 使用安全的随机数生成,确保索引为正数
+                int index = random.nextInt(charsetSize);
+                result[i] = charset[index];
+            }
+
+            return new String(result);
+        }
+
+        /**
+         * 基于时间戳生成 - 修复线程ID获取问题
+         */
+        private String generateByTimestamp(int length) {
+            long currentTime = System.currentTimeMillis();
+            long sequence;
+
+            lock.writeLock().lock();
+            try {
+                if (currentTime == lastTimestamp) {
+                    sequence = counter.incrementAndGet();
+                } else {
+                    lastTimestamp = currentTime;
+                    counter.set(0);
+                    sequence = 0;
+                }
+            } finally {
+                lock.writeLock().unlock();
+            }
+
+            ThreadLocalRandom random = ThreadLocalRandom.current();
+            long randomPart = random.nextLong();
+
+            // 获取当前线程ID
+            long threadId = Thread.currentThread().getId();
+
+            // 组合各种熵源
+            long combined = currentTime ^ (sequence << 16) ^ randomPart;
+            combined ^= System.nanoTime();
+            combined ^= (PROCESS_ID << 48);
+            combined ^= (threadId << 32);
+
+            // 确保为正数
+            combined = Math.abs(combined);
+            if (combined == Long.MIN_VALUE) {
+                combined = Long.MAX_VALUE; // 处理边界情况
+            }
+
+            return convertToBaseSafe(combined, length);
+        }
+
+        /**
+         * 基于哈希算法生成 - 修复线程ID获取问题
+         */
+        private String generateByHash(int length) {
+            try {
+                MessageDigest md = MessageDigest.getInstance("SHA-256");
+
+                // 使用多个熵源确保唯一性
+                StringBuilder entropy = new StringBuilder();
+                entropy.append(System.nanoTime())
+                        .append(Thread.currentThread().getId())  // 修复这里
+                        .append(System.identityHashCode(this))
+                        .append(secureRandom.nextLong())
+                        .append(counter.get())
+                        .append(Runtime.getRuntime().freeMemory());
+
+                byte[] hash = md.digest(entropy.toString().getBytes());
+
+                // 使用BigInteger避免负数和溢出问题
+                BigInteger bigInt = new BigInteger(1, hash); // 正数模式
+                BigInteger modValue = BigInteger.valueOf(charsetSize).pow(length);
+
+                // 取模确保值在范围内
+                BigInteger result = bigInt.mod(modValue);
+
+                return convertBigIntegerToBase(result, length);
+
+            } catch (NoSuchAlgorithmException e) {
+                // 降级方案:使用增强随机数
+                return generateEnhancedRandomCode(length);
+            }
+        }
+
+        /**
+         * 增强随机数生成
+         */
+        private String generateEnhancedRandomCode(int length) {
+            char[] result = new char[length];
+
+            for (int i = 0; i < length; i++) {
+                // 使用安全的随机索引生成
+                int index = secureRandom.nextInt(charsetSize);
+                result[i] = charset[index];
+            }
+
+            return new String(result);
+        }
+
+        /**
+         * 安全的进制转换 - 完全修复版本
+         */
+        private String convertToBaseSafe(long number, int length) {
+            char[] result = new char[length];
+
+            // 确保number为正数
+            long temp = Math.abs(number);
+            if (temp == 0) {
+                temp = System.nanoTime() & 0x7FFFFFFF; // 使用纳秒时间作为备用值
+            }
+
+            // 基本转换
+            for (int i = length - 1; i >= 0; i--) {
+                long remainder = temp % charsetSize;
+                int index = (int) remainder;
+
+                // 确保索引在有效范围内
+                if (index < 0) {
+                    index = (int) Math.abs(remainder);
+                }
+                index = index % charsetSize;
+
+                result[i] = charset[index];
+                temp = temp / charsetSize;
+            }
+
+            return new String(result);
+        }
+
+        /**
+         * BigInteger版本的安全进制转换
+         */
+        private String convertBigIntegerToBase(BigInteger number, int length) {
+            char[] result = new char[length];
+            BigInteger base = BigInteger.valueOf(charsetSize);
+            BigInteger temp = number;
+
+            // 确保temp为正数
+            if (temp.signum() < 0) {
+                temp = temp.abs();
+            }
+
+            for (int i = length - 1; i >= 0; i--) {
+                BigInteger[] divResult = temp.divideAndRemainder(base);
+                int index = divResult[1].intValue();
+
+                // 确保索引为正数且在范围内
+                if (index < 0) {
+                    index = -index;
+                }
+                index = index % charsetSize;
+
+                result[i] = charset[index];
+                temp = divResult[0];
+            }
+
+            return new String(result);
+        }
+
+        /**
+         * 混合并最终化代码
+         */
+        private String mixAndFinalize(String code1, String code2, int length) {
+            char[] result = new char[length];
+
+            for (int i = 0; i < length; i++) {
+                char c1 = code1.charAt(i % code1.length());
+                char c2 = code2.charAt((i + 1) % code2.length());
+
+                // 使用安全的混合方法
+                int mixed = safeMixChars(c1, c2, i);
+                result[i] = charset[mixed];
+            }
+
+            return new String(result);
+        }
+
+        /**
+         * 安全的字符混合
+         */
+        private int safeMixChars(char c1, char c2, int position) {
+            long l1 = (long) c1;
+            long l2 = (long) c2;
+
+            // 使用不同的混合策略,但都确保结果为非负数
+            long mixedLong;
+            switch (position % 4) {
+                case 0:
+                    mixedLong = (l1 + l2 * 31L) % charsetSize;
+                    break;
+                case 1:
+                    mixedLong = (l1 ^ l2) % charsetSize;
+                    break;
+                case 2:
+                    mixedLong = (l1 * 17L + l2) % charsetSize;
+                    break;
+                default:
+                    mixedLong = (l1 * 3L + l2 * 7L) % charsetSize;
+                    break;
+            }
+
+            // 确保结果为正数
+            int mixed = (int) Math.abs(mixedLong) % charsetSize;
+            return mixed;
+        }
+
+        /**
+         * 获取进程ID
+         */
+        private static long getProcessId() {
+            try {
+                String processName = java.lang.management.ManagementFactory
+                        .getRuntimeMXBean()
+                        .getName();
+                String pidStr = processName.split("@")[0];
+                return Long.parseLong(pidStr) & 0xFFFF; // 限制范围
+            } catch (Exception e) {
+                // 返回备用值
+                return Thread.currentThread().getId() & 0xFFFF;
+            }
+        }
+    }
+}

+ 1 - 1
fs-service/src/main/resources/application-config-druid-bjzm-test.yml

@@ -91,7 +91,7 @@ cloud_host:
   company_name: 北京卓美
   projectCode: BJZM
   spaceName:
-  volcengineUrl: https://myhkvolcengine.ylrztop.com
+  volcengineUrl: https://bjzmvolcengine.ylrztop.com
 headerImg:
   imgUrl:
 

+ 3 - 3
fs-service/src/main/resources/application-config-druid-bjzm.yml

@@ -37,8 +37,8 @@ wx:
       port: 6379
       timeout: 2000
     configs:
-      - appId: wxc8534f3a7c4f306c # 第一个公众号的appid  //公众号名称:德瑞康
-        secret: 7a4bac8d7628c2adf70575628826e2b8 # 公众号的appsecret--德瑞康
+      - appId: wx346276e5e6abd1a5 # 第一个公众号的appid  //公众号名称:德瑞康
+        secret: 4591d841f1df62bd97827ec0de1f7a93 # 公众号的appsecret--德瑞康
         token: PPKOdAlCoMO # 接口配置里的Token值
         aesKey: Eswa6VjwtVMCcw03qZy6fWllgrv5aytIA1SZPEU0kU2 # 接口配置里的EncodingAESKey值
 aifabu:  #爱链接
@@ -86,7 +86,7 @@ cloud_host:
   company_name: 北京卓美
   projectCode: BJZM
   spaceName:
-  volcengineUrl: https://myhkvolcengine.ylrztop.com
+  volcengineUrl: https://bjzmvolcengine.ylrztop.com
 headerImg:
   imgUrl:
 

+ 41 - 2
fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml

@@ -517,11 +517,25 @@
         <if test='config.isAudit == "1"'>
             and p.is_audit = '1'
         </if>
-        <if test='appId != null and appId = "" '>
+        <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
         and p.is_new=1 and p.is_display=1 order by p.sort desc limit #{count}
     </select>
+    <select id="selectFsStoreProductNewQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
+        select p.* from fs_store_product_scrm p
+        <if test='config.isAudit == "1"'>
+            inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
+        </if>
+        where p.is_del=0 and p.is_show=1
+        <if test='config.isAudit == "1"'>
+            and p.is_audit = '1'
+        </if>
+        <if test='appId != null and appId != ""'>
+            and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
+        </if>
+        and p.is_new=1 and p.is_display=1 order by p.sort desc
+    </select>
     <select id="selectFsStoreProductHotQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
         <if test='config.isAudit == "1" '>
@@ -531,11 +545,25 @@
         <if test='config.isAudit == "1" '>
             and p.is_audit = '1'
         </if>
-        <if test='appId != null and appId = "" '>
+        <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
         and  p.is_hot=1 and p.is_display=1 order by p.sort desc limit #{count}
     </select>
+    <select id="selectFsStoreProductHotQueryPage" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
+        select p.* from fs_store_product_scrm p
+        <if test='config.isAudit == "1" '>
+            inner join fs_store_scrm fs on fs.store_id = p.store_id and fs.is_audit = 1
+        </if>
+        where p.is_del=0 and p.is_show=1
+        <if test='config.isAudit == "1" '>
+            and p.is_audit = '1'
+        </if>
+        <if test='appId != null and appId != ""'>
+            and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
+        </if>
+        and  p.is_hot=1 and p.is_display=1 order by p.sort desc
+    </select>
     <select id="selectFsStoreProductGoodListQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
         <if test='config.isAudit == "1" '>
@@ -557,4 +585,15 @@
         #{item}
     </foreach>
     </select>
+
+    <select id="getStoreProductInProductIdsForApp" resultType="com.fs.hisStore.domain.FsStoreProductScrm">
+        <include refid="selectFsStoreProductVo"/>
+        where is_del = 0 and is_show = 1 and is_audit = 1
+        and product_id IN
+        <foreach collection="productIds" index="index" item="item" open="(" separator="," close=")">#{item}</foreach>
+    </select>
+
+    <select id="selectProductIdsForApp" resultType="java.lang.Long">
+        select product_id from fs_store_product_scrm where is_del = 0 and is_show = 1 and is_audit = 1  order by sort desc, product_id desc
+    </select>
 </mapper>

+ 34 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductTagRelationScrmMapper.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.hisStore.mapper.FsStoreProductTagRelationScrmMapper">
+
+    <insert id="insertBatch">
+        insert into fs_store_product_tag_relation_scrm (product_id, tag_id) values
+        <foreach collection="tagIds" item="tid" separator=",">(#{productId}, #{tid})</foreach>
+    </insert>
+
+    <delete id="deleteByProductId">
+        delete from fs_store_product_tag_relation_scrm where product_id = #{productId}
+    </delete>
+
+    <select id="selectTagIdsByProductId" resultType="java.lang.Long">
+        select tag_id from fs_store_product_tag_relation_scrm where product_id = #{productId}
+    </select>
+
+    <select id="countByTagId" resultType="int">
+        select count(*) from fs_store_product_tag_relation_scrm where tag_id = #{tagId}
+    </select>
+
+    <delete id="deleteByTagIds">
+        delete from fs_store_product_tag_relation_scrm where tag_id in
+        <foreach collection="tagIds" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </delete>
+
+    <select id="selectProductTagNamesByProductIds" resultType="com.fs.hisStore.vo.FsStoreProductTagNameVO">
+        select r.product_id as productId, t.tag_name as tagName
+        from fs_store_product_tag_relation_scrm r
+        inner join fs_store_product_tag_scrm t on r.tag_id = t.id and t.status = 1
+        where r.product_id in
+        <foreach collection="productIds" item="pid" open="(" separator="," close=")">#{pid}</foreach>
+    </select>
+</mapper>

+ 65 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductTagScrmMapper.xml

@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.hisStore.mapper.FsStoreProductTagScrmMapper">
+
+    <resultMap id="BaseResultMap" type="com.fs.hisStore.domain.FsStoreProductTagScrm">
+        <id property="id" column="id"/>
+        <result property="storeId" column="store_id"/>
+        <result property="tagName" column="tag_name"/>
+        <result property="status" column="status"/>
+        <result property="sort" column="sort"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">id, store_id, tag_name, status, sort, create_time, update_time</sql>
+
+    <select id="selectById" resultMap="BaseResultMap">
+        select <include refid="Base_Column_List"/> from fs_store_product_tag_scrm where id = #{id}
+    </select>
+
+    <select id="selectListVO" resultType="com.fs.hisStore.vo.FsStoreProductTagListVO">
+        select t.id, t.store_id, t.tag_name, t.status, t.sort, t.create_time,
+               (select count(*) from fs_store_product_tag_relation_scrm r where r.tag_id = t.id) as product_count
+        from fs_store_product_tag_scrm t
+        <where>
+            <if test="storeId != null">and t.store_id = #{storeId}</if>
+            <if test="tagName != null and tagName != ''">and t.tag_name like concat('%', #{tagName}, '%')</if>
+            <if test="status != null">and t.status = #{status}</if>
+        </where>
+        order by t.sort asc, t.create_time desc
+    </select>
+
+    <insert id="insert" parameterType="com.fs.hisStore.domain.FsStoreProductTagScrm" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_product_tag_scrm (store_id, tag_name, status, sort, create_time, update_time)
+        values (#{storeId}, #{tagName}, #{status}, #{sort}, #{createTime}, #{updateTime})
+    </insert>
+
+    <update id="update" parameterType="com.fs.hisStore.domain.FsStoreProductTagScrm">
+        update fs_store_product_tag_scrm
+        <set>
+            <if test="tagName != null">tag_name = #{tagName},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="sort != null">sort = #{sort},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        delete from fs_store_product_tag_scrm where id = #{id}
+    </delete>
+
+    <delete id="deleteByIds">
+        delete from fs_store_product_tag_scrm where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </delete>
+
+    <select id="selectProductsByTagId" resultType="com.fs.hisStore.vo.FsStoreTagProductVO">
+        select p.product_id as productId, p.product_name as productName, p.image as image, p.price as price, p.is_show as isShow
+        from fs_store_product_scrm p
+        inner join fs_store_product_tag_relation_scrm r on p.product_id = r.product_id
+        where r.tag_id = #{tagId} and (p.is_del is null or p.is_del = 0)
+        order by p.create_time desc
+    </select>
+</mapper>

+ 34 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreProductUserEndCategoryMapper.xml

@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.hisStore.mapper.FsStoreProductUserEndCategoryMapper">
+
+    <insert id="insertBatch">
+        insert into fs_store_product_user_end_category (product_id, user_end_category_id) values
+        <foreach collection="categoryIds" item="cid" separator=",">(#{productId}, #{cid})</foreach>
+    </insert>
+
+    <delete id="deleteByProductId">
+        delete from fs_store_product_user_end_category where product_id = #{productId}
+    </delete>
+
+    <select id="selectCategoryIdsByProductId" resultType="java.lang.Long">
+        select user_end_category_id from fs_store_product_user_end_category where product_id = #{productId}
+    </select>
+
+    <delete id="deleteByCategoryIds">
+        delete from fs_store_product_user_end_category where user_end_category_id in
+        <foreach collection="categoryIds" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </delete>
+
+    <select id="selectDistinctProductIdsByCategoryId" resultType="java.lang.Long">
+        select distinct a.product_id from fs_store_product_user_end_category a left join fs_store_product_scrm c on a.product_id = c.product_id
+        where a.user_end_category_id = #{categoryId} and c.is_del = 0 and c.is_show = 1
+        order by c.sort desc, c.create_time desc, a.product_id
+    </select>
+
+    <select id="selectDistinctProductIds" resultType="java.lang.Long">
+        select distinct a.product_id from fs_store_product_user_end_category  a left join fs_store_product_scrm c on a.product_id = c.product_id
+        where c.is_del = 0 and c.is_show = 1
+        order by c.sort desc, c.create_time desc, a.product_id
+    </select>
+</mapper>

+ 75 - 0
fs-service/src/main/resources/mapper/hisStore/FsStoreUserEndCategoryScrmMapper.xml

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.hisStore.mapper.FsStoreUserEndCategoryScrmMapper">
+
+    <resultMap id="BaseResultMap" type="com.fs.hisStore.domain.FsStoreUserEndCategoryScrm">
+        <id property="id" column="id"/>
+        <result property="storeId" column="store_id"/>
+        <result property="categoryName" column="category_name"/>
+        <result property="position" column="position"/>
+        <result property="icon" column="icon"/>
+        <result property="status" column="status"/>
+        <result property="sort" column="sort"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <sql id="Base_Column_List">id, store_id, category_name, position, icon, status, sort, create_time, update_time</sql>
+
+    <select id="selectById" resultMap="BaseResultMap">
+        select <include refid="Base_Column_List"/> from fs_store_user_end_category_scrm where id = #{id}
+    </select>
+
+    <select id="selectList" resultMap="BaseResultMap">
+        select <include refid="Base_Column_List"/> from fs_store_user_end_category_scrm
+        <where>
+            <if test="storeId != null">and store_id = #{storeId}</if>
+            <if test="categoryName != null and categoryName != ''">and category_name like concat('%', #{categoryName}, '%')</if>
+            <if test="position != null">and position = #{position}</if>
+            <if test="status != null">and status = #{status}</if>
+        </where>
+        order by sort asc, create_time desc
+    </select>
+
+    <select id="selectListForProduct" resultMap="BaseResultMap">
+        select <include refid="Base_Column_List"/> from fs_store_user_end_category_scrm
+        where status = 1
+        <if test="storeId != null">and store_id = #{storeId}</if>
+        order by position asc, sort asc, create_time desc
+    </select>
+
+    <select id="selectTop8ByPosition" resultMap="BaseResultMap">
+        select <include refid="Base_Column_List"/> from fs_store_user_end_category_scrm
+        where status = 1 and position = #{position}
+        <if test="storeId != null">and store_id = #{storeId}</if>
+        order by sort asc, create_time desc
+        limit 8
+    </select>
+
+    <insert id="insert" parameterType="com.fs.hisStore.domain.FsStoreUserEndCategoryScrm" useGeneratedKeys="true" keyProperty="id">
+        insert into fs_store_user_end_category_scrm (store_id, category_name, position, icon, status, sort, create_time, update_time)
+        values (#{storeId}, #{categoryName}, #{position}, #{icon}, #{status}, #{sort}, #{createTime}, #{updateTime})
+    </insert>
+
+    <update id="update" parameterType="com.fs.hisStore.domain.FsStoreUserEndCategoryScrm">
+        update fs_store_user_end_category_scrm
+        <set>
+            <if test="categoryName != null">category_name = #{categoryName},</if>
+            <if test="position != null">position = #{position},</if>
+            <if test="icon != null">icon = #{icon},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="sort != null">sort = #{sort},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </set>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteById">
+        delete from fs_store_user_end_category_scrm where id = #{id}
+    </delete>
+
+    <delete id="deleteByIds">
+        delete from fs_store_user_end_category_scrm where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </delete>
+</mapper>

+ 8 - 0
fs-service/src/main/resources/mapper/live/LiveDataMapper.xml

@@ -52,6 +52,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                  inner join live B on A.live_id = B.live_id
         where B.status in (1,2,4) and b.is_audit = 1 and b.is_show = 1 and is_del=0
     </select>
+
+    <!-- 首页推荐:正在直播的直播间,仅查 liveId、liveType、封面、标题 -->
+    <select id="selectLivingLivesForApp" resultType="com.fs.live.vo.LiveAppSimpleVO">
+        SELECT live_id AS liveId, live_type AS liveType, live_img_url AS liveImgUrl, live_name AS liveName
+        FROM live
+        WHERE status = 2 AND is_show = 1 AND (is_del = 0 OR is_del IS NULL) AND is_audit = 1
+        ORDER BY start_time DESC
+    </select>
     <select id="getRecentLive" resultType="com.fs.live.vo.RecentLiveDataVo">
         SELECT A.page_views AS pageViews,A.unique_visitors AS uniqueVisitors, B.live_id AS liveId,B.live_name AS liveName,B.`status` AS status,B.live_img_url AS liveImgUrl,B.start_time AS startTime
         FROM live_data A LEFT JOIN live B ON A.live_id = B.live_id ORDER BY B.start_time LIMIT 4

+ 14 - 4
fs-user-app/src/main/java/com/fs/app/controller/IndexController.java

@@ -10,6 +10,8 @@ import com.fs.his.domain.*;
 import com.fs.his.param.*;
 import com.fs.his.service.*;
 import com.fs.his.vo.*;
+import com.fs.hisStore.domain.FsStoreUserEndCategoryScrm;
+import com.fs.hisStore.service.IFsStoreUserEndCategoryScrmService;
 import com.fs.store.config.ConceptConfig;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
@@ -53,6 +55,9 @@ public class IndexController extends AppBaseController {
 	@Autowired
 	private IFsChineseMedicineService chineseMedicineService;
 
+	@Autowired
+	private IFsStoreUserEndCategoryScrmService userEndCategoryScrmService;
+
 	@ApiOperation("获取名方列表")
 	@Cacheable(value = "getFamousPrescribeList", key = "#param")
 	@GetMapping("/getFamousPrescribeList")
@@ -146,14 +151,19 @@ public class IndexController extends AppBaseController {
 
 	/**
 	 * 首页初始化核心数据(频道入口、分类标签、商品分类导航)
-	 * 合并请求,提升首屏加载速度
+	 * channelList:金刚区用户端分类,按排序和创建时间取前8个
+	 * categoryTags:瀑布流用户端分类,按排序和创建时间取前8个
 	 */
 	@ApiOperation("首页初始化")
 	@GetMapping("/home/init")
-	public R homeInit() {
+	public R homeInit(@RequestParam(value = "storeId", required = false) Long storeId) {
 		Map<String, Object> data = new HashMap<>();
-		data.put("channelList", Collections.emptyList());
-		data.put("categoryTags", Collections.emptyList());
+		// 金刚区:前8条,排序 asc、创建时间 desc
+		List<FsStoreUserEndCategoryScrm> channelList = userEndCategoryScrmService.selectTop8ByPosition(storeId, 1);
+		// 瀑布流:前8条,排序 asc、创建时间 desc
+		List<FsStoreUserEndCategoryScrm> categoryTags = userEndCategoryScrmService.selectTop8ByPosition(storeId, 2);
+		data.put("channelList", channelList != null ? channelList : Collections.emptyList());
+		data.put("categoryTags", categoryTags != null ? categoryTags : Collections.emptyList());
 		data.put("goodsNav", Collections.emptyList());
 		return R.ok().put("data", data);
 	}

+ 83 - 1
fs-user-app/src/main/java/com/fs/app/controller/store/IndexScrmController.java

@@ -8,6 +8,7 @@ import com.fs.app.annotation.Login;
 import com.fs.app.controller.AppBaseController;
 import com.fs.app.vo.IndexVO;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.his.config.AgreementConfig;
@@ -15,6 +16,8 @@ import com.fs.hisStore.domain.*;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
+import com.fs.live.service.ILiveDataService;
+import com.fs.live.vo.LiveAppSimpleVO;
 import com.fs.store.config.ConceptConfig;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
@@ -27,11 +30,13 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.servlet.ModelAndView;
 import springfox.documentation.annotations.Cacheable;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 
 @Api("首页接口")
@@ -64,6 +69,83 @@ public class IndexScrmController extends AppBaseController {
 	@Autowired
 	private IFsCoursePlaySourceConfigService coursePlaySourceConfigService;
 
+	@Autowired
+	private IFsStoreUserEndCategoryScrmService userEndCategoryScrmService;
+
+	@Autowired
+	private ILiveDataService liveDataService;
+
+	/**
+	 * 首页初始化核心数据(频道入口、分类标签、商品分类导航)
+	 * channelList:金刚区用户端分类,按排序和创建时间取前8个
+	 * categoryTags:瀑布流用户端分类,按排序和创建时间取前8个
+	 */
+	@ApiOperation("首页初始化")
+	@GetMapping("/home/init")
+	public R homeInit(@RequestParam(value = "storeId", required = false) Long storeId) {
+		Map<String, Object> data = new HashMap<>();
+		// 金刚区:前8条,排序 asc、创建时间 desc
+		List<FsStoreUserEndCategoryScrm> categoryTags = userEndCategoryScrmService.selectTop8ByPosition(storeId, 1);
+		// 瀑布流:前8条,排序 asc、创建时间 desc
+		List<FsStoreUserEndCategoryScrm> goodsNav = userEndCategoryScrmService.selectTop8ByPosition(storeId, 2);
+		data.put("channelList", Collections.emptyList());
+		data.put("categoryTags", categoryTags != null ? categoryTags : Collections.emptyList());
+		data.put("goodsNav", goodsNav != null ? goodsNav : Collections.emptyList());
+		return R.ok().put("data", data);
+	}
+
+	/**
+	 * 首页推荐区块(直播中、上新推荐等,可延迟加载)
+	 * live:正在直播的直播间(liveId、liveType、封面、标题;liveType=2 时 liveFlag 从 Redis 查)
+	 * green:商品“新品首发”开启的 2 条
+	 * hot:商品“是否热卖”开启的 2 条
+	 */
+	@ApiOperation("首页推荐区块")
+	@GetMapping("/home/recommend")
+	public R homeRecommend(HttpServletRequest request) {
+		String appId = request.getParameter("appId");
+		List<LiveAppSimpleVO> liveList = liveDataService.listLivingLivesForApp();
+		List<FsStoreProductListQueryVO> greenProduct = productService.selectFsStoreProductNewQuery(2, appId);
+		List<FsStoreProductListQueryVO> hotProduct = productService.selectFsStoreProductHotQuery(2, appId);
+		return R.ok()
+				.put("live", liveList != null ? liveList : Collections.emptyList())
+				.put("green", greenProduct != null ? greenProduct : Collections.emptyList())
+				.put("hot", hotProduct != null ? hotProduct : Collections.emptyList());
+	}
+
+	/**
+	 * 首页商品列表(支持用户端分类 id,id 为空查全部)
+	 * 去重分页查商品ID → 查商品简表+标签 → 返回 productId、productName、image、price、otPrice、sales、tagList
+	 */
+	@ApiOperation("首页商品列表")
+	@GetMapping("/home/goods")
+	public R homeGoods(@RequestParam(value = "id", required = false) Long id,
+					   @RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+					   @RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+		java.util.Map<String, Object> data = userEndCategoryScrmService.listProductsForApp(id, pageNum, pageSize);
+		return R.ok().put("data", data);
+	}
+
+	/**
+	 * 首页推荐「更多」分页:绿色有机(green) / 上新推荐(hot),逻辑同 homeRecommend 的 green、hot 查询,支持分页,默认第1页10条
+	 */
+	@ApiOperation("首页推荐更多分页")
+	@GetMapping("/home/goods/recommend")
+	public R homeGoodsRecommend(HttpServletRequest request,
+								@RequestParam(value = "type") String type,
+								@RequestParam(value = "pageNum", defaultValue = "1") Integer pageNum,
+								@RequestParam(value = "pageSize", defaultValue = "10") Integer pageSize) {
+		String appId = request.getParameter("appId");
+		if (!"green".equalsIgnoreCase(type) && !"hot".equalsIgnoreCase(type)) {
+			return R.ok().put("data", new PageInfo<>(Collections.emptyList()));
+		}
+		List<FsStoreProductListQueryVO> list = "green".equalsIgnoreCase(type)
+				? productService.selectFsStoreProductNewQueryPage(pageNum, pageSize, appId)
+				: productService.selectFsStoreProductHotQueryPage(pageNum, pageSize, appId);
+		PageInfo<FsStoreProductListQueryVO> pageInfo = new PageInfo<>(list);
+		return R.ok().put("data", pageInfo);
+	}
+
 	@ApiOperation("获取首页数据")
 	@GetMapping("/getIndexData")
 	public R getIndexData(HttpServletRequest request){

+ 16 - 0
fs-wx-task/src/main/resources/application.yml

@@ -0,0 +1,16 @@
+# 开发环境配置
+server:
+  # 服务器的HTTP端口,默认为8080
+  port: 7007
+logging:
+  level:
+    org: INFO
+    com: DEBUG
+# Spring配置
+spring:
+  profiles:
+    active: dev
+#    active: druid-hcl
+#    active: druid-sxjz
+#    active: druid-hdt
+#    active: druid-myhk-test