Przeglądaj źródła

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

caoliqin 1 tydzień temu
rodzic
commit
8c0a28739d
47 zmienionych plików z 2017 dodań i 21 usunięć
  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);
         FsStoreProductScrm product=fsStoreProductService.selectFsStoreProductById(productId);
         List<FsStoreProductAttrScrm> attrs=attrService.selectFsStoreProductAttrByProductId(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')")
     @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.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.fs.voice.utils.StringUtil;
 import com.fs.voice.utils.StringUtil;
 import lombok.extern.slf4j.Slf4j;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeanUtils;
@@ -1883,6 +1884,25 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
                         log.error("浏览器看课模板解析失败:" + e);
                         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;
                     break;
                 default:
                 default:
                     break;
                     break;
@@ -2300,6 +2320,108 @@ public class SopLogsTaskServiceImpl implements SopLogsTaskService {
         return sortLink.replaceAll("^[\\s\\u2005]+", "");
         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,
     private QwCreateLinkByAppVO createLinkByApp(QwSopTempSetting.Content.Setting setting, SopUserLogsVo logVo, Date sendTime,
                                                 Long courseId, Long videoId, String qwUserId,
                                                 Long courseId, Long videoId, String qwUserId,
                                                 String companyUserId, String companyId, String externalId, String corpId, String qwUserName) {
                                                 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 realLinkH5DomainName;//H5通用看课域名
     private String realLinkH5LiveName;//H5通用直播域名
     private String realLinkH5LiveName;//H5通用直播域名
     private String authDomainName;//网页授权域名
     private String authDomainName;//网页授权域名
+    private String smsDomainName;//短信推送域名
+    private String smsDomain;//短信推送域名
     private String mpAppId;//看课公众号APPID
     private String mpAppId;//看课公众号APPID
     private String registerDomainName;//注册域名
     private String registerDomainName;//注册域名
     private String courseDomainName;//链接域名
     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 Long id;
 
 
     private String chatId;
     private String chatId;
+    private String uNo;
 
 
     private Long projectId;//项目ID
     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.utils.ConfigUtil;
 import com.fs.his.vo.OptionsVO;
 import com.fs.his.vo.OptionsVO;
 import com.fs.hisStore.domain.FsStoreProductScrm;
 import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.mapper.FsStoreProductScrmMapper;
 import com.fs.im.service.OpenIMService;
 import com.fs.im.service.OpenIMService;
 import com.fs.qw.domain.*;
 import com.fs.qw.domain.*;
 import com.fs.qw.domain.QwCompany;
 import com.fs.qw.domain.QwCompany;
@@ -170,6 +171,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     private ISopUserLogsInfoService iSopUserLogsInfoService;
     private ISopUserLogsInfoService iSopUserLogsInfoService;
     @Autowired
     @Autowired
     private FsCourseLinkMapper fsCourseLinkMapper;
     private FsCourseLinkMapper fsCourseLinkMapper;
+    @Autowired
+    private FsStoreProductScrmMapper fsStoreProductScrmMapper;
 
 
 
 
     @Autowired
     @Autowired
@@ -4560,15 +4563,38 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 throw new RuntimeException(e);
                 throw new RuntimeException(e);
             }
             }
             if (jsonNode.isArray()) {
             if (jsonNode.isArray()) {
-                List<FsStoreProductScrm> fsPackageListVOS = new ArrayList<>();
+                // 第一步:收集所有 productId
+                List<Long> productIds = new ArrayList<>();
+                Map<Long, JsonNode> productIdToJsonNodeMap = new HashMap<>();
                 for (JsonNode node : jsonNode) {
                 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 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);
                     fsPackageListVOS.add(fsStoreProductScrm);
                 }
                 }
                 vo.setFsStoreProductScrms(fsPackageListVOS);
                 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> selectFsStoreProductNewQuery(Map<String, Object> params);
 
 
+    List<FsStoreProductListQueryVO> selectFsStoreProductNewQueryPage(Map<String, Object> params);
+
     List<FsStoreProductListQueryVO> selectFsStoreProductHotQuery(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 " +
     @Select("select p.* from fs_store_product_scrm p " +
             //新增审核状态及所属店铺审核状态
             //新增审核状态及所属店铺审核状态
             "<if test='config.isAudit == \"1\" '>" +
             "<if test='config.isAudit == \"1\" '>" +
@@ -412,7 +416,13 @@ public interface FsStoreProductScrmMapper
 
 
     List<FsStoreProductScrm> bulkCopyFsStoreProductByIds(Long[] productIds);
     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({"<script> " +
             "SELECT distinct fsp.* FROM fs_store_product_scrm fsp " +
             "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,多个用逗号隔开 */
     /** 所属小程序app_id,多个用逗号隔开 */
     private String appIds;
     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);
     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> selectFsStoreProductGoodQuery(int count);
 
 
     List<FsStoreProductListQueryVO> selectFsStoreProductTuiListQuery(BaseQueryParam param);
     List<FsStoreProductListQueryVO> selectFsStoreProductTuiListQuery(BaseQueryParam param);
@@ -148,4 +154,10 @@ public interface IFsStoreProductScrmService
     R copyStoreProduct(Long productId);
     R copyStoreProduct(Long productId);
 
 
     R updateCache(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
     @Autowired
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
     private FsStoreProductCategoryScrmMapper fsStoreProductCategoryScrmMapper;
 
 
+    @Autowired
+    private FsStoreProductUserEndCategoryMapper fsStoreProductUserEndCategoryMapper;
+    @Autowired
+    private FsStoreProductTagRelationScrmMapper fsStoreProductTagRelationScrmMapper;
+
     @Autowired
     @Autowired
     @Lazy
     @Lazy
     private ILiveService liveService;
     private ILiveService liveService;
@@ -252,6 +257,14 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     {
     {
         storeAuditLogUtil.addBatchAuditArray(productIds, "", "");
         storeAuditLogUtil.addBatchAuditArray(productIds, "", "");
         log.info("批量删除商品:{}", 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);
         int result = fsStoreProductMapper.deleteFsStoreProductByIds(productIds);
 
 
         // 清除缓存
         // 清除缓存
@@ -512,6 +525,10 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
     @Override
     @Override
     public int deleteFsStoreProductById(Long productId)
     public int deleteFsStoreProductById(Long productId)
     {
     {
+        if (productId != null) {
+            fsStoreProductUserEndCategoryMapper.deleteByProductId(productId);
+            fsStoreProductTagRelationScrmMapper.deleteByProductId(productId);
+        }
         int result = fsStoreProductMapper.deleteFsStoreProductById(productId);
         int result = fsStoreProductMapper.deleteFsStoreProductById(productId);
         // 清除缓存
         // 清除缓存
         clearProductDetailCache(productId);
         clearProductDetailCache(productId);
@@ -759,6 +776,19 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         } else {
         } else {
             addProductAttr(product.getProductId(),param.getItems(),param.getValues());
             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) {
         if (product.getProductId() != null) {
             FsStoreProductScrm cacheProduct = fsStoreProductMapper.selectFsStoreProductById(product.getProductId());
             FsStoreProductScrm cacheProduct = fsStoreProductMapper.selectFsStoreProductById(product.getProductId());
@@ -1141,6 +1171,24 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         return fsStoreProductMapper.selectFsStoreProductHotQuery(map);
         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
     @Override
     public List<FsStoreProductListQueryVO> selectFsStoreProductGoodQuery(int count) {
     public List<FsStoreProductListQueryVO> selectFsStoreProductGoodQuery(int count) {
         return fsStoreProductMapper.selectFsStoreProductGoodQuery(count,medicalMallConfig);
         return fsStoreProductMapper.selectFsStoreProductGoodQuery(count,medicalMallConfig);
@@ -1636,4 +1684,16 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         }
         }
         return R.ok();
         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.LiveDataListVo;
 import com.fs.live.vo.LiveDataStatisticsVo;
 import com.fs.live.vo.LiveDataStatisticsVo;
 import com.fs.live.vo.LiveUserDetailVo;
 import com.fs.live.vo.LiveUserDetailVo;
+import com.fs.live.vo.LiveAppSimpleVO;
 import com.fs.live.vo.RecentLiveDataVo;
 import com.fs.live.vo.RecentLiveDataVo;
 import com.fs.live.vo.TrendDataVO;
 import com.fs.live.vo.TrendDataVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Param;
@@ -238,4 +239,11 @@ public interface LiveDataMapper {
     @DataSource(DataSourceType.SLAVE)
     @DataSource(DataSourceType.SLAVE)
     List<LiveDataCompanyVO> selectCompanyEmployeeCountByLiveIds(@Param("liveIds") List<Long> liveIds,
     List<LiveDataCompanyVO> selectCompanyEmployeeCountByLiveIds(@Param("liveIds") List<Long> liveIds,
                                                                 @Param("companyIds") List<Long> companyIds);
                                                                 @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);
     List<LiveDataListVo> exportLiveData(LiveDataParam param);
 
 
+    /**
+     * 首页推荐:正在直播的直播间列表,仅返回 liveId/liveType/封面/标题;当 liveType=2 时 liveFlag 从 Redis 查询
+     */
+    List<LiveAppSimpleVO> listLivingLivesForApp();
+
     /**
     /**
      * 查询分公司直播数据统计列表
      * 查询分公司直播数据统计列表
      * @param param 查询参数
      * @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.ILiveUserFavoriteService;
 import com.fs.live.service.ILiveUserFollowService;
 import com.fs.live.service.ILiveUserFollowService;
 import com.fs.live.service.ILiveUserLikeService;
 import com.fs.live.service.ILiveUserLikeService;
+import com.fs.live.service.ILiveWatchUserService;
 import com.fs.live.vo.*;
 import com.fs.live.vo.*;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.Company;
 import com.fs.company.domain.CompanyUser;
 import com.fs.company.domain.CompanyUser;
@@ -78,6 +79,8 @@ public class LiveDataServiceImpl implements ILiveDataService {
     @Autowired
     @Autowired
     private ILiveUserFavoriteService liveUserFavoriteService;
     private ILiveUserFavoriteService liveUserFavoriteService;
     @Autowired
     @Autowired
+    private ILiveWatchUserService liveWatchUserService;
+    @Autowired
     private LiveDataMapper baseMapper;
     private LiveDataMapper baseMapper;
     @Autowired
     @Autowired
     private LiveUserFirstEntryMapper liveUserFirstEntryMapper;
     private LiveUserFirstEntryMapper liveUserFirstEntryMapper;
@@ -571,6 +574,27 @@ public class LiveDataServiceImpl implements ILiveDataService {
         return baseMapper.getAllLiveDatas();
         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
     @Override
     public void updateBatchById(List<LiveData> liveDatas) {
     public void updateBatchById(List<LiveData> liveDatas) {
         baseMapper.updateBatchLiveData(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
          * 群id
          */
          */
         private String chatId;
         private String chatId;
+        // 短信CODE
+        private String smsTemplateCode;
+
+        /**
+         * 短信模板id
+         */
+        private Integer smsTemplateId;
+        /**
+         * 短信模板标题
+         */
+        private String smsTemplateTitle;
+        /**
+         * 短信模板内容
+         */
+        private String smsTemplateContent;
         @Override
         @Override
         public Setting clone() {
         public Setting clone() {
             try {
             try {

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

@@ -161,7 +161,21 @@ public class QwSopTempSetting implements Serializable{
              * 业务id
              * 业务id
              */
              */
             private String businessId;
             private String businessId;
+            // 短信CODE
+            private String smsTemplateCode;
 
 
+            /**
+             * 短信模板id
+             */
+            private Integer smsTemplateId;
+            /**
+             * 短信模板标题
+             */
+            private String smsTemplateTitle;
+            /**
+             * 短信模板内容
+             */
+            private String smsTemplateContent;
 
 
             @Override
             @Override
             public Setting clone() {
             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.domain.SysConfig;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.mapper.SysConfigMapper;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
+import com.fs.utils.ShortCodeGeneratorUtils;
 import com.fs.voice.utils.StringUtil;
 import com.fs.voice.utils.StringUtil;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.LoggerFactory;
@@ -1545,6 +1546,25 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                                 log.error("浏览器看课模板解析失败:" + e);
                                 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;
                             break;
                         //群公告(仅用于一键群发,个人不应该有群公告)
                         //群公告(仅用于一键群发,个人不应该有群公告)
                         case "11":
                         case "11":
@@ -2240,7 +2260,26 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
                         log.error("赋值-小程序封面地址失败-" + e);
                         log.error("赋值-小程序封面地址失败-" + e);
                     }
                     }
                     break;
                     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:
                 default:
                     break;
                     break;
 
 
@@ -2584,6 +2623,44 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         return link.getRealLink().replaceAll("^[\\s\\u2005]+", "");
         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,
     private QwCreateLinkByAppVO createLinkByApp(QwSopCourseFinishTempSetting.Setting setting, String corpId,
                                                 Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                                 Date sendTime, Integer courseId, Integer videoId, Long qwUserId,
                                                 String companyUserId, String companyId, Long externalId,
                                                 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: 北京卓美
   company_name: 北京卓美
   projectCode: BJZM
   projectCode: BJZM
   spaceName:
   spaceName:
-  volcengineUrl: https://myhkvolcengine.ylrztop.com
+  volcengineUrl: https://bjzmvolcengine.ylrztop.com
 headerImg:
 headerImg:
   imgUrl:
   imgUrl:
 
 

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

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

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

@@ -517,11 +517,25 @@
         <if test='config.isAudit == "1"'>
         <if test='config.isAudit == "1"'>
             and p.is_audit = '1'
             and p.is_audit = '1'
         </if>
         </if>
-        <if test='appId != null and appId = "" '>
+        <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
         </if>
         and p.is_new=1 and p.is_display=1 order by p.sort desc limit #{count}
         and p.is_new=1 and p.is_display=1 order by p.sort desc limit #{count}
     </select>
     </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 id="selectFsStoreProductHotQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
         select p.* from fs_store_product_scrm p
         <if test='config.isAudit == "1" '>
         <if test='config.isAudit == "1" '>
@@ -531,11 +545,25 @@
         <if test='config.isAudit == "1" '>
         <if test='config.isAudit == "1" '>
             and p.is_audit = '1'
             and p.is_audit = '1'
         </if>
         </if>
-        <if test='appId != null and appId = "" '>
+        <if test='appId != null and appId != "" '>
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
             and ((FIND_IN_SET(#{appId}, p.app_ids) > 0))
         </if>
         </if>
         and  p.is_hot=1 and p.is_display=1 order by p.sort desc limit #{count}
         and  p.is_hot=1 and p.is_display=1 order by p.sort desc limit #{count}
     </select>
     </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 id="selectFsStoreProductGoodListQuery" resultType="com.fs.hisStore.vo.FsStoreProductListQueryVO">
         select p.* from fs_store_product_scrm p
         select p.* from fs_store_product_scrm p
         <if test='config.isAudit == "1" '>
         <if test='config.isAudit == "1" '>
@@ -557,4 +585,15 @@
         #{item}
         #{item}
     </foreach>
     </foreach>
     </select>
     </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>
 </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
                  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
         where B.status in (1,2,4) and b.is_audit = 1 and b.is_show = 1 and is_del=0
     </select>
     </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 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
         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
         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.param.*;
 import com.fs.his.service.*;
 import com.fs.his.service.*;
 import com.fs.his.vo.*;
 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.store.config.ConceptConfig;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageHelper;
@@ -53,6 +55,9 @@ public class IndexController extends AppBaseController {
 	@Autowired
 	@Autowired
 	private IFsChineseMedicineService chineseMedicineService;
 	private IFsChineseMedicineService chineseMedicineService;
 
 
+	@Autowired
+	private IFsStoreUserEndCategoryScrmService userEndCategoryScrmService;
+
 	@ApiOperation("获取名方列表")
 	@ApiOperation("获取名方列表")
 	@Cacheable(value = "getFamousPrescribeList", key = "#param")
 	@Cacheable(value = "getFamousPrescribeList", key = "#param")
 	@GetMapping("/getFamousPrescribeList")
 	@GetMapping("/getFamousPrescribeList")
@@ -146,14 +151,19 @@ public class IndexController extends AppBaseController {
 
 
 	/**
 	/**
 	 * 首页初始化核心数据(频道入口、分类标签、商品分类导航)
 	 * 首页初始化核心数据(频道入口、分类标签、商品分类导航)
-	 * 合并请求,提升首屏加载速度
+	 * channelList:金刚区用户端分类,按排序和创建时间取前8个
+	 * categoryTags:瀑布流用户端分类,按排序和创建时间取前8个
 	 */
 	 */
 	@ApiOperation("首页初始化")
 	@ApiOperation("首页初始化")
 	@GetMapping("/home/init")
 	@GetMapping("/home/init")
-	public R homeInit() {
+	public R homeInit(@RequestParam(value = "storeId", required = false) Long storeId) {
 		Map<String, Object> data = new HashMap<>();
 		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());
 		data.put("goodsNav", Collections.emptyList());
 		return R.ok().put("data", data);
 		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.controller.AppBaseController;
 import com.fs.app.vo.IndexVO;
 import com.fs.app.vo.IndexVO;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.domain.FsCoursePlaySourceConfig;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.course.service.IFsCoursePlaySourceConfigService;
 import com.fs.his.config.AgreementConfig;
 import com.fs.his.config.AgreementConfig;
@@ -15,6 +16,8 @@ import com.fs.hisStore.domain.*;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.param.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.service.*;
 import com.fs.hisStore.vo.*;
 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.store.config.ConceptConfig;
 import com.fs.system.service.ISysConfigService;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 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.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.bind.annotation.RestController;
-import org.springframework.web.servlet.ModelAndView;
 import springfox.documentation.annotations.Cacheable;
 import springfox.documentation.annotations.Cacheable;
 
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
+import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
 import java.util.List;
+import java.util.Map;
 
 
 
 
 @Api("首页接口")
 @Api("首页接口")
@@ -64,6 +69,83 @@ public class IndexScrmController extends AppBaseController {
 	@Autowired
 	@Autowired
 	private IFsCoursePlaySourceConfigService coursePlaySourceConfigService;
 	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("获取首页数据")
 	@ApiOperation("获取首页数据")
 	@GetMapping("/getIndexData")
 	@GetMapping("/getIndexData")
 	public R getIndexData(HttpServletRequest request){
 	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