Kaynağa Gözat

课程查询 销售小程序端上传音色

yuhongqi 1 hafta önce
ebeveyn
işleme
18a8685e5e

+ 208 - 23
fs-company-app/src/main/java/com/fs/app/controller/CompanyUserController.java

@@ -5,12 +5,17 @@ import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.fs.aiSoundReplication.VoiceCloneController;
+import com.fs.aiSoundReplication.param.TtsRequest;
+import com.fs.aiSoundReplication.service.impl.TtsServiceImpl;
+import com.fs.aiSoundReplication.util.FileToMultipartConverterUtil;
 import com.fs.app.annotation.Login;
 import com.fs.app.param.*;
 import com.fs.app.service.IAppService;
 import com.fs.app.vo.CompanySubUserVO;
 import com.fs.common.annotation.RepeatSubmit;
 import com.fs.common.constant.UserConstants;
+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.core.redis.RedisCache;
@@ -20,6 +25,7 @@ import com.fs.common.utils.bean.BeanUtils;
 import com.fs.company.domain.*;
 import com.fs.company.mapper.CompanyRoleMapper;
 import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.company.param.VcCompanyUser;
 import com.fs.company.param.companyUserAddPrintParam;
 import com.fs.company.service.*;
 import com.fs.company.vo.CompanyTagUserVO;
@@ -33,7 +39,11 @@ import com.fs.fastGpt.domain.FastgptChatVoiceHomo;
 import com.fs.fastGpt.mapper.FastgptChatVoiceHomoMapper;
 import com.fs.fastgptApi.util.AudioUtils;
 import com.fs.fastgptApi.vo.AudioVO;
+import com.fs.his.utils.ConfigUtil;
+import com.fs.hisStore.enums.SysConfigEnum;
+import com.fs.qw.domain.QwUser;
 import com.fs.qw.dto.UserProjectDTO;
+import com.fs.qw.mapper.QwUserMapper;
 import com.fs.sop.domain.QwSopTempVoice;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.system.oss.CloudStorageService;
@@ -57,14 +67,20 @@ import org.apache.http.util.EntityUtils;
 import org.apache.ibatis.annotations.Param;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
+import javax.sound.sampled.AudioFormat;
+import javax.sound.sampled.AudioInputStream;
+import javax.sound.sampled.AudioSystem;
 import javax.validation.Valid;
-import java.io.File;
-import java.io.FileInputStream;
+import java.io.*;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.net.HttpURLConnection;
+import java.net.URL;
 import java.time.LocalDate;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 @Slf4j
@@ -91,6 +107,13 @@ public class CompanyUserController extends AppBaseController {
     private final ICompanyTagUserService companyTagUserService;
     private final FastgptChatVoiceHomoMapper fastgptChatVoiceHomoMapper;
     public static final String SOP_TEMP_VOICE_KEY = "sop:tempVoice";
+    private final ConfigUtil configUtil;
+    private final VoiceCloneController voiceCloneController;
+
+    private static final String VOICE_CLONE_CACHE_KEY = "voice:clone:company:user:id";
+    private static final Integer CACHE_TTL = 3600;
+    private final TtsServiceImpl ttsServiceImpl;
+    private final QwUserMapper qwUserMapper;
 
     @Login
     @ApiOperation("查询用户列表")
@@ -492,35 +515,177 @@ public class CompanyUserController extends AppBaseController {
         //更新销售员工声纹
         companyUser.setVoicePrintUrl(wavUrl);
         companyUserMapper.updateCompanyUser(companyUser);
+        JSONObject vcConfig = configUtil.generateConfigByKey(SysConfigEnum.VS_CONFIG.getKey());
+        if (vcConfig != null && !vcConfig.isEmpty() &&
+//                !vcConfig.equals(new JSONObject()) &&
+                "2".equals(vcConfig.getString("type"))) {
+            /*走豆包直接不走原逻辑*/
+            AjaxResult ajaxResult = uploadVoice(userId, wavUrl);
+            if (!ajaxResult.get("code").equals(200)) {
+                return R.error((String) ajaxResult.get("msg"));
+            }
+            voiceService.insertQwSopTempVoiceModel(userId);
+            return R.ok(ajaxResult.get("msg").toString());
+        } else {
+            try {
+                CloseableHttpClient httpClient = HttpClients.createDefault();
+                HttpPost httpPost = new HttpPost(aiHostProper.getCommonApi()+"/app/common/addCompanyAudio");
+                String json = "{\"url\":\""+wavUrl+"\",\"id\":\""+userId+"\"}";
+                StringEntity entity = new StringEntity(json);
+                httpPost.setEntity(entity);
+                httpPost.setHeader("Content-type", "application/json");
+                HttpResponse response = httpClient.execute(httpPost);
+
+                if (response.getStatusLine().getStatusCode() == 200) {
+                    String responseBody = EntityUtils.toString(response.getEntity());
+                    JSONObject jsonObject = JSON.parseObject(responseBody);
+                    Integer code = (Integer)jsonObject.get("code");
+                    if (code==200){
+                        voiceService.insertQwSopTempVoiceModel(userId);
+                        return R.ok();
+                    }
+                } else {
+                    return R.error();
+                }
+
+                httpClient.close();
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+
+        return R.error();
+
+    }
+
+    /**
+     * 上传音频豆包
+     *
+     * @param voicePrintUrl
+     * @return
+     * @throws Exception
+     */
+    @PostMapping("/uploadVoice")
+    public AjaxResult uploadVoice(
+            Long userId,
+            String voicePrintUrl) throws Exception {
+        if (userId == null) userId = 123L;
+        VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(userId);
+        if (vcCompanyUser == null) {
+            return AjaxResult.error("用户没有声纹槽位,请联系管理员");
+        }
+        if (vcCompanyUser.getTimes() != null && vcCompanyUser.getTimes() >= 5)
+            return AjaxResult.error("用户已上传声纹达到上限");
+        vcCompanyUser.setUploadUrl(voicePrintUrl);
+        File file = downloadFileFromUrl(voicePrintUrl);
+        /*获取文件时长*/
+        AudioInputStream audioInputStream = AudioSystem.getAudioInputStream(file);
+        AudioFormat format = audioInputStream.getFormat();
+        long frames = audioInputStream.getFrameLength();
+        // 计算时长(秒)= 总帧数 / 帧率
+        Double duration = (double) frames / format.getFrameRate();
+        audioInputStream.close();
+        MultipartFile convert = FileToMultipartConverterUtil.convert(file);
+        R r = voiceCloneController.uploadVoice(vcCompanyUser.getSpeakerId(), convert, vcCompanyUser.getVersionId(), 0);
+        if (!r.get("code").equals(200))
+            return AjaxResult.error("上传声纹失败", r.get("msg"));
+        vcCompanyUser.incrementTimes();
+        vcCompanyUser.setUploadTime(duration);
+        companyUserMapper.updateVcCompanyUser(vcCompanyUser);
+        return AjaxResult.success(String.format("您已上传%d次声纹,总共有5次录入次数", vcCompanyUser.getTimes()));
+    }
+
+    /**
+     * 获取文件扩展名
+     *
+     * @param fileUrl 文件路径
+     * @return 文件扩展名
+     */
+    private static String getFileExtension(String fileUrl) {
+        return fileUrl.substring(fileUrl.lastIndexOf("."));
+    }
 
+
+    private File downloadFileFromUrl(String fileUrl) throws IOException {
+        InputStream inputStream = null;
+        FileOutputStream outputStream = null;
         try {
-            CloseableHttpClient httpClient = HttpClients.createDefault();
-            HttpPost httpPost = new HttpPost(aiHostProper.getCommonApi()+"/app/common/addCompanyAudio");
-            String json = "{\"url\":\""+wavUrl+"\",\"id\":\""+userId+"\"}";
-            StringEntity entity = new StringEntity(json);
-            httpPost.setEntity(entity);
-            httpPost.setHeader("Content-type", "application/json");
-            HttpResponse response = httpClient.execute(httpPost);
-
-            if (response.getStatusLine().getStatusCode() == 200) {
-                String responseBody = EntityUtils.toString(response.getEntity());
-                JSONObject jsonObject = JSON.parseObject(responseBody);
-                Integer code = (Integer)jsonObject.get("code");
-                if (code==200){
-                    voiceService.insertQwSopTempVoiceModel(userId);
-                    return R.ok();
+            // 创建 HTTP 连接
+            URL url = new URL(fileUrl);
+            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+            // 设置Referer请求头
+//            connection.setRequestProperty("Referer", "cos.his.cdwjyyh.com");
+            connection.setRequestMethod("GET");
+            connection.connect();
+
+            // 检查是否成功连接
+            if (connection.getResponseCode() != 200) {
+                throw new ServiceException("无法下载音频文件,HTTP 响应码:" + connection.getResponseCode());
+            }
+
+            // 获取输入流
+            inputStream = connection.getInputStream();
+
+            // 创建临时文件,并指定存放地址
+            String tempFileName = "temp_" + UUID.randomUUID() + "_" + getFileExtension(fileUrl);
+            File destinationDirectory = new File("c:\\hook\\");
+
+            // 参照 transferAudioSilk 方法,同步确保目录创建的线程安全
+            synchronized (AudioUtils.class) {
+                if (!destinationDirectory.exists()) {
+                    destinationDirectory.mkdirs();
                 }
-            } else {
-                return R.error();
             }
 
-            httpClient.close();
+            // 将文件保存到指定路径
+            File tempFile = new File(destinationDirectory, tempFileName);
+
+            // 写入文件
+            outputStream = new FileOutputStream(tempFile);
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, bytesRead);
+            }
+
+            return tempFile;
         } catch (Exception e) {
             e.printStackTrace();
+        } finally {
+            try {
+                if (inputStream != null) inputStream.close();
+                if (outputStream != null) outputStream.close();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
         }
+        return null;
+    }
 
-        return R.error();
-
+    public VcCompanyUser getVcCompanyUser(Long companyUserId) {
+        String cacheKey = VOICE_CLONE_CACHE_KEY + companyUserId;
+        VcCompanyUser vcCompanyUser = redisCache.getCacheObject(cacheKey);
+        if (vcCompanyUser != null) {
+            return vcCompanyUser;
+        }
+        // 使用双重检查锁,防止缓存击穿
+        synchronized (this) {
+            // 再次检查缓存,防止在等待锁的过程中已经被其他线程加载
+            vcCompanyUser = redisCache.getCacheObject(cacheKey);
+            if (vcCompanyUser != null) {
+                return vcCompanyUser;
+            }
+            // 查询数据库
+            vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(companyUserId);
+            if (vcCompanyUser == null) {
+                // 缓存空对象,防止缓存穿透
+                redisCache.setCacheObject(cacheKey, new VcCompanyUser(), CACHE_TTL, TimeUnit.SECONDS);
+                throw new RuntimeException("用户不存在");
+            }
+            // 设置缓存
+            redisCache.setCacheObject(cacheKey, vcCompanyUser, CACHE_TTL,TimeUnit.SECONDS);
+            return vcCompanyUser;
+        }
     }
 
     /**
@@ -544,7 +709,27 @@ public class CompanyUserController extends AppBaseController {
 
         QwSopTempVoice qwSopTempVoice = voiceService.selectQwSopTempVoiceByIdAndUserVoiceUrl(id);
         if(qwSopTempVoice != null && qwSopTempVoice.getId() != null){
-            audioVO = AudioUtils.createVoiceUrl(qwSopTempVoice.getCompanyUserId(), userVoiceUrl);
+
+            JSONObject vcConfig = configUtil.generateConfigByKey(SysConfigEnum.VS_CONFIG.getKey());
+            if (vcConfig != null && !vcConfig.isEmpty() &&
+                    "2".equals(vcConfig.getString("type"))) {
+                VcCompanyUser vcCompanyUser = getVcCompanyUser(companyUserId);
+//                VcCompanyUser vcCompanyUser = companyUserMapper.selectVcCompanyUserByCompanyUserId(companyUserId);
+//                if (vcCompanyUser == null) throw new RuntimeException("用户不存在");
+                audioVO = ttsServiceImpl.textToSpeech(new TtsRequest(null, null, vcCompanyUser.getSpeakerId(), qwSopTempVoice.getVoiceTxt().replace(" ", "")));
+                QwUser qwUser = new QwUser();
+                qwUser.setCompanyUserId(companyUserId);
+                List<QwUser> qwUsers = qwUserMapper.selectQwUserList(qwUser);
+                if (qwUsers != null && !qwUsers.isEmpty()){
+                    List<QwUser> collect = qwUsers.stream().filter(o -> o.getFastGptRoleId() != null).collect(Collectors.toList());
+                    if (!collect.isEmpty()){
+                        qwUsers = collect;
+                    }
+                }else qwUsers= Collections.singletonList(new QwUser());
+                ttsServiceImpl.ttsChargeByCount(vcCompanyUser, audioVO, qwUsers.get(0));
+            } else {
+                audioVO = AudioUtils.createVoiceUrl(qwSopTempVoice.getCompanyUserId(), userVoiceUrl);
+            }
             if(audioVO != null && audioVO.getUrl() != null){
                 qwSopTempVoice.setVoiceUrl(audioVO.getUrl());
                 qwSopTempVoice.setUserVoiceUrl(userVoiceUrl);

+ 4 - 0
fs-service/src/main/java/com/fs/course/domain/FsUserCourseCategory.java

@@ -44,5 +44,9 @@ public class FsUserCourseCategory extends BaseEntity
     @Excel(name = "分类类型")
     private Integer cateType;
 
+    /** 小程序首页频道一级标记:1=频道Tab 0=默认长视频等非频道 */
+    @Excel(name = "小程序频道")
+    private Integer appChannelFlag;
+
     private Long userId;
 }

+ 27 - 0
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseStudyMapper.java

@@ -80,6 +80,33 @@ public interface FsUserCourseStudyMapper
             " order by s.study_id  "+
             "</script>"})
     List<FsUserCourseStudyListUVO> selectFsUserCourseStudyListUVO(@Param("maps")FsUserCourseListUParam param);
+        /* 备用更新
+                "select s.*,c.img_url,c.description as courseDesc from fs_user_course_study s " +
+            "left join fs_user_course c on c.course_id = s.course_id " +
+            "where c.is_del = 0 and ifnull(c.is_private, 0) = 0 " +
+            "<if test='maps.userId != null'> " +
+            "and s.user_id = #{maps.userId} " +
+            "</if>" +
+            "<if test='maps.courseType != null and maps.courseType != 0'> " +
+            "and c.cate_id = #{maps.courseType} " +
+            "and exists ( " +
+            "  select 1 from fs_user_course_category ch " +
+            "  where ch.cate_id = #{maps.courseType} and ch.pid = 0 and ch.is_del = 0 " +
+            "  and ifnull(ch.app_channel_flag, 0) = 1 " +
+            ") " +
+            "</if>" +
+            "<if test='maps.courseType == null or maps.courseType == 0'> " +
+            "and exists ( " +
+            "  select 1 from fs_user_course_category ch " +
+            "  where ch.cate_id = c.cate_id and ch.pid = 0 and ch.is_del = 0 " +
+            "  and ifnull(ch.app_channel_flag, 0) = 0 " +
+            ") " +
+            "</if>" +
+            "order by s.study_id " +
+            "</script>"})
+
+
+    * */
 
     @Select("select * from fs_user_course_study where user_id = #{userId} and course_id = #{courseId} limit 1")
     FsUserCourseStudy selectFsUserCourseStudyByAddStudy(@Param("userId")Long userId,@Param("courseId")Long courseId);

+ 3 - 3
fs-service/src/main/java/com/fs/course/param/FsUserCourseCategoryAppQueryParam.java

@@ -36,8 +36,8 @@ public class FsUserCourseCategoryAppQueryParam implements Serializable {
     @ApiModelProperty(value = "分类类型:1=公域课程分类,0=普通;不传时接口默认按1(公域)查询")
     private Integer cateType;
 
-    @ApiModelProperty(value = "原乡行标签:不传或0=只统计/展示下挂课程中无「原乡行」标签的;1=只统计/展示含「原乡行」标签的课程", example = "0")
-    private Integer yxxTag;
+    @ApiModelProperty(value = "频道一级分类ID:0或不传=全部公域;>0 查对应频道", example = "0")
+    private Long yxxTag;
 
     @ApiModelProperty(value = "首页排序 1 是首页,后端添加标签排序,0不是首页", example = "0")
     private Integer homePage;
@@ -50,7 +50,7 @@ public class FsUserCourseCategoryAppQueryParam implements Serializable {
         int pn = pageNum != null ? pageNum : 1;
         int ps = pageSize != null ? pageSize : 10;
         int ct = cateType != null ? cateType : 1;
-        int yxx = yxxTag != null ? yxxTag : 0;
+        String yxx = yxxTag != null ? String.valueOf(yxxTag) : "null";
         int home = homePage != null ? homePage : 0;
         return pn + "|" + ps + "|" + n(cateName) + "|" + n(pid) + "|" + n(isShow) + "|" + ct + "|" + yxx  + "|" + home;
     }

+ 3 - 1
fs-service/src/main/java/com/fs/course/param/FsUserCourseListUParam.java

@@ -22,7 +22,9 @@ public class FsUserCourseListUParam  implements Serializable {
 
     Integer isBest;
 
-    Integer courseType;
+    /** 频道一级 cateId:0 或不传=非频道(app_channel_flag=0)公域课学习记录;>0=对应频道 */
+    @ApiModelProperty(value = "频道一级分类ID:0或不传=默认公域;>0=频道cateId", example = "0")
+    Long courseType;
 
     @ApiModelProperty(value = "页码,默认为1")
     private Integer pageNum =1;

+ 3 - 3
fs-service/src/main/java/com/fs/course/param/FsUserCoursePublicAppQueryParam.java

@@ -36,14 +36,14 @@ public class FsUserCoursePublicAppQueryParam implements Serializable {
     @ApiModelProperty(value = "推荐位:1首页顶部 2商城首页 3首页长视频瀑布流;不传则不限")
     private Integer recommendSlot;
 
-    @ApiModelProperty(value = "原乡行标签:不传或0=只查无「原乡行」标签的公域课;1=只查带「原乡行」标签的", example = "0")
-    private Integer yxxTag;
+    @ApiModelProperty(value = "频道一级分类ID:0或不传=全部公域;>0 查对应频道", example = "0")
+    private Long yxxTag;
 
     /** 公域课列表缓存 key,与业务默认分页一致 */
     public String appListCacheKey() {
         int pn = pageNum != null ? pageNum : 1;
         int ps = pageSize != null ? pageSize : 10;
-        int yxx = yxxTag != null ? yxxTag : 0;
+        String yxx = yxxTag != null ? String.valueOf(yxxTag) : "null";
         return pn + "|" + ps + "|" + n(cateId) + "|" + n(subCateId) + "|" + n(keyword) + "|" + n(recommendSlot) + "|" + yxx;
     }
 

+ 18 - 6
fs-service/src/main/java/com/fs/fastGpt/service/impl/AiHookServiceImpl.java

@@ -72,6 +72,7 @@ import org.apache.commons.lang3.StringUtils;
 import org.jetbrains.annotations.Nullable;
 import org.json.JSONObject;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
@@ -188,6 +189,9 @@ public class AiHookServiceImpl implements AiHookService {
     @Autowired
     private FastgptChatQuestionCollectServiceImpl fastgptChatQuestionCollectServiceImpl;
 
+    @Value("${cloud_host.company_name}")
+    private String signProjectName;
+
 
     /** Ai半小时未回复提醒 **/
     /**
@@ -661,9 +665,13 @@ public class AiHookServiceImpl implements AiHookService {
                     addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
 
                     if(isNewVersion){
-                        //发送图片消息
-                        sendImgMsg(contentKh,sender,uid,serverId);
-                        sendAiMsg(content,sender,uid,serverId);
+                        if ("北京卓美".equals(signProjectName) && type == 16) {
+                            sendAiVoiceMsg(content, sender, uid, serverId, user);
+                        } else {
+                            //发送图片消息
+                            sendImgMsg(contentKh,sender,uid,serverId);
+                            sendAiMsg(content,sender,uid,serverId);
+                        }
                     } else {
                         if (type==16){
                             sendAiVoiceMsg(content,sender,uid,serverId,user);
@@ -689,9 +697,13 @@ public class AiHookServiceImpl implements AiHookService {
                     addUserInfo(contentKh, qwExternalContacts.getId(),fastGptChatSession);
                     for (String msg : countList) {
                         if(isNewVersion){
-                            //发送图片消息
-                            sendImgMsg(contentKh,sender,uid,serverId);
-                            sendAiMsg(msg,sender,uid,serverId);
+                            if ("北京卓美".equals(signProjectName) && type == 16) {
+                                sendAiVoiceMsg(msg,sender,uid,serverId,user);
+                            } else {
+                                //发送图片消息
+                                sendImgMsg(contentKh,sender,uid,serverId);
+                                sendAiMsg(msg,sender,uid,serverId);
+                            }
                         } else {
                             if (type==16){
                                 sendAiVoiceMsg(msg,sender,uid,serverId,user);

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -767,6 +767,10 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
 
         // 5. 调用对应 ERP 服务(当前是聚水潭ERP)
         IErpOrderService erpService = erpServiceMap.get(erpType);
+        if (erpService == jSTOrderService) {
+            FsSysConfig erpConfig = configUtil.generateStructConfigByKey(SysConfigEnum.HIS_CONFIG.getKey(), FsSysConfig.class);
+            erpOrder.setShop_code(erpConfig.getErpJstShopCode());
+        }
         //执行商城订单推送逻辑
         ErpOrderResponse response = erpService.addOrderScrm(erpOrder);
         log.info("ERP地址推送结果 - 商城订单: {}, ERP类型: {}, 成功: {}, 外部单号: {}",

+ 1 - 1
fs-service/src/main/java/com/fs/system/mapper/SysDictDataMapper.java

@@ -95,7 +95,7 @@ public interface SysDictDataMapper
      * @return 结果
      */
     public int updateDictDataType(@Param("oldDictType") String oldDictType, @Param("newDictType") String newDictType);
-    @Select("select dict_type,dict_label,dict_value from sys_dict_data where dict_type =#{dictType} and status = 0")
+    @Select("select dict_type,dict_label,dict_value from sys_dict_data where dict_type =#{dictType} and status = 0 order by dict_sort asc")
     List<DictVO> selectDictDataListByType(String dictType);
 
     SysDictData selectDictDataByTypeAndValue(@Param("type") String type, @Param("value") String value);

+ 29 - 18
fs-service/src/main/resources/mapper/course/FsUserCourseCategoryMapper.xml

@@ -14,11 +14,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="updateTime"    column="update_time"    />
         <result property="isDel"    column="is_del"    />
         <result property="cateType"    column="cate_type"    />
+        <result property="appChannelFlag"    column="app_channel_flag"    />
         <result property="userId"    column="user_id"    />
     </resultMap>
 
     <sql id="selectFsUserCourseCategoryVo">
-        select cate_id, pid, cate_name, sort, is_show, create_time, update_time, is_del, cate_type from fs_user_course_category
+        select cate_id, pid, cate_name, sort, is_show, create_time, update_time, is_del, cate_type, app_channel_flag from fs_user_course_category
+    </sql>
+
+    <!-- 原乡行一级:首页顶部推荐排序(其它频道一级用长视频瀑布流推荐) -->
+    <sql id="appChannelYuanXiangXingRootId">
+        (SELECT cate_id FROM fs_user_course_category WHERE pid = 0 AND cate_type = 1 AND is_del = 0 AND app_channel_flag = 1 AND cate_name LIKE '%央广原乡行%' LIMIT 1)
     </sql>
 
     <select id="selectFsUserCourseCategoryList" parameterType="FsUserCourseCategory" resultMap="FsUserCourseCategoryResult">
@@ -39,10 +45,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectFsUserCourseCategoryAppList" parameterType="com.fs.course.param.FsUserCourseCategoryAppQueryParam" resultType="com.fs.course.domain.FsUserCourseCategory">
         SELECT
          distinct
-        <if test="yxxTag != null and yxxTag == 1 and homePage != null and homePage == 1">
-            d.rec_home_course_top_sort,
+        <if test="yxxTag != null and yxxTag > 0 and homePage != null and homePage == 1">
+            (CASE WHEN #{yxxTag} = <include refid="appChannelYuanXiangXingRootId"/> THEN d.rec_home_course_top_sort ELSE d.rec_home_long_video_sort END),
         </if>
-        <if test="yxxTag == null and homePage != null and homePage == 1">
+        <if test="(yxxTag == null or yxxTag == 0) and homePage != null and homePage == 1">
             d.rec_home_long_video_sort,
         </if>
         c.cate_id, c.pid, c.cate_name, c.sort, c.is_show, c.create_time, c.update_time, c.is_del, c.cate_type
@@ -59,20 +65,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
                 and d.is_del = 0
                 and d.is_show = 1
             </if>
-                <if test="yxxTag != null and yxxTag == 1 and homePage != null and homePage == 1">
-                    and d.rec_home_course_top_enabled = 1
+                <if test="yxxTag != null and yxxTag > 0 and homePage != null and homePage == 1">
+                    AND (
+                        (#{yxxTag} = <include refid="appChannelYuanXiangXingRootId"/> AND d.rec_home_course_top_enabled = 1)
+                        OR (#{yxxTag} != <include refid="appChannelYuanXiangXingRootId"/> AND d.rec_home_long_video_enabled = 1)
+                    )
                 </if>
-                <if test="yxxTag == null and homePage != null and homePage == 1">
+                <if test="(yxxTag == null or yxxTag == 0) and homePage != null and homePage == 1">
                     and d.rec_home_long_video_enabled = 1
                 </if>
-            <choose>
-                <when test="yxxTag != null and yxxTag == 1">
-                    AND c.pid = (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1)
-                </when>
-                <otherwise>
-                    AND c.pid != (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1)
-                </otherwise>
-            </choose>
+            <if test="yxxTag != null and yxxTag > 0">
+                AND c.pid = #{yxxTag}
+                AND IFNULL(p.app_channel_flag, 0) = 1
+            </if>
+            <if test="yxxTag == null or yxxTag == 0">
+                AND IFNULL(p.app_channel_flag, 0) = 0
+            </if>
             AND c.pid <![CDATA[>]]> 0
             <if test="cateType != null">
                 AND c.cate_type = #{cateType}
@@ -125,10 +133,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             </if>
         </where>
         ORDER BY
-        <if test="yxxTag != null and yxxTag == 1 and homePage != null and homePage == 1">
-            ifnull(d.rec_home_course_top_sort,9999),
+        <if test="yxxTag != null and yxxTag > 0 and homePage != null and homePage == 1">
+            ifnull((CASE WHEN #{yxxTag} = <include refid="appChannelYuanXiangXingRootId"/> THEN d.rec_home_course_top_sort ELSE d.rec_home_long_video_sort END), 9999),
         </if>
-        <if test="yxxTag == null and homePage != null and homePage == 1">
+        <if test="(yxxTag == null or yxxTag == 0) and homePage != null and homePage == 1">
             ifnull(d.rec_home_long_video_sort,9999),
         </if>
         c.sort ASC, c.cate_id ASC
@@ -158,6 +166,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time,</if>
             <if test="isDel != null">is_del,</if>
             <if test="cateType != null">cate_type,</if>
+            <if test="appChannelFlag != null">app_channel_flag,</if>
             <if test="userId != null">user_id,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
@@ -169,6 +178,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">#{updateTime},</if>
             <if test="isDel != null">#{isDel},</if>
             <if test="cateType != null">#{cateType},</if>
+            <if test="appChannelFlag != null">#{appChannelFlag},</if>
             <if test="userId != null">#{userId},</if>
          </trim>
     </insert>
@@ -184,6 +194,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="isDel != null">is_del = #{isDel},</if>
             <if test="cateType != null">cate_type = #{cateType},</if>
+            <if test="appChannelFlag != null">app_channel_flag = #{appChannelFlag},</if>
         </trim>
         where cate_id = #{cateId}
     </update>

+ 7 - 5
fs-service/src/main/resources/mapper/course/FsUserCourseMapper.xml

@@ -408,11 +408,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         AND c.is_private = 0
 
 
-        <if test="q.yxxTag != null and q.yxxTag == 1">
-            AND c.cate_id = (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1)
-        </if>
-        <if test="q.yxxTag == null or q.yxxTag == 0">
-            AND c.cate_id != (select cate_id from fs_user_course_category WHERE cate_name like '%央广原乡行%' AND cate_type = 1 limit 1)
+        <if test="q.yxxTag != null and q.yxxTag > 0">
+            AND c.cate_id = #{q.yxxTag}
+            AND EXISTS (
+                SELECT 1 FROM fs_user_course_category ch
+                WHERE ch.cate_id = #{q.yxxTag} AND ch.pid = 0 AND ch.is_del = 0
+                AND IFNULL(ch.app_channel_flag, 0) = 1
+            )
         </if>
           AND EXISTS (
                 SELECT 1 FROM fs_user_course_category pc2

+ 9 - 3
fs-user-app/src/main/java/com/fs/app/controller/CourseController.java

@@ -131,12 +131,15 @@ public class CourseController extends  AppBaseController{
     /**
      * 小程序端:公域课程分类分页(默认 cateType=1,与后台公域课分类一致)
      */
-    @ApiOperation(value = "小程序-课程分类分页", notes = "仅返回「二级分类」,且(按 yxxTag)至少被一门公域课占用;一级父分类也需为公域。可选 pid=一级 cateId 收窄范围。yxxTag:不传/0=仅统计/展示未打「原乡行」标签的课所挂分类,1=仅含「原乡行」标签的课。排序 sort asc, cate_id asc。")
+    @ApiOperation(value = "小程序-课程分类分页", notes = "yxxTag:0或不传=全部公域二级分类;>0 传频道一级 cateId 查该频道。homePage=1 时 0/null 用长视频推荐排序,频道用对应推荐位。")
     @GetMapping("/publicCourseCategory/list")
     public R listPublicCourseCategory(FsUserCourseCategoryAppQueryParam param) {
         if (param == null) {
             param = new FsUserCourseCategoryAppQueryParam();
         }
+        if (param.getYxxTag() != null && param.getYxxTag() == 0L) {
+            param.setYxxTag(39L);
+        }
         PageInfo<FsUserCourseCategory> pageInfo = courseCategoryService.selectFsUserCourseCategoryAppPage(param);
         if (StringUtils.isNotEmpty(param.getCateName())) {
             int relatedCourseCount = (int) pageInfo.getTotal();
@@ -159,12 +162,15 @@ public class CourseController extends  AppBaseController{
     @ApiOperation(value = "小程序-公域课程分页", notes = "仅公域课;返回封面、标题、看课人数、推荐管理字段。看课人数=fs_course_watch_log send_type=1 去重 user_id。"
             + "分类:同时传 cateId+subCateId 时按父子双条件;仅 subCateId 按二级;仅 cateId 时按一级或二级 id 匹配(cate_id 或 sub_cate_id)。"
             + "推荐位 recommendSlot:1首页顶部 2商城首页 3长视频瀑布流,仅返回对应推荐位已勾选课程并按位置序号排序。"
-            + "yxxTag:不传/0=只查课程 tags 中不含「原乡行」的;1=只查带「原乡行」标签的;依据 fs_user_course.tags(逗号分隔)。")
+            + "yxxTag:0或不传=全部公域课;>0 传频道一级 cateId 查该频道。")
     @GetMapping("/publicCourse/list")
     public R listPublicCourse(FsUserCoursePublicAppQueryParam param) {
         if (param == null) {
             param = new FsUserCoursePublicAppQueryParam();
         }
+        if (param.getYxxTag() != null && param.getYxxTag() == 0L) {
+            param.setYxxTag(39L);
+        }
         PageInfo<FsUserCoursePublicAppVO> pageInfo = courseService.selectFsUserCoursePublicAppPage(param);
         if (StringUtils.isNotEmpty(param.getKeyword())) {
             int relatedCourseCount = (int) pageInfo.getTotal();
@@ -224,7 +230,7 @@ public class CourseController extends  AppBaseController{
     }
 
     @Login
-    @ApiOperation("获取在学课程")
+    @ApiOperation(value = "获取在学课程", notes = "courseType:0或不传=非频道公域课学习记录(app_channel_flag=0);>0=频道一级cateId")
     @GetMapping("/getCourseStudyList")
     public R getMyCourseList(FsUserCourseListUParam param)
     {

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

@@ -21,6 +21,8 @@ import com.fs.live.service.ILiveDataService;
 import com.fs.live.vo.LiveAppSimpleVO;
 import com.fs.store.config.ConceptConfig;
 import com.fs.system.service.ISysConfigService;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.system.vo.DictVO;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import io.swagger.annotations.Api;
@@ -68,6 +70,8 @@ public class IndexScrmController extends AppBaseController {
 	@Autowired
 	private ISysConfigService configService;
 	@Autowired
+	private ISysDictDataService dictDataService;
+	@Autowired
 	private IFsCoursePlaySourceConfigService coursePlaySourceConfigService;
 
 	@Autowired
@@ -401,18 +405,23 @@ public class IndexScrmController extends AppBaseController {
 		String json=configService.selectConfigByKey("his.store");
 		Object moduleOne = null;
 		Object moduleTwo = null;
+		List<DictVO> courseChannels = null;
 		switch (name){
 			case "moduleShow":
 			case "enableHomeModuleTwoShow":
 				StoreConfig storeConfig = JSONUtil.toBean(json, StoreConfig.class);
 				moduleOne = storeConfig.getEnableHomeModuleOneShow();
 				moduleTwo = storeConfig.getEnableHomeModuleTwoShow();
+				courseChannels = dictDataService.selectDictDataListByType("app_course_channel");
 				break;
 			default:
 				break;
 		}
 
-		return R.ok().put("moduleOneShow",moduleOne).put("moduleTwoShow",moduleTwo);
+		return R.ok()
+				.put("moduleOneShow", moduleOne)
+				.put("moduleTwoShow", moduleTwo)
+				.put("courseChannels", courseChannels != null ? courseChannels : Collections.emptyList());
 	}
 
 	/**