Explorar el Código

聚水潭合单功能,单笔订单限购,跳过答题,课程评分

yuhongqi hace 1 semana
padre
commit
2cb631e42e
Se han modificado 28 ficheros con 794 adiciones y 227 borrados
  1. 8 0
      fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java
  2. 2 14
      fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java
  3. 9 0
      fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java
  4. 4 0
      fs-service/src/main/java/com/fs/course/config/CourseConfig.java
  5. 3 3
      fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java
  6. 5 0
      fs-service/src/main/java/com/fs/course/param/CourseStatisticsUserDetailParam.java
  7. 69 53
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  8. 25 4
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java
  9. 55 0
      fs-service/src/main/java/com/fs/course/util/CourseConfigUserAnswerExpose.java
  10. 57 0
      fs-service/src/main/java/com/fs/course/util/CourseRatingAnswerExtract.java
  11. 6 0
      fs-service/src/main/java/com/fs/course/vo/CourseStatisticsUserDetailVO.java
  12. 4 0
      fs-service/src/main/java/com/fs/erp/service/impl/FsJstAftersalePushScrmServiceImpl.java
  13. 93 4
      fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java
  14. 3 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java
  15. 4 0
      fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java
  16. 176 104
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java
  17. 28 7
      fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java
  18. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/FsStoreProductAddEditParam.java
  19. 36 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java
  20. 79 2
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java
  21. 6 0
      fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreProductScrmServiceImpl.java
  22. 3 0
      fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductQueryVO.java
  23. 14 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java
  24. 1 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  25. 93 32
      fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml
  26. 2 1
      fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml
  27. 6 2
      fs-service/src/main/resources/mapper/hisStore/FsStoreProductScrmMapper.xml
  28. 1 1
      fs-service/src/main/resources/mapper/live/LiveMapper.xml

+ 8 - 0
fs-admin/src/main/java/com/fs/course/controller/FsCourseWatchLogController.java

@@ -10,6 +10,7 @@ import com.fs.course.param.FsCourseWatchLogListParam;
 import com.fs.course.param.FsCourseWatchLogStatisticsListParam;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
+import com.fs.course.util.CourseConfigUserAnswerExpose;
 import com.fs.course.vo.*;
 import com.fs.qw.param.QwWatchLogStatisticsListParam;
 import com.fs.qw.service.IQwWatchLogService;
@@ -34,6 +35,7 @@ import com.fs.course.domain.FsCourseWatchLog;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
+import com.fs.system.service.ISysConfigService;
 
 /**
  * 短链课程看课记录Controller
@@ -54,6 +56,8 @@ public class FsCourseWatchLogController extends BaseController
     private IFsUserCoursePeriodDaysService userCoursePeriodDaysService;
     @Autowired
     private IFsUserCoursePeriodService userCoursePeriodService;
+    @Autowired
+    private ISysConfigService configService;
     /**
      * 查询短链课程看课记录列表
      */
@@ -281,6 +285,8 @@ public class FsCourseWatchLogController extends BaseController
         param.setPeriodId(periodId);
         param.setPageNum(pageNum);
         param.setPageSize(pageSize);
+        String courseCfg = configService.selectConfigByKey("course.config");
+        param.setIncludeCourseRating(CourseConfigUserAnswerExpose.includeUserAnswerContentInStatistics(courseCfg));
         PageHelper.startPage(pageNum, pageSize);
         return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.getCourseStatisticsUserDetailList(param)));
     }
@@ -300,6 +306,8 @@ public class FsCourseWatchLogController extends BaseController
         com.fs.course.param.CourseStatisticsUserDetailParam param = new com.fs.course.param.CourseStatisticsUserDetailParam();
         param.setVideoId(videoId);
         param.setPeriodId(periodId);
+        String courseCfg = configService.selectConfigByKey("course.config");
+        param.setIncludeCourseRating(CourseConfigUserAnswerExpose.includeUserAnswerContentInStatistics(courseCfg));
         List<com.fs.course.vo.CourseStatisticsUserDetailVO> list = fsCourseWatchLogService.getCourseStatisticsUserDetailExportList(param);
         ExcelUtil<com.fs.course.vo.CourseStatisticsUserDetailVO> util = new ExcelUtil<>(com.fs.course.vo.CourseStatisticsUserDetailVO.class);
         return util.exportExcel(list, "用户看课数据");

+ 2 - 14
fs-admin/src/main/java/com/fs/hisStore/controller/FsStoreHealthOrderScrmController.java

@@ -171,16 +171,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
     @Log(title = "健康商城订单", businessType = BusinessType.EXPORT)
     @PostMapping("/healthExport")
     public AjaxResult export1(@RequestBody FsStoreOrderParam param) {
-        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())){
-            param.setBeginTime(null);
-            param.setEndTime(null);
-        }
+        normalizeExportParam(param);
         if (fsStoreOrderService.isEntityNull(param)){
             return AjaxResult.error("请筛选数据导出");
         }
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
         if(!StringUtils.isEmpty(param.getPayTimeRange())){
             param.setPayTimeList(param.getPayTimeRange().split("--"));
         }
@@ -231,16 +225,10 @@ public class FsStoreHealthOrderScrmController extends BaseController {
     @Log(title = "健康商城订单", businessType = BusinessType.EXPORT)
     @PostMapping("/healthExportDetails")
     public AjaxResult healthExportDetails(@RequestBody FsStoreOrderParam param) {
-        if ("".equals(param.getBeginTime()) && "".equals(param.getEndTime())){
-            param.setBeginTime(null);
-            param.setEndTime(null);
-        }
+        normalizeExportParam(param);
         if (fsStoreOrderService.isEntityNull(param)){
             return AjaxResult.error("请筛选数据导出");
         }
-        if(!StringUtils.isEmpty(param.getCreateTimeRange())){
-            param.setCreateTimeList(param.getCreateTimeRange().split("--"));
-        }
         if(!StringUtils.isEmpty(param.getPayTimeRange())){
             param.setPayTimeList(param.getPayTimeRange().split("--"));
         }

+ 9 - 0
fs-company/src/main/java/com/fs/company/controller/course/FsCourseWatchLogController.java

@@ -16,6 +16,7 @@ import com.fs.course.param.*;
 import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
+import com.fs.course.util.CourseConfigUserAnswerExpose;
 import com.fs.course.vo.FsCourseOverVO;
 import com.fs.course.vo.FsCourseUserStatisticsListVO;
 import com.fs.course.vo.FsCourseWatchLogListVO;
@@ -29,6 +30,7 @@ import com.fs.qw.service.IQwWatchLogService;
 import com.fs.qw.vo.QwWatchLogAllStatisticsListVO;
 import com.fs.qw.vo.QwWatchLogStatisticsListVO;
 import com.fs.sop.mapper.SopUserLogsMapper;
+import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import com.google.gson.Gson;
@@ -72,6 +74,9 @@ public class FsCourseWatchLogController extends BaseController
     @Autowired
     private IQwTagService iQwTagService;
 
+    @Autowired
+    private ISysConfigService configService;
+
     /**
      * 查询短链课程看课记录列表
      */
@@ -447,6 +452,8 @@ public class FsCourseWatchLogController extends BaseController
         if(!"00".equals(loginUser.getUser().getUserType())){
             param.setCompanyUserId(loginUser.getUser().getUserId());
         }
+        String courseCfg = configService.selectConfigByKey("course.config");
+        param.setIncludeCourseRating(CourseConfigUserAnswerExpose.includeUserAnswerContentInStatistics(courseCfg));
         PageHelper.startPage(pageNum, pageSize);
         return R.ok().put("data", new PageInfo<>(fsCourseWatchLogService.getCourseStatisticsUserDetailList(param)));
     }
@@ -474,6 +481,8 @@ public class FsCourseWatchLogController extends BaseController
         if(!"00".equals(loginUser.getUser().getUserType())){
             param.setCompanyUserId(loginUser.getUser().getUserId());
         }
+        String courseCfg = configService.selectConfigByKey("course.config");
+        param.setIncludeCourseRating(CourseConfigUserAnswerExpose.includeUserAnswerContentInStatistics(courseCfg));
         List<com.fs.course.vo.CourseStatisticsUserDetailVO> list = fsCourseWatchLogService.getCourseStatisticsUserDetailExportList(param);
         ExcelUtil<com.fs.course.vo.CourseStatisticsUserDetailVO> util = new ExcelUtil<>(com.fs.course.vo.CourseStatisticsUserDetailVO.class);
         return util.exportExcel(list, "用户看课数据");

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

@@ -12,6 +12,10 @@ import java.util.List;
 public class CourseConfig implements Serializable {
     private Integer answerRate; //可答题百分比
     private Integer answerErrorCount; //允许错误次数
+    /**
+     * 看课是否校验答案:1-是(默认) 0-否,与后台点播配置 JSON 字段 validateAnswerWhenWatch 一致
+     */
+        private String validateAnswerWhenWatch;
     private Integer videoLinkExpireDate; //课程小节短链过期时间
     private Integer maxBufferLength;//最大缓冲时长
     private Integer videoIntegral;//每十分钟获取多少积分

+ 3 - 3
fs-service/src/main/java/com/fs/course/mapper/FsCourseWatchLogMapper.java

@@ -773,10 +773,10 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
     Long selectVideoDurationByVideoId(@Param("videoId") Long videoId);
 
     /**
-     * 统计累计观看人数(对userId去重
+     * 统计累计到课人数(对 userId 去重,存在本视频本营期且 duration&gt;0 的看课记录
      * @param videoId 视频ID
      * @param periodId 营期ID
-     * @return 累计观看人数
+     * @return 累计到课人数
      */
     @Select("<script>" +
             "SELECT COUNT(DISTINCT user_id) FROM fs_course_watch_log " +
@@ -790,7 +790,7 @@ public interface FsCourseWatchLogMapper extends BaseMapper<FsCourseWatchLog> {
                                  @Param("companyUserId") Long companyUserId);
 
     /**
-     * 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重
+     * 统计累计完课人数(存在完课记录 log_type=2 且 duration&gt;0,对 user_id 去重;与「实际看课数据」口径一致
      * @param videoId 视频ID
      * @param periodId 营期ID
      * @return 累计完课人数

+ 5 - 0
fs-service/src/main/java/com/fs/course/param/CourseStatisticsUserDetailParam.java

@@ -27,4 +27,9 @@ public class CourseStatisticsUserDetailParam implements Serializable {
 
     // 公司ID
     private Long companyUserId;
+
+    /**
+     * 为 true 时 SQL 联表查询答题日志 question_json,填充 VO.courseRating(课程评分-用户作答内容)
+     */
+    private Boolean includeCourseRating;
 }

+ 69 - 53
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -15,6 +15,7 @@ import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.mapper.*;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
 import com.fs.course.service.IFsCourseQuestionBankService;
+import com.fs.course.util.CourseConfigUserAnswerExpose;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsStorePaymentService;
@@ -155,6 +156,8 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         new FsCourseAnswerLogs();
         FsCourseAnswerLogs rightLog;
         //判断短链类型
+        boolean skipAnswerValidation = CourseConfigUserAnswerExpose.skipValidateAnswerOnSubmit(json);
+
 
         if ("泽林文化".equals(signProjectName)){
             FsCourseWatchLog log = courseWatchLogMapper.getWatchLogByFsUserAndPeriodId(param.getVideoId(), param.getUserId(), param.getCompanyUserId(),param.getPeriodId());
@@ -218,31 +221,35 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         // 一次性获取所有问题的正确答案
         List<FsCourseQuestionBank> questions = param.getQuestions();
         if (questions != null && !questions.isEmpty()) {
-            Map<Long, FsCourseQuestionBank> correctAnswersMap = fsCourseQuestionBankMapper.selectFsCourseQuestionBankByIds(
-                    questions.stream().map(FsCourseQuestionBank::getId).collect(Collectors.toList())
-            ).stream().collect(Collectors.toMap(FsCourseQuestionBank::getId, question -> question));
-
-            for (FsCourseQuestionBank questionBank : questions) {
-                FsCourseQuestionBank correctAnswer = correctAnswersMap.get(questionBank.getId());
-                if (correctAnswer.getType() == 1) {
-                    if (questionBank.getAnswer().equals(correctAnswer.getAnswer())) {
-                        thisRightCount++;
-                    } else {
-                        correctAnswer.setAnswer(null);
-                        incorrectQuestions.add(correctAnswer);
-                    }
-                } else if (correctAnswer.getType() == 2) {
-                    String[] userAnswers = convertStringToArray(questionBank.getAnswer());
-                    String[] correctAnswers = convertStringToArray(correctAnswer.getAnswer());
-
-                    Arrays.sort(userAnswers);
-                    Arrays.sort(correctAnswers);
-
-                    if (Arrays.equals(userAnswers, correctAnswers)) {
-                        thisRightCount++;
-                    } else {
-                        correctAnswer.setAnswer(null);
-                        incorrectQuestions.add(correctAnswer);
+            if (skipAnswerValidation) {
+                thisRightCount = questions.size();
+            } else {
+                Map<Long, FsCourseQuestionBank> correctAnswersMap = fsCourseQuestionBankMapper.selectFsCourseQuestionBankByIds(
+                        questions.stream().map(FsCourseQuestionBank::getId).collect(Collectors.toList())
+                ).stream().collect(Collectors.toMap(FsCourseQuestionBank::getId, question -> question));
+
+                for (FsCourseQuestionBank questionBank : questions) {
+                    FsCourseQuestionBank correctAnswer = correctAnswersMap.get(questionBank.getId());
+                    if (correctAnswer.getType() == 1) {
+                        if (questionBank.getAnswer().equals(correctAnswer.getAnswer())) {
+                            thisRightCount++;
+                        } else {
+                            correctAnswer.setAnswer(null);
+                            incorrectQuestions.add(correctAnswer);
+                        }
+                    } else if (correctAnswer.getType() == 2) {
+                        String[] userAnswers = convertStringToArray(questionBank.getAnswer());
+                        String[] correctAnswers = convertStringToArray(correctAnswer.getAnswer());
+
+                        Arrays.sort(userAnswers);
+                        Arrays.sort(correctAnswers);
+
+                        if (Arrays.equals(userAnswers, correctAnswers)) {
+                            thisRightCount++;
+                        } else {
+                            correctAnswer.setAnswer(null);
+                            incorrectQuestions.add(correctAnswer);
+                        }
                     }
                 }
             }
@@ -259,7 +266,8 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         logs.setCreateTime(new Date());
         logs.setPeriodId(param.getPeriodId());
 
-        if (thisRightCount == questions.size()) {
+        int questionCount = questions == null ? 0 : questions.size();
+        if (thisRightCount == questionCount) {
             logs.setIsRight(1);
             courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
             return R.ok("答题成功");
@@ -437,32 +445,39 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         }
         int remainCount = config.getAnswerErrorCount()-errorCount-1;
 
-        // 一次性获取所有问题的正确答案
-        Map<Long, FsCourseQuestionBank> correctAnswersMap = fsCourseQuestionBankMapper.selectFsCourseQuestionBankByIds(
-                param.getQuestions().stream().map(FsCourseQuestionBank::getId).collect(Collectors.toList())
-        ).stream().collect(Collectors.toMap(FsCourseQuestionBank::getId, question -> question));
-
-        for (FsCourseQuestionBank questionBank : param.getQuestions()) {
-            FsCourseQuestionBank correctAnswer = correctAnswersMap.get(questionBank.getId());
-            if (correctAnswer.getType() == 1) {
-                if (questionBank.getAnswer().equals(correctAnswer.getAnswer())) {
-                    thisRightCount++;
-                } else {
-                    correctAnswer.setAnswer(null);
-                    incorrectQuestions.add(correctAnswer);
-                }
-            } else if (correctAnswer.getType() == 2) {
-                String[] userAnswers = convertStringToArray(questionBank.getAnswer());
-                String[] correctAnswers = convertStringToArray(correctAnswer.getAnswer());
-
-                Arrays.sort(userAnswers);
-                Arrays.sort(correctAnswers);
-
-                if (Arrays.equals(userAnswers, correctAnswers)) {
-                    thisRightCount++;
-                } else {
-                    correctAnswer.setAnswer(null);
-                    incorrectQuestions.add(correctAnswer);
+        boolean skipAnswerValidation = CourseConfigUserAnswerExpose.skipValidateAnswerOnSubmit(json);
+        List<FsCourseQuestionBank> questions = param.getQuestions();
+        if (questions != null && !questions.isEmpty()) {
+            if (skipAnswerValidation) {
+                thisRightCount = questions.size();
+            } else {
+                Map<Long, FsCourseQuestionBank> correctAnswersMap = fsCourseQuestionBankMapper.selectFsCourseQuestionBankByIds(
+                        questions.stream().map(FsCourseQuestionBank::getId).collect(Collectors.toList())
+                ).stream().collect(Collectors.toMap(FsCourseQuestionBank::getId, question -> question));
+
+                for (FsCourseQuestionBank questionBank : questions) {
+                    FsCourseQuestionBank correctAnswer = correctAnswersMap.get(questionBank.getId());
+                    if (correctAnswer.getType() == 1) {
+                        if (questionBank.getAnswer().equals(correctAnswer.getAnswer())) {
+                            thisRightCount++;
+                        } else {
+                            correctAnswer.setAnswer(null);
+                            incorrectQuestions.add(correctAnswer);
+                        }
+                    } else if (correctAnswer.getType() == 2) {
+                        String[] userAnswers = convertStringToArray(questionBank.getAnswer());
+                        String[] correctAnswers = convertStringToArray(correctAnswer.getAnswer());
+
+                        Arrays.sort(userAnswers);
+                        Arrays.sort(correctAnswers);
+
+                        if (Arrays.equals(userAnswers, correctAnswers)) {
+                            thisRightCount++;
+                        } else {
+                            correctAnswer.setAnswer(null);
+                            incorrectQuestions.add(correctAnswer);
+                        }
+                    }
                 }
             }
         }
@@ -479,7 +494,8 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         logs.setCreateTime(new Date());
         logs.setPeriodId(param.getPeriodId());
 
-        if (thisRightCount == param.getQuestions().size()) {
+        int questionCount = questions == null ? 0 : questions.size();
+        if (thisRightCount == questionCount) {
             logs.setIsRight(1);
             courseAnswerLogsMapper.insertFsCourseAnswerLogs(logs);
             return R.ok("答题成功");

+ 25 - 4
fs-service/src/main/java/com/fs/course/service/impl/FsCourseWatchLogServiceImpl.java

@@ -41,6 +41,7 @@ import com.fs.course.service.IFsCourseWatchLogService;
 import com.fs.course.service.IFsUserCoursePeriodDaysService;
 import com.fs.course.service.IFsUserCoursePeriodService;
 import com.fs.course.service.cache.IFsUserCourseVideoCacheService;
+import com.fs.course.util.CourseRatingAnswerExtract;
 import com.fs.course.vo.*;
 import com.fs.his.config.FsSysConfig;
 import com.fs.his.domain.FsUser;
@@ -1784,11 +1785,11 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
 //        FsUserCoursePeriod fsUserCoursePeriod = fsUserCoursePeriodMapper.selectFsUserCoursePeriodById(periodId);
 
 
-        // 2. 统计累计观看人数(对userId去重
+        // 2. 统计累计到课人数(对 userId 去重,需存在 duration>0 的看课记录,与「实际看课」i.1 一致
         Long totalWatchCount = fsCourseWatchLogMapper.countDistinctWatchUsers(videoId, periodId,companyId,companyUserId);
         vo.setTotalWatchCount(totalWatchCount != null ? totalWatchCount : 0L);
 
-        // 3. 统计累计完课人数(duration >= 1200秒,即20分钟,对userId去重)
+        // 3. 统计累计完课人数(log_type=2 且 duration>0,对 userId 去重,与「实际看课」i.2 一致
         Long totalCompleteCount = fsCourseWatchLogMapper.countDistinctCompleteUsers(videoId, periodId,companyId,companyUserId);
         vo.setTotalCompleteCount(totalCompleteCount != null ? totalCompleteCount : 0L);
 
@@ -1926,7 +1927,9 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         if (param == null || param.getVideoId() == null || param.getPeriodId() == null) {
             return Collections.emptyList();
         }
-        return fsCourseWatchLogMapper.selectCourseStatisticsUserDetailList(param);
+        List<CourseStatisticsUserDetailVO> list = fsCourseWatchLogMapper.selectCourseStatisticsUserDetailList(param);
+        applyCourseRatingAnswerDisplay(list);
+        return list;
     }
 
     @Override
@@ -1934,7 +1937,25 @@ public class FsCourseWatchLogServiceImpl extends ServiceImpl<FsCourseWatchLogMap
         if (param == null || param.getVideoId() == null || param.getPeriodId() == null) {
             return Collections.emptyList();
         }
-        return fsCourseWatchLogMapper.selectCourseStatisticsUserDetailExportList(param);
+        List<CourseStatisticsUserDetailVO> list = fsCourseWatchLogMapper.selectCourseStatisticsUserDetailExportList(param);
+        applyCourseRatingAnswerDisplay(list);
+        return list;
+    }
+
+    private void applyCourseRatingAnswerDisplay(List<CourseStatisticsUserDetailVO> list) {
+        if (CollectionUtils.isEmpty(list)) {
+            return;
+        }
+        for (CourseStatisticsUserDetailVO vo : list) {
+            if (vo == null) {
+                continue;
+            }
+            String raw = vo.getCourseRating();
+            if (raw == null || raw.isEmpty()) {
+                continue;
+            }
+            vo.setCourseRating(CourseRatingAnswerExtract.toDisplayCourseRating(raw));
+        }
     }
 
     @Override

+ 55 - 0
fs-service/src/main/java/com/fs/course/util/CourseConfigUserAnswerExpose.java

@@ -0,0 +1,55 @@
+package com.fs.course.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+
+/**
+ * 点播配置 course.config(参数键 course.config):与 JSON 字段 validateAnswerWhenWatch 相关的判定。
+ */
+public final class CourseConfigUserAnswerExpose {
+
+    private CourseConfigUserAnswerExpose() {
+    }
+
+    /**
+     * 当 JSON 中存在 validateAnswerWhenWatch 且非 null,并表示「不校验答案」(false / 0 / "0" / "false")时返回 true。
+     */
+    public static boolean isValidateAnswerWhenWatchDisabled(String courseConfigJson) {
+        if (StrUtil.isBlank(courseConfigJson)) {
+            return false;
+        }
+        try {
+            JSONObject obj = JSONUtil.parseObj(courseConfigJson);
+            if (!obj.containsKey("validateAnswerWhenWatch")) {
+                return false;
+            }
+            Object v = obj.get("validateAnswerWhenWatch");
+            if (v == null) {
+                return false;
+            }
+            if (Boolean.FALSE.equals(v)) {
+                return true;
+            }
+            String s = String.valueOf(v).trim();
+            return "0".equals(s) || "false".equalsIgnoreCase(s);
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * 当 JSON 中存在 validateAnswerWhenWatch 且表示「不校验答案」(false / 0 / "0" / "false")时返回 true,
+     * 此时课程小结用户详情/导出可展示「课程评分」字段(答题内容)。
+     */
+    public static boolean includeUserAnswerContentInStatistics(String courseConfigJson) {
+        return isValidateAnswerWhenWatchDisabled(courseConfigJson);
+    }
+
+    /**
+     * 会员/ H5 提交答题时是否跳过对错校验:关闭校验时任意作答视为全对,写入正确答题日志,可走后续积分/红包领取流程。
+     */
+    public static boolean skipValidateAnswerOnSubmit(String courseConfigJson) {
+        return isValidateAnswerWhenWatchDisabled(courseConfigJson);
+    }
+}

+ 57 - 0
fs-service/src/main/java/com/fs/course/util/CourseRatingAnswerExtract.java

@@ -0,0 +1,57 @@
+package com.fs.course.util;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+
+/**
+ * 从答题日志 question_json(JSON 数组)中提取各题 {@code answer},作为统计/导出中的课程评分展示值。
+ */
+public final class CourseRatingAnswerExtract {
+
+    private CourseRatingAnswerExtract() {
+    }
+
+    /**
+     * 解析数组元素中的 {@code answer};多条时用中文分号拼接。非 JSON 数组或解析失败时返回原文。
+     */
+    public static String toDisplayCourseRating(String questionJson) {
+        if (StrUtil.isBlank(questionJson)) {
+            return questionJson;
+        }
+        String trimmed = questionJson.trim();
+        if (!trimmed.startsWith("[")) {
+            return questionJson;
+        }
+        try {
+            JSONArray arr = JSONUtil.parseArray(questionJson);
+            if (arr == null || arr.isEmpty()) {
+                return questionJson;
+            }
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < arr.size(); i++) {
+                JSONObject o = arr.getJSONObject(i);
+                if (o == null || !o.containsKey("answer")) {
+                    continue;
+                }
+                Object a = o.get("answer");
+                if (a == null) {
+                    continue;
+                }
+                String s = String.valueOf(a).trim();
+                if (s.isEmpty()) {
+                    continue;
+                }
+                if (sb.length() > 0) {
+                    sb.append(';');
+                }
+                sb.append(s);
+                break;
+            }
+            return sb.length() > 0 ? sb.toString() : questionJson;
+        } catch (Exception e) {
+            return questionJson;
+        }
+    }
+}

+ 6 - 0
fs-service/src/main/java/com/fs/course/vo/CourseStatisticsUserDetailVO.java

@@ -31,4 +31,10 @@ public class CourseStatisticsUserDetailVO implements Serializable {
     private String companyName;
     @Excel(name = "销售名称")
     private String salesName;
+
+    /**
+     * 看课不校验答案时导出/列表展示:用户最近一次提交答题的 question_json 原文(多题为 JSON 字符串)
+     */
+    @Excel(name = "课程评分", width = 60)
+    private String courseRating;
 }

+ 4 - 0
fs-service/src/main/java/com/fs/erp/service/impl/FsJstAftersalePushScrmServiceImpl.java

@@ -197,6 +197,10 @@ public class FsJstAftersalePushScrmServiceImpl implements FsJstAftersalePushScrm
             itemDTO.setQty(cartDTO.getNum());
             itemDTO.setAmount(cartDTO.getPrice());
             itemDTO.setType("退货");
+            if (StringUtils.isNotEmpty(fsStoreOrder.getOuterOiId())) {
+                itemDTO.setOuterOiId(fsStoreOrder.getOuterOiId());
+            }
+
             refundItemDTOS.add(itemDTO);
         }
         dto.setItems(refundItemDTOS);

+ 93 - 4
fs-service/src/main/java/com/fs/erp/service/impl/JSTErpOrderServiceImpl.java

@@ -29,6 +29,7 @@ import com.fs.his.service.IFsStoreProductService;
 import com.fs.hisStore.domain.FsStoreOrderItemScrm;
 import com.fs.hisStore.domain.FsStoreOrderScrm;
 import com.fs.hisStore.domain.FsStoreProductScrm;
+import com.fs.hisStore.mapper.FsStoreOrderScrmMapper;
 import com.fs.hisStore.service.IFsStoreOrderItemScrmService;
 import com.fs.hisStore.service.IFsStoreOrderScrmService;
 import com.fs.hisStore.service.IFsStoreProductScrmService;
@@ -54,6 +55,10 @@ import java.util.stream.Collectors;
 @Slf4j
 @Service
 public class JSTErpOrderServiceImpl implements IErpOrderService {
+
+    /** 合并订单:按 linkOId 跟进拉主单的最大轮数 */
+    private static final int JST_MERGED_FOLLOW_MAX_ROUNDS = 3;
+
     @Autowired
     private JstErpHttpService jstErpHttpService;
 
@@ -63,6 +68,9 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
     @Autowired
     private IFsStoreOrderScrmService fsStoreOrderScrmService;
 
+    @Autowired
+    private FsStoreOrderScrmMapper fsStoreOrderScrmMapper;
+
     @Autowired
     private IFsStoreOrderItemService fsStoreOrderItemService;
     @Autowired
@@ -535,10 +543,26 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
 
         // 5. 转换订单数据
         if (query.getOrders() != null && !query.getOrders().isEmpty()) {
-            List<ErpOrderQuery> erpOrders = query.getOrders().stream()
-                    .map(this::convertToErpOrderQueryScrm)
-                    .collect(Collectors.toList());
-            response.setOrders(erpOrders);
+            OrderQueryResponseDTO.Order firstOrder = query.getOrders().get(0);
+
+            if (ErpQueryOrderStatusEnum.MERGED.getCode().equals(firstOrder.getStatus())
+                    && StringUtils.isNotEmpty(firstOrder.getLinkOId())) {
+                OrderQueryResponseDTO mergeQuery = followMergedJstQueryResponse(query, 1);
+                if (mergeQuery != null && mergeQuery.getOrders() != null && !mergeQuery.getOrders().isEmpty()) {
+                    List<ErpOrderQuery> erpOrders = mergeQuery.getOrders().stream()
+                            .map(this::convertToErpOrderQueryScrm)
+                            .collect(Collectors.toList());
+                    response.setOrders(erpOrders);
+                    persistOuterOiIdFromJstMergedOrder(firstOrder);
+                } else {
+                    response.setOrders(Collections.emptyList());
+                }
+            } else {
+                List<ErpOrderQuery> erpOrders = query.getOrders().stream()
+                        .map(this::convertToErpOrderQueryScrm)
+                        .collect(Collectors.toList());
+                response.setOrders(erpOrders);
+            }
         } else {
             response.setOrders(Collections.emptyList());
         }
@@ -858,7 +882,72 @@ public class JSTErpOrderServiceImpl implements IErpOrderService {
         return erpOrder;
     }
 
+    /**
+     * 合并单跟进成功后:取「原合并子单」firstOrder 明细首行的 outerOiId,按 soId=order_code 仅更新商城订单 outer_oi_id。
+     */
+    private void persistOuterOiIdFromJstMergedOrder(OrderQueryResponseDTO.Order firstOrder) {
+        if (firstOrder == null || StringUtils.isEmpty(firstOrder.getSoId())) {
+            return;
+        }
+        List<OrderQueryResponseDTO.OrderItem> items = firstOrder.getItems();
+        if (CollectionUtils.isEmpty(items)) {
+            return;
+        }
+        OrderQueryResponseDTO.OrderItem line = items.get(0);
+        if (line == null || StringUtils.isEmpty(line.getOuterOiId())) {
+            return;
+        }
+        int rows = fsStoreOrderScrmMapper.updateOuterOiIdByOrderCode(firstOrder.getSoId(), line.getOuterOiId());
+        if (rows > 0) {
+            log.info("合并订单:已回写 outerOiId,orderCode={} outerOiId={}", firstOrder.getSoId(), line.getOuterOiId());
+        }
+    }
+
+    private static String firstOrderSoId(OrderQueryResponseDTO query) {
+        if (query == null || CollectionUtils.isEmpty(query.getOrders())) {
+            return null;
+        }
+        return query.getOrders().get(0).getSoId();
+    }
 
+    /**
+     * 首单若为「被合并」且带 linkOId,则按 oId 整单重查明细,最多 maxRounds 轮。
+     */
+    private OrderQueryResponseDTO followMergedJstQueryResponse(OrderQueryResponseDTO query, int maxRounds) {
+        if (query == null || CollectionUtils.isEmpty(query.getOrders())) {
+            return query;
+        }
+        OrderQueryResponseDTO current = query;
+        for (int round = 0; round < maxRounds; round++) {
+            if (current.getOrders() == null || current.getOrders().isEmpty()) {
+                break;
+            }
+            OrderQueryResponseDTO.Order first = current.getOrders().get(0);
+            if (!ErpQueryOrderStatusEnum.MERGED.getCode().equals(first.getStatus())
+                    || StringUtils.isEmpty(first.getLinkOId())) {
+                break;
+            }
+            long linkOid;
+            try {
+                linkOid = Long.parseLong(first.getLinkOId().trim());
+            } catch (NumberFormatException e) {
+                log.warn("聚水潭合单 linkOId 非数字: {}, soId={}", first.getLinkOId(), first.getSoId());
+                break;
+            }
+            OrderQueryRequestDTO requestDTO = new OrderQueryRequestDTO();
+            requestDTO.setOIds(Collections.singletonList(linkOid));
+            OrderQueryResponseDTO next = jstErpHttpService.query(requestDTO);
+            if (next == null || CollectionUtils.isEmpty(next.getOrders())) {
+                log.warn("聚水潭按 linkOId={} 重新查询无结果, 子单 soId={}", linkOid, first.getSoId());
+                break;
+            }
+            log.info("合单重新查询聚水潭: round={}/{}, linkOId={}, 子单 oId/soId={}/{}, 新单首条 oId/soId={}/{}",
+                    round + 1, maxRounds, linkOid, first.getOId(), first.getSoId(),
+                    next.getOrders().get(0).getOId(), next.getOrders().get(0).getSoId());
+            current = next;
+        }
+        return current;
+    }
 
     private ErpOrderQuery convertToErpOrderQueryScrm(OrderQueryResponseDTO.Order order) {
         ErpOrderQuery erpOrder = new ErpOrderQuery();

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreOrderScrm.java

@@ -27,6 +27,9 @@ public class FsStoreOrderScrm extends BaseEntity
     @Excel(name = "订单号")
     private String orderCode;
 
+    /** 聚水潭外部订单明细ID(outerOiId,合并单等场景取明细首行) */
+    private String outerOiId;
+
     /** 额外订单号 */
     private String extendOrderId;
 

+ 4 - 0
fs-service/src/main/java/com/fs/hisStore/domain/FsStoreProductScrm.java

@@ -350,6 +350,10 @@ public class FsStoreProductScrm extends BaseEntity
     @Excel(name = "限购数量")
     private Integer purchaseLimit;
 
+    /** 单次购买数量上限(0 表示不限制) */
+    @Excel(name = "单次购买数量")
+    private Integer singlePurchaseLimit;
+
     @TableField(exist = false)
     private String onShelfTime;
 

+ 176 - 104
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderItemScrmMapper.java

@@ -88,16 +88,17 @@ public interface FsStoreOrderItemScrmMapper
             " left join company_tcm_schedule cts on cts.id = o.schedule_id " +
             " left join fs_store_product_scrm psps on i.product_id=psps.product_id " +
             " left join fs_store_product_category_scrm fspcs on fspcs.cate_id=psps.cate_id " +
-            "            LEFT JOIN (\n" +
-            "            SELECT\n" +
-            "            sp.*,\n" +
-            "            ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn\n" +
-            "            FROM fs_store_payment_scrm sp\n" +
-            "            WHERE sp.business_code IS NOT NULL\n" +
-            "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
+            " LEFT JOIN (" +
+            " SELECT sp.*, ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn" +
+            " FROM fs_store_payment_scrm sp WHERE sp.business_code IS NOT NULL" +
+            " ) sp_latest ON sp_latest.order_id = o.id AND sp_latest.rn = 1" +
+            " LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id" +
+            " <if test=\"maps.erpAccount != null and maps.erpAccount != ''\">" +
+            " LEFT JOIN fs_store_order_df df on df.order_id=o.id " +
+            " </if>" +
             " where 1=1 " +
-            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
-            " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
+            "<if test=\"maps.appId != null and maps.appId != ''\">" +
+            " and csc.appid = #{maps.appId} " +
             "</if>" +
             "<if test=\"maps.orderCodes != null  and maps.orderCodes.size > 0\">" +
             " and o.order_code in" +
@@ -108,12 +109,24 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +
-            "<if test = 'maps.deliveryId != null    '> " +
-            "and o.delivery_id =#{maps.deliveryId} " +
+            "<if test=\"maps.bankTransactionId != null and  maps.bankTransactionId !=''\">" +
+            " and sp_latest.bank_transaction_id like CONCAT('%',#{maps.bankTransactionId},'%') " +
+            "</if>" +
+            "<if test=\"maps.isPayRemain != null\">" +
+            " and o.is_pay_remain =#{maps.isPayRemain} " +
+            "</if>" +
+            "<if test=\"maps.userId != null\">" +
+            " and o.user_id =#{maps.userId} " +
+            "</if>" +
+            "<if test=\"maps.deliveryId != null and  maps.deliveryId !=''\">" +
+            " and o.delivery_id =#{maps.deliveryId} " +
             "</if>" +
             "<if test = 'maps.nickname != null and  maps.nickname !=\"\"     '> " +
             "and u.nickname like CONCAT('%',#{maps.nickname},'%') " +
             "</if>" +
+            "<if test=\"maps.realName != null and  maps.realName !=''\">" +
+            " and o.real_name like CONCAT('%',#{maps.realName},'%') " +
+            "</if>" +
             "<if test = 'maps.phone != null and  maps.phone !=\"\"     '> " +
             "and u.phone like CONCAT('%',#{maps.phone},'%') " +
             "</if>" +
@@ -126,19 +139,26 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.status != null and maps.status == 6    '> " +
             "and o.`status`= 1 and (o.extend_order_id is null or o.extend_order_id like '') " +
             "</if>" +
+            "<if test = 'maps.isUpload != null and maps.isUpload == 0    '> " +
+            "and o.certificates is null  " +
+            "</if>" +
+            "<if test = 'maps.isUpload != null and maps.isUpload == 1    '> " +
+            "and o.certificates is not null " +
+            "</if>" +
+            "<if test=\"maps.deliveryStatus != null     \">" +
+            " and o.delivery_status =#{maps.deliveryStatus}" +
+            "</if>" +
+            "<if test=\"maps.deliveryPayStatus != null  \">" +
+            " and o.delivery_pay_status =#{maps.deliveryPayStatus}" +
+            "</if>" +
             "<if test = 'maps.companyId != null    '> " +
             "and o.company_id =#{maps.companyId} " +
             "</if>" +
-            "            <if test=\"maps.realName != null and  maps.realName !=''\">\n" +
-            "                and o.real_name like CONCAT('%',#{maps.realName},'%')\n" +
-            "            </if>"+
             "<if test = 'maps.isHealth != null and maps.isHealth !=  \"\"  '> " +
-//            "and (o.company_id is null or o.order_type = 2 ) " +
-            "              and (o.company_id is null\n" +
-            "                or o.order_type = 2 or o.order_type = 3)"+
+            " and (o.company_id is null or o.order_type = 2 or o.order_type = 3)" +
             "</if>" +
-            "<if test = 'maps.notHealth != null and maps.notHealth !=  \"\"  '> " +
-            "and o.company_id is not null " +
+            "<if test = 'maps.notHealth != null  '> " +
+            "and o.company_id is not null and o.order_type = 0 " +
             "</if>" +
             "<if test = 'maps.companyUserId != null    '> " +
             "and o.company_user_id =#{maps.companyUserId} " +
@@ -146,52 +166,70 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=  \"\" '> " +
             "and cu.nick_name like concat('%', #{maps.companyUserNickName}, '%') " +
             "</if>" +
+            "<if test=\"maps.payCode != null and maps.payCode != ''\">" +
+            " and sp_latest.pay_code like CONCAT('%', #{maps.payCode}, '%') " +
+            "</if>" +
+            "<if test=\"maps.productName != null and  maps.productName !=  '' \">" +
+            " and psps.product_name like concat('%', #{maps.productName}, '%') " +
+            "</if>" +
             "<if test = 'maps.orderType != null and maps.orderType != -1    '> " +
             "and o.order_type =#{maps.orderType} " +
             "</if>" +
             "<if test = 'maps.orderType != null and maps.orderType == -1    '> " +
             "and o.order_type in (2, 3) " +
             "</if>" +
+            "<if test = 'maps.payType != null    '> " +
+            "and o.pay_type =#{maps.payType} " +
+            "</if>" +
+            "<if test = 'maps.scheduleId != null    '> " +
+            "and o.schedule_id =#{maps.scheduleId} " +
+            "</if>" +
             "<if test = 'maps.createTimeList != null    '> " +
             " AND date_format(o.create_time,'%y%m%d') &gt;= date_format(#{maps.createTimeList[0]},'%y%m%d') " +
             " AND date_format(o.create_time,'%y%m%d') &lt;= date_format(#{maps.createTimeList[1]},'%y%m%d') " +
             "</if>" +
-            "<if test = 'maps.payTimeList != null    '> " +
-            " AND date_format(o.pay_time,'%y%m%d') &gt;= date_format(#{maps.payTimeList[0]},'%y%m%d') " +
-            " AND date_format(o.pay_time,'%y%m%d') &lt;= date_format(#{maps.payTimeList[1]},'%y%m%d') " +
-            "</if>" +
             "<if test = 'maps.deliverySendTimeList != null    '> " +
             " AND date_format(o.delivery_send_time,'%y%m%d') &gt;= date_format(#{maps.deliverySendTimeList[0]},'%y%m%d') " +
             " AND date_format(o.delivery_send_time,'%y%m%d') &lt;= date_format(#{maps.deliverySendTimeList[1]},'%y%m%d') " +
             "</if>" +
+            "<if test = 'maps.paidStatus != null    '> " +
+            "and o.paid =#{maps.paidStatus} " +
+            "</if>" +
+            "<if test = 'maps.payTimeList != null    '> " +
+            " AND date_format(o.pay_time,'%y%m%d') &gt;= date_format(#{maps.payTimeList[0]},'%y%m%d') " +
+            " AND date_format(o.pay_time,'%y%m%d') &lt;= date_format(#{maps.payTimeList[1]},'%y%m%d') " +
+            "</if>" +
             "<if test = 'maps.deliveryImportTimeList != null    '> " +
             " AND date_format(o.delivery_import_time,'%y%m%d') &gt;= date_format(#{maps.deliveryImportTimeList[0]},'%y%m%d') " +
             " AND date_format(o.delivery_import_time,'%y%m%d') &lt;= date_format(#{maps.deliveryImportTimeList[1]},'%y%m%d') " +
             "</if>" +
-            "<if test = 'maps.isUpload != null and maps.isUpload == 0    '> " +
-            "and o.certificates is null  " +
+            "<if test=\"maps.deptId != null\">" +
+            " AND (cu.dept_id = #{maps.deptId} OR cu.dept_id IN (" +
+            " SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors)" +
+            " ))" +
             "</if>" +
-            "<if test = 'maps.isUpload != null and maps.isUpload == 1    '> " +
-            "and o.certificates is not null " +
+            "<if test=\"maps.erpPhoneNumber != null and maps.erpPhoneNumber != ''\">" +
+            " and o.erp_phone like concat(#{maps.erpPhoneNumber},'%') " +
             "</if>" +
-            "<if test = 'maps.scheduleId != null    '> " +
-            "and o.schedule_id =#{maps.scheduleId} " +
+            "<if test=\"maps.erpAccount != null and maps.erpAccount != '未分拣' and maps.erpAccount != ''\">" +
+            " and df.login_account like #{maps.erpAccount} " +
+            "</if>" +
+            "<if test=\"maps.erpAccount == '未分拣'\">" +
+            " and ( df.login_account is null or df.login_account like '') " +
             "</if>" +
-            "            <if test=\"maps.deliveryPayStatus != null  \">\n" +
-            "                and o.delivery_pay_status =#{maps.deliveryPayStatus}\n" +
-            "            </if>"+
-            "            <if test=\"maps.deliveryStatus != null     \">\n" +
-            "                and o.delivery_status =#{maps.deliveryStatus}\n" +
-            "            </if>"+
-            "           <if test=\"maps.productName != null and  maps.productName !=  '' \">\n" +
-            "                and psps.product_name like concat('%', #{maps.productName}, '%')\n" +
-            "            </if>"+
             "<if test = 'maps.isAudit != null'> " +
-            "and o.is_audit = #{maps.isAudit} \n" +
+            "and o.is_audit = #{maps.isAudit} " +
+            "</if>" +
+            "<if test=\"maps.isCompanyOrder != null and maps.isCompanyOrder > 0\">" +
+            " and o.order_type != 3 and o.order_type != 2" +
             "</if>" +
-            "            <if test=\"maps.isCompanyOrder != null and maps.isCompanyOrder > 0\">\n" +
-            "                and o.order_type != 3 and o.order_type != 2\n" +
-            "            </if>"+
+            "<if test=\"maps.companyUserIds != null  and maps.companyUserIds.size > 0\">" +
+            " and o.company_user_id in" +
+            " <foreach collection=\"maps.companyUserIds\" item=\"companyUserId\" open=\"(\" close=\")\" separator=\",\">" +
+            " #{companyUserId}" +
+            " </foreach>" +
+            "</if>" +
+            " ${maps.params.dataScope} " +
             "GROUP BY  o.id order by o.id desc limit 50000"+
             "</script>"})
     List<FsStoreOrderItemExportVO> selectFsStoreOrderItemListExportVO(@Param("maps")FsStoreOrderParam fsStoreOrder);
@@ -204,67 +242,78 @@ public interface FsStoreOrderItemScrmMapper
             "left join company c on c.company_id=o.company_id " +
             "left join company_user cu on cu.user_id=o.company_user_id " +
             "left join company_tcm_schedule cts on cts.id = o.schedule_id " +
-            "LEFT JOIN fs_store_order_df df on df.order_id=o.id\n" +
-            "        <if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">\n" +
-            "            LEFT JOIN (\n" +
-            "            SELECT\n" +
-            "            sp.*,\n" +
-            "            ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn\n" +
-            "            FROM fs_store_payment_scrm sp\n" +
-            "            WHERE sp.business_code IS NOT NULL\n" +
-            "            ) sp_latest ON sp_latest.business_code = o.order_code AND sp_latest.rn = 1\n" +
-                        "<if test=\"maps.appId != null and maps.appId != ''\">" +
-            "            LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id\n" +
-                        "</if>" +
-            "        </if>" +
+            " left join fs_store_product_scrm psps on i.product_id=psps.product_id " +
+            " LEFT JOIN (" +
+            " SELECT sp.*, ROW_NUMBER() OVER (PARTITION BY sp.business_code ORDER BY sp.create_time DESC) as rn" +
+            " FROM fs_store_payment_scrm sp WHERE sp.business_code IS NOT NULL" +
+            " ) sp_latest ON sp_latest.order_id = o.id AND sp_latest.rn = 1" +
+            " LEFT JOIN fs_course_play_source_config csc ON csc.appid = sp_latest.app_id" +
+            " <if test=\"maps.erpAccount != null and maps.erpAccount != ''\">" +
+            " LEFT JOIN fs_store_order_df df on df.order_id=o.id " +
+            " </if>" +
             "where 1=1 " +
-            "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
-            "and sp_latest.bank_transaction_id = #{maps.bankTransactionId}\n" +
-            "</if>" +
-            "<if test=\"maps.appId != null and maps.appId != ''\">\n" +
-            "   and csc.appid = #{maps.appId}\n" +
-            " </if>\n" +
-            "            <if test=\"maps.orderCodes != null  and maps.orderCodes.size > 0\">\n" +
-            "                and o.order_code in\n" +
-            "                <foreach collection=\"maps.orderCodes\" item=\"orderCode\" open=\"(\" close=\")\" separator=\",\">\n" +
-            "                    #{orderCode}\n" +
-            "                </foreach>\n" +
-            "            </if>" +
+            "<if test=\"maps.appId != null and maps.appId != ''\">" +
+            " and csc.appid = #{maps.appId} " +
+            "</if>" +
+            "<if test=\"maps.orderCodes != null  and maps.orderCodes.size > 0\">" +
+            " and o.order_code in" +
+            " <foreach collection=\"maps.orderCodes\" item=\"orderCode\" open=\"(\" close=\")\" separator=\",\">" +
+            "     #{orderCode}" +
+            " </foreach>" +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +
-            "<if test = 'maps.deliveryId != null    '> " +
-            "and o.delivery_id =#{maps.deliveryId} " +
+            "<if test=\"maps.bankTransactionId != null and  maps.bankTransactionId !=''\">" +
+            " and sp_latest.bank_transaction_id like CONCAT('%',#{maps.bankTransactionId},'%') " +
+            "</if>" +
+            "<if test=\"maps.isPayRemain != null\">" +
+            " and o.is_pay_remain =#{maps.isPayRemain} " +
+            "</if>" +
+            "<if test=\"maps.userId != null\">" +
+            " and o.user_id =#{maps.userId} " +
+            "</if>" +
+            "<if test=\"maps.deliveryId != null and  maps.deliveryId !=''\">" +
+            " and o.delivery_id =#{maps.deliveryId} " +
             "</if>" +
             "<if test = 'maps.nickname != null and  maps.nickname !=\"\"     '> " +
             "and u.nickname like CONCAT('%',#{maps.nickname},'%') " +
             "</if>" +
+            "<if test=\"maps.realName != null and  maps.realName !=''\">" +
+            " and o.real_name like CONCAT('%',#{maps.realName},'%') " +
+            "</if>" +
             "<if test = 'maps.phone != null and  maps.phone !=\"\"     '> " +
             "and u.phone like CONCAT('%',#{maps.phone},'%') " +
             "</if>" +
-            "<if test = 'maps.realName != null and  maps.realName !=\"\"     '> " +
-            "and o.real_name like CONCAT('%',#{maps.realName},'%') " +
-            "</if>" +
             "<if test = 'maps.userPhone != null and  maps.userPhone !=\"\"     '> " +
             "and o.user_phone like CONCAT('%',#{maps.userPhone},'%') " +
             "</if>" +
-            "<if test=\"maps.status != null and maps.status != 6\">\n" +
-            "                and o.status = #{maps.status}\n" +
-            "            </if>\n" +
-            "            <if test=\"maps.status == 6\">\n" +
-            "                and o.`status`= 1\n" +
-            "\n" +
-            "                and  (o.extend_order_id is null or  o.extend_order_id like '')\n" +
-            "            </if>" +
-
+            "<if test = 'maps.status != null and maps.status != 6    '> " +
+            "and o.status =#{maps.status} " +
+            "</if>" +
+            "<if test = 'maps.status != null and maps.status == 6    '> " +
+            "and o.`status`= 1 and (o.extend_order_id is null or o.extend_order_id like '') " +
+            "</if>" +
+            "<if test = 'maps.isUpload != null and maps.isUpload == 0    '> " +
+            "and o.certificates is null  " +
+            "</if>" +
+            "<if test = 'maps.isUpload != null and maps.isUpload == 1    '> " +
+            "and o.certificates is not null " +
+            "</if>" +
+            "<if test=\"maps.deliveryStatus != null     \">" +
+            " and o.delivery_status =#{maps.deliveryStatus}" +
+            "</if>" +
+            "<if test=\"maps.deliveryPayStatus != null  \">" +
+            " and o.delivery_pay_status =#{maps.deliveryPayStatus}" +
+            "</if>" +
             "<if test = 'maps.companyId != null    '> " +
             "and o.company_id =#{maps.companyId} " +
             "</if>" +
             "<if test = 'maps.isHealth != null and maps.isHealth !=  \"\"  '> " +
-            "and o.company_id is null " +
+            " and (o.company_id is null or o.order_type = 2 or o.order_type = 3)" +
             "</if>" +
-            "<if test = 'maps.notHealth != null and maps.notHealth !=  \"\"  '> " +
-            "and o.company_id is not null " +
+            "<if test = 'maps.notHealth != null  '> " +
+            "and o.company_id is not null and o.order_type = 0 " +
             "</if>" +
             "<if test = 'maps.companyUserId != null    '> " +
             "and o.company_user_id =#{maps.companyUserId} " +
@@ -272,47 +321,70 @@ public interface FsStoreOrderItemScrmMapper
             "<if test = 'maps.companyUserNickName != null and  maps.companyUserNickName !=  \"\" '> " +
             "and cu.nick_name like concat('%', #{maps.companyUserNickName}, '%') " +
             "</if>" +
+            "<if test=\"maps.payCode != null and maps.payCode != ''\">" +
+            " and sp_latest.pay_code like CONCAT('%', #{maps.payCode}, '%') " +
+            "</if>" +
+            "<if test=\"maps.productName != null and  maps.productName !=  '' \">" +
+            " and psps.product_name like concat('%', #{maps.productName}, '%') " +
+            "</if>" +
             "<if test = 'maps.orderType != null and maps.orderType != -1    '> " +
             "and o.order_type =#{maps.orderType} " +
             "</if>" +
             "<if test = 'maps.orderType != null and maps.orderType == -1    '> " +
             "and o.order_type in (2, 3) " +
             "</if>" +
+            "<if test = 'maps.payType != null    '> " +
+            "and o.pay_type =#{maps.payType} " +
+            "</if>" +
+            "<if test = 'maps.scheduleId != null    '> " +
+            "and o.schedule_id =#{maps.scheduleId} " +
+            "</if>" +
             "<if test = 'maps.createTimeList != null    '> " +
             " AND date_format(o.create_time,'%y%m%d') &gt;= date_format(#{maps.createTimeList[0]},'%y%m%d') " +
             " AND date_format(o.create_time,'%y%m%d') &lt;= date_format(#{maps.createTimeList[1]},'%y%m%d') " +
             "</if>" +
-            "<if test = 'maps.payTimeList != null    '> " +
-            " AND date_format(o.pay_time,'%y%m%d') &gt;= date_format(#{maps.payTimeList[0]},'%y%m%d') " +
-            " AND date_format(o.pay_time,'%y%m%d') &lt;= date_format(#{maps.payTimeList[1]},'%y%m%d') " +
-            "</if>" +
             "<if test = 'maps.deliverySendTimeList != null    '> " +
             " AND date_format(o.delivery_send_time,'%y%m%d') &gt;= date_format(#{maps.deliverySendTimeList[0]},'%y%m%d') " +
             " AND date_format(o.delivery_send_time,'%y%m%d') &lt;= date_format(#{maps.deliverySendTimeList[1]},'%y%m%d') " +
             "</if>" +
+            "<if test = 'maps.paidStatus != null    '> " +
+            "and o.paid =#{maps.paidStatus} " +
+            "</if>" +
+            "<if test = 'maps.payTimeList != null    '> " +
+            " AND date_format(o.pay_time,'%y%m%d') &gt;= date_format(#{maps.payTimeList[0]},'%y%m%d') " +
+            " AND date_format(o.pay_time,'%y%m%d') &lt;= date_format(#{maps.payTimeList[1]},'%y%m%d') " +
+            "</if>" +
             "<if test = 'maps.deliveryImportTimeList != null    '> " +
             " AND date_format(o.delivery_import_time,'%y%m%d') &gt;= date_format(#{maps.deliveryImportTimeList[0]},'%y%m%d') " +
             " AND date_format(o.delivery_import_time,'%y%m%d') &lt;= date_format(#{maps.deliveryImportTimeList[1]},'%y%m%d') " +
             "</if>" +
-            "<if test = 'maps.isUpload != null and maps.isUpload == 0    '> " +
-            "and o.certificates is null  " +
+            "<if test=\"maps.deptId != null\">" +
+            " AND (cu.dept_id = #{maps.deptId} OR cu.dept_id IN (" +
+            " SELECT t.dept_id FROM company_dept t WHERE find_in_set(#{maps.deptId}, ancestors)" +
+            " ))" +
             "</if>" +
-            "<if test = 'maps.isUpload != null and maps.isUpload == 1    '> " +
-            "and o.certificates is not null " +
+            "<if test=\"maps.erpPhoneNumber != null and maps.erpPhoneNumber != ''\">" +
+            " and o.erp_phone like concat(#{maps.erpPhoneNumber},'%') " +
             "</if>" +
-            "<if test = 'maps.scheduleId != null    '> " +
-            "and o.schedule_id =#{maps.scheduleId} " +
+            "<if test=\"maps.erpAccount != null and maps.erpAccount != '未分拣' and maps.erpAccount != ''\">" +
+            " and df.login_account like #{maps.erpAccount} " +
+            "</if>" +
+            "<if test=\"maps.erpAccount == '未分拣'\">" +
+            " and ( df.login_account is null or df.login_account like '') " +
+            "</if>" +
+            "<if test = 'maps.isAudit != null'> " +
+            "and o.is_audit = #{maps.isAudit} " +
+            "</if>" +
+            "<if test=\"maps.isCompanyOrder != null and maps.isCompanyOrder > 0\">" +
+            " and o.order_type != 3 and o.order_type != 2" +
+            "</if>" +
+            "<if test=\"maps.companyUserIds != null  and maps.companyUserIds.size > 0\">" +
+            " and o.company_user_id in" +
+            " <foreach collection=\"maps.companyUserIds\" item=\"companyUserId\" open=\"(\" close=\")\" separator=\",\">" +
+            " #{companyUserId}" +
+            " </foreach>" +
             "</if>" +
-            "<if test=\"maps.erpPhoneNumber != null and maps.erpPhoneNumber != ''\">\n" +
-            "                and o.erp_phone like concat(#{maps.erpPhoneNumber},'%')\n" +
-            "            </if>\n" +
-            "            <if test=\"maps.erpAccount != null and maps.erpAccount != '未分拣' and maps.erpAccount != ''\">\n" +
-            "                and df.login_account like #{maps.erpAccount}\n" +
-            "            </if>\n" +
-            "            <if test=\"maps.erpAccount == '未分拣'\">\n" +
-            "                and ( df.login_account is null or df.login_account like '')\n" +
-            "            </if>" +
-            " order by o.id desc "+
+            " ${maps.params.dataScope} " +
             "</script>"})
     Long itemsCount(@Param("maps")FsStoreOrderParam fsStoreOrder);
 

+ 28 - 7
fs-service/src/main/java/com/fs/hisStore/mapper/FsStoreOrderScrmMapper.java

@@ -493,6 +493,12 @@ public interface FsStoreOrderScrmMapper
     List<FsMyStoreOrderListQueryVO> selectFsMyStoreOrderListVO(@Param("maps")FsMyStoreOrderQueryParam param);
     @Select("select * from fs_store_order_scrm where order_code=#{orderCode}")
     FsStoreOrderScrm selectFsStoreOrderByOrderCode(String orderCode);
+
+    /**
+     * 仅更新聚水潭外部订单明细ID
+     */
+    @Update("UPDATE fs_store_order_scrm SET outer_oi_id = #{outerOiId} WHERE order_code = #{orderCode}")
+    int updateOuterOiIdByOrderCode(@Param("orderCode") String orderCode, @Param("outerOiId") String outerOiId);
     @Update("update fs_store_order_scrm set status=-3 where id=#{orderId}")
     int cancelOrder(Long orderId);
     @Select({"<script> " +
@@ -677,6 +683,12 @@ public interface FsStoreOrderScrmMapper
             "<if test=\"maps.bankTransactionId !=null and maps.bankTransactionId!=''\">" +
             " and sp_latest.bank_transaction_id = #{maps.bankTransactionId} " +
             "</if>" +
+            "<if test=\"maps.orderCodes != null  and maps.orderCodes.size > 0\">" +
+            " and o.order_code in" +
+            " <foreach collection=\"maps.orderCodes\" item=\"orderCode\" open=\"(\" close=\")\" separator=\",\">" +
+            "     #{orderCode}" +
+            " </foreach>" +
+            "</if>" +
             "<if test = 'maps.orderCode != null and  maps.orderCode !=\"\"    '> " +
             "and o.order_code like CONCAT('%',#{maps.orderCode},'%') " +
             "</if>" +
@@ -698,9 +710,12 @@ public interface FsStoreOrderScrmMapper
             "<if test = 'maps.userPhone != null and  maps.userPhone !=\"\"     '> " +
             "and o.user_phone like CONCAT('%',#{maps.userPhone},'%') " +
             "</if>" +
-            "<if test = 'maps.status != null    '> " +
+            "<if test = 'maps.status != null and maps.status != 6    '> " +
             "and o.status =#{maps.status} " +
             "</if>" +
+            "<if test = 'maps.status != null and maps.status == 6    '> " +
+            "and o.`status`= 1 and (o.extend_order_id is null or o.extend_order_id like '') " +
+            "</if>" +
             "<if test = 'maps.deliveryStatus != null    '> " +
             "and o.delivery_status =#{maps.deliveryStatus} " +
             "</if>" +
@@ -717,10 +732,10 @@ public interface FsStoreOrderScrmMapper
             "and sp_latest.pay_code like CONCAT('%', #{maps.payCode}, '%') " +
             "</if>" +
             "<if test = 'maps.isHealth != null and maps.isHealth !=  \"\"  '> " +
-            "and o.company_id is null " +
+            "and (o.company_id is null or o.order_type = 2 or o.order_type = 3) " +
             "</if>" +
             "<if test = 'maps.notHealth != null and maps.notHealth !=  \"\"  '> " +
-            "and o.company_id is not null " +
+            "and o.company_id is not null and o.order_type = 0 " +
             "</if>" +
             "<if test = 'maps.companyUserId != null    '> " +
             "and o.company_user_id =#{maps.companyUserId} " +
@@ -731,9 +746,12 @@ public interface FsStoreOrderScrmMapper
             "<if test = 'maps.productName != null and maps.productName != \"\" '> " +
             "and EXISTS (select 1 from fs_store_order_item_scrm oi2 join fs_store_product_scrm fsp2 on oi2.product_id = fsp2.product_id where oi2.order_id = o.id and fsp2.product_name like CONCAT('%', #{maps.productName}, '%')) " +
             "</if>" +
-            "<if test = 'maps.orderType != null    '> " +
+            "<if test = 'maps.orderType != null and maps.orderType != -1    '> " +
             "and o.order_type =#{maps.orderType} " +
             "</if>" +
+            "<if test = 'maps.orderType != null and maps.orderType == -1    '> " +
+            "and o.order_type in (2, 3) " +
+            "</if>" +
             "<if test = 'maps.payType != null    '> " +
             "and o.pay_type =#{maps.payType} " +
             "</if>" +
@@ -1361,10 +1379,10 @@ public interface FsStoreOrderScrmMapper
             "and sp_latest.pay_code like CONCAT('%', #{maps.payCode}, '%') " +
             "</if>" +
             "<if test = 'maps.isHealth != null and maps.isHealth !=  \"\"  '> " +
-            "and o.company_id is null " +
+            "and (o.company_id is null or o.order_type = 2 or o.order_type = 3) " +
             "</if>" +
             "<if test = 'maps.notHealth != null and maps.notHealth !=  \"\"  '> " +
-            "and o.company_id is not null " +
+            "and o.company_id is not null and o.order_type = 0 " +
             "</if>" +
             "<if test = 'maps.companyUserId != null    '> " +
             "and o.company_user_id =#{maps.companyUserId} " +
@@ -1375,9 +1393,12 @@ public interface FsStoreOrderScrmMapper
             "<if test = 'maps.productName != null and maps.productName != \"\" '> " +
             "and EXISTS (select 1 from fs_store_order_item_scrm oi2 join fs_store_product_scrm fsp2 on oi2.product_id = fsp2.product_id where oi2.order_id = o.id and fsp2.product_name like CONCAT('%', #{maps.productName}, '%')) " +
             "</if>" +
-            "<if test = 'maps.orderType != null    '> " +
+            "<if test = 'maps.orderType != null and maps.orderType != -1    '> " +
             "and o.order_type =#{maps.orderType} " +
             "</if>" +
+            "<if test = 'maps.orderType != null and maps.orderType == -1    '> " +
+            "and o.order_type in (2, 3) " +
+            "</if>" +
             "<if test = 'maps.payType != null    '> " +
             "and o.pay_type =#{maps.payType} " +
             "</if>" +

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

@@ -281,6 +281,8 @@ public class FsStoreProductAddEditParam implements Serializable
     /** 限购数量 */
     private Integer purchaseLimit;
 
+    /** 单次购买数量上限(0 表示不限制) */
+    private Integer singlePurchaseLimit;
 
     /** 原产地 */
     @Excel(name = "原产地")

+ 36 - 0
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreCartScrmServiceImpl.java

@@ -163,6 +163,7 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
     public R addCart(long uid, FsStoreCartParam cartParam) {
         // 检查并调整限购数量
         Integer adjustedNum = adjustPurchaseLimit(uid, cartParam.getProductId(), cartParam.getCartNum());
+        adjustedNum = capBySinglePurchaseLimit(cartParam.getProductId(), adjustedNum);
         cartParam.setCartNum(adjustedNum);
         
         //如果是直接购买,直接写入记录
@@ -213,6 +214,7 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
                 int newCartNum = cartParam.getCartNum() + cart.get(0).getCartNum();
                 // 检查并调整限购数量(需要检查新的总数量)
                 Integer adjustedNewNum = adjustPurchaseLimit(uid, cartParam.getProductId(), newCartNum);
+                adjustedNewNum = capBySinglePurchaseLimit(cartParam.getProductId(), adjustedNewNum);
                 storeCart.setCartNum(adjustedNewNum);
                 storeCart.setUpdateTime(new Date());
                 checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId());
@@ -239,6 +241,8 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
         FsStoreCartScrm cart=fsStoreCartMapper.selectFsStoreCartById(cartParam.getId());
         // 检查限购
         checkPurchaseLimit(userId, cart.getProductId(), cartParam.getNumber());
+        FsStoreProductScrm productForSingle = productService.selectFsStoreProductById(cart.getProductId());
+        checkSinglePurchaseLimit(productForSingle, cartParam.getNumber());
         checkProductStock(cart.getProductId(),cart.getProductAttrValueId());
         cart.setCartNum(cartParam.getNumber());
         cart.setUpdateTime(new Date());
@@ -252,6 +256,36 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
      * @param productId 商品ID
      * @param num 要购买的数量
      */
+    /**
+     * 单次购买数量上限:购物车单行数量不得超过该上限(0 或空表示不限制)
+     */
+    private void checkSinglePurchaseLimit(FsStoreProductScrm product, Integer num) {
+        if (product == null || num == null) {
+            return;
+        }
+        if (product.getSinglePurchaseLimit() != null && product.getSinglePurchaseLimit() > 0
+                && num > product.getSinglePurchaseLimit()) {
+            throw new CustomException("该商品单次最多购买" + product.getSinglePurchaseLimit() + "件");
+        }
+    }
+
+    /**
+     * 将数量限制在单次购买上限内(用于加购时自动 cap,避免超过上限仍入库)
+     */
+    private Integer capBySinglePurchaseLimit(Long productId, Integer num) {
+        if (num == null) {
+            return null;
+        }
+        FsStoreProductScrm product = productService.selectFsStoreProductById(productId);
+        if (product == null) {
+            return num;
+        }
+        if (product.getSinglePurchaseLimit() == null || product.getSinglePurchaseLimit() <= 0) {
+            return num;
+        }
+        return Math.min(num, product.getSinglePurchaseLimit());
+    }
+
     private void checkPurchaseLimit(Long userId, Long productId, Integer num) {
         // 查询商品信息
         FsStoreProductScrm product = productService.selectFsStoreProductById(productId);
@@ -432,6 +466,7 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
     public R addCartBySidebar(Long uid, FsStoreCartParam cartParam) {
         // 检查并调整限购数量
         Integer adjustedNum = adjustPurchaseLimit(uid, cartParam.getProductId(), cartParam.getCartNum());
+        adjustedNum = capBySinglePurchaseLimit(cartParam.getProductId(), adjustedNum);
         cartParam.setCartNum(adjustedNum);
 
         //如果是直接购买,直接写入记录
@@ -482,6 +517,7 @@ public class FsStoreCartScrmServiceImpl implements IFsStoreCartScrmService
                 int newCartNum = cartParam.getCartNum() + cart.get(0).getCartNum();
                 // 检查并调整限购数量(需要检查新的总数量)
                 Integer adjustedNewNum = adjustPurchaseLimit(uid, cartParam.getProductId(), newCartNum);
+                adjustedNewNum = capBySinglePurchaseLimit(cartParam.getProductId(), adjustedNewNum);
                 storeCart.setCartNum(adjustedNewNum);
                 storeCart.setUpdateTime(new Date());
                 checkProductStock(cartParam.getProductId(),storeCart.getProductAttrValueId());

+ 79 - 2
fs-service/src/main/java/com/fs/hisStore/service/impl/FsStoreOrderScrmServiceImpl.java

@@ -796,6 +796,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             throw new CustomException("订单已过期", 501);
         }
         List<FsStoreCartQueryVO> carts = redisCache.getCacheObject("orderCarts:" + param.getOrderKey());
+        validateSinglePurchaseLimitForCarts(carts);
         BigDecimal payPrice = getOrderSumPrice(carts, "truePrice");
         if (StringUtils.isNotEmpty(param.getCreateOrderKey())) {
             Integer payType = redisCache.getCacheObject("createOrderPayType:" + param.getCreateOrderKey());
@@ -1478,11 +1479,59 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
         }
     }
 
+    /**
+     * OMS 侧常见承运商编码(与快递 100 等标准码一致)。按长度降序,避免 YD 抢占 YZPY 等长码前缀。
+     * 如 ZTO1、ZTO1.1 归一为 ZTO 再查库;库中 oms_code 须为标准码(如 ZTO),不能仅 ZTO1。
+     */
+    private static final String[] OMS_EXPRESS_KNOWN_CODES_LONGEST_FIRST = {
+            "HTKY", "YZPY", "JTSD", "ZTO", "STO", "YTO", "EMS", "DBL", "ZYE", "ZJS",
+            "SF", "YD", "JD", "UC"
+    };
+
+    /**
+     * 先用传入的 deliverCode(含 trim、大写)精确查 OMS;若无记录,再按 {@link #OMS_EXPRESS_KNOWN_CODES_LONGEST_FIRST}
+     * 做前缀/等于模糊匹配(如 ZTO1.1 → ZTO),用标准码查库后拷贝一条 express,{@code omsCode} 保留为传入的原始编码,{@code code} 等为库中标准数据。
+     */
+    private FsExpressScrm selectExpressByOmsDeliverCode(String deliverCode) {
+        if (StringUtils.isEmpty(deliverCode)) {
+            return null;
+        }
+        String raw = deliverCode.trim();
+        FsExpressScrm express = expressService.selectFsExpressByOmsCode(raw);
+        if (express != null) {
+            return express;
+        }
+        String upper = raw.toUpperCase(Locale.ROOT);
+        if (!upper.equals(raw)) {
+            express = expressService.selectFsExpressByOmsCode(upper);
+            if (express != null) {
+                return express;
+            }
+        }
+        for (String standardOms : OMS_EXPRESS_KNOWN_CODES_LONGEST_FIRST) {
+            if (upper.equals(standardOms) || upper.startsWith(standardOms)) {
+                FsExpressScrm base = expressService.selectFsExpressByCode(standardOms);
+                if (base != null) {
+                    FsExpressScrm assembled = new FsExpressScrm();
+                    BeanUtils.copyProperties(base, assembled);
+                    assembled.setOmsCode(raw);
+                    return assembled;
+                }
+            }
+        }
+        return null;
+    }
+
     @Override
     public void deliveryOrder(String orderCode, String deliveryId, String deliverCode, String deliverName) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderByOrderCode(orderCode);
         if (order != null && order.getStatus() == OrderInfoEnum.STATUS_1.getValue()) {
-            FsExpressScrm express = expressService.selectFsExpressByOmsCode(deliverCode);
+            FsExpressScrm express = selectExpressByOmsDeliverCode(deliverCode);
+            if (express == null) {
+                // 这里输出订单号 还有相关的物流信息,
+                log.error("发货失败:未找到快递公司,订单号:{},deliveryId:{},deliverCode:{},deliverName:{}", orderCode, deliveryId, deliverCode, deliverName);
+                return;
+            }
             if (express != null) {
                 order.setDeliveryName(deliverName);
                 order.setDeliverySn(express.getCode());
@@ -1537,7 +1586,7 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     public void updateDeliveryOrder(Long id, String deliveryId, String deliverCode, String deliverName) {
         FsStoreOrderScrm order = fsStoreOrderMapper.selectFsStoreOrderById(id);
         if (order != null) {
-            FsExpressScrm express = expressService.selectFsExpressByOmsCode(deliverCode);
+            FsExpressScrm express = selectExpressByOmsDeliverCode(deliverCode);
             if (express != null) {
                 order.setDeliveryName(deliverName);
                 order.setDeliverySn(express.getCode());
@@ -3325,6 +3374,29 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
     /**
      * 退回库存
      */
+    /**
+     * 订单计算/提交:校验每个购物车行的数量不超过商品「单次购买上限」
+     */
+    private void validateSinglePurchaseLimitForCarts(List<FsStoreCartQueryVO> carts) {
+        if (carts == null || carts.isEmpty()) {
+            return;
+        }
+        for (FsStoreCartQueryVO c : carts) {
+            if (c.getProductId() == null || c.getCartNum() == null) {
+                continue;
+            }
+            FsStoreProductScrm product = productService.selectFsStoreProductById(c.getProductId());
+            if (product == null) {
+                continue;
+            }
+            if (product.getSinglePurchaseLimit() != null && product.getSinglePurchaseLimit() > 0
+                    && c.getCartNum() > product.getSinglePurchaseLimit()) {
+                throw new CustomException("商品「" + product.getProductName() + "」单次最多购买"
+                        + product.getSinglePurchaseLimit() + "件");
+            }
+        }
+    }
+
     /**
      * 检查并记录限购
      * @param userId 用户ID
@@ -3338,6 +3410,11 @@ public class FsStoreOrderScrmServiceImpl implements IFsStoreOrderScrmService {
             return;
         }
 
+        if (product.getSinglePurchaseLimit() != null && product.getSinglePurchaseLimit() > 0
+                && num != null && num > product.getSinglePurchaseLimit()) {
+            throw new CustomException("该商品单次最多购买" + product.getSinglePurchaseLimit() + "件");
+        }
+
         // 如果商品没有设置限购,直接返回
         if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
             return;

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

@@ -699,6 +699,12 @@ public class FsStoreProductScrmServiceImpl implements IFsStoreProductScrmService
         } else {
             product.setPurchaseLimit(0);
         }
+        // 单次购买上限:与限购字段含义一致,0 表示不限制
+        if (param.getSinglePurchaseLimit() != null && param.getSinglePurchaseLimit() > 0) {
+            product.setSinglePurchaseLimit(param.getSinglePurchaseLimit());
+        } else {
+            product.setSinglePurchaseLimit(0);
+        }
         //校验店铺资质信息
         if (!CompanyEnum.contains(cloudHostProper.getCompanyName())) {
             //获取店铺

+ 3 - 0
fs-service/src/main/java/com/fs/hisStore/vo/FsStoreProductQueryVO.java

@@ -142,4 +142,7 @@ public class FsStoreProductQueryVO implements Serializable
     /** 限购数量 */
     private Integer purchaseLimit;
 
+    /** 单次购买数量上限 */
+    private Integer singlePurchaseLimit;
+
 }

+ 14 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveOrderServiceImpl.java

@@ -2285,6 +2285,15 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             log.error("商品不存在");
             return null;
         }
+        if (StringUtils.isEmpty(param.getTotalNum())) {
+            log.error("商品数量不能为空");
+            return null;
+        }
+        int purchaseNum = Integer.parseInt(param.getTotalNum());
+        if (fsStoreProduct.getSinglePurchaseLimit() != null && fsStoreProduct.getSinglePurchaseLimit() > 0
+                && purchaseNum > fsStoreProduct.getSinglePurchaseLimit()) {
+            throw new CustomException("该商品单次最多购买" + fsStoreProduct.getSinglePurchaseLimit() + "件");
+        }
         FsStoreProductAttrValueScrm fsStoreProductAttrValue = null;
         if (!Objects.isNull(param.getAttrValueId())) {
             fsStoreProductAttrValue = attrValueScrmMapper.selectFsStoreProductAttrValueById(param.getAttrValueId());
@@ -4791,6 +4800,11 @@ public class LiveOrderServiceImpl implements ILiveOrderService {
             return;
         }
 
+        if (product.getSinglePurchaseLimit() != null && product.getSinglePurchaseLimit() > 0
+                && num != null && num > product.getSinglePurchaseLimit()) {
+            throw new CustomException("该商品单次最多购买" + product.getSinglePurchaseLimit() + "件");
+        }
+
         // 如果商品没有设置限购,直接返回
         if (product.getPurchaseLimit() == null || product.getPurchaseLimit() <= 0) {
             return;

+ 1 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java

@@ -1607,6 +1607,7 @@ public class LiveServiceImpl implements ILiveService
         try {
             String cacheKey = String.format(LiveKeysConstant.LIVE_DATA_CACHE, liveId);
             redisCache.deleteObject(cacheKey);
+            redisCache.hashDelete(LiveKeysConstant.LIVE_HOME_PAGE_DETAIL, String.valueOf(liveId));
             log.debug("清除直播间缓存: liveId={}", liveId);
             return R.ok("缓存清理成功");
         } catch (Exception e) {

+ 93 - 32
fs-service/src/main/resources/mapper/course/FsCourseWatchLogMapper.xml

@@ -1453,6 +1453,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             COALESCE(ord.order_amount, 0) AS orderAmount,
             c.company_name AS companyName,
             cu.nick_name AS salesName
+            <if test="param.includeCourseRating != null and param.includeCourseRating">
+            , ans.course_rating AS courseRating
+            </if>
         FROM (
             SELECT
                 l.user_id,
@@ -1495,6 +1498,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ) ord ON ord.user_id = ua.user_id
         LEFT JOIN company c ON c.company_id = ua.company_id
         LEFT JOIN company_user cu ON cu.user_id = ua.company_user_id
+        <if test="param.includeCourseRating != null and param.includeCourseRating">
+        LEFT JOIN (
+            SELECT a.user_id, a.question_json AS course_rating
+            FROM fs_course_answer_logs a
+            INNER JOIN (
+                SELECT user_id, MAX(log_id) AS max_log_id
+                FROM fs_course_answer_logs
+                WHERE video_id = #{param.videoId} AND period_id = #{param.periodId}
+                GROUP BY user_id
+            ) latest ON latest.user_id = a.user_id AND latest.max_log_id = a.log_id
+        ) ans ON ans.user_id = ua.user_id
+        </if>
         ORDER BY ua.max_create_time DESC
     </select>
 
@@ -1509,6 +1524,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             COALESCE(ord.order_amount, 0) AS orderAmount,
             c.company_name AS companyName,
             cu.nick_name AS salesName
+            <if test="param.includeCourseRating != null and param.includeCourseRating">
+            , ans.course_rating AS courseRating
+            </if>
         FROM (
             SELECT
                 l.user_id,
@@ -1551,44 +1569,87 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ) ord ON ord.user_id = ua.user_id
         LEFT JOIN company c ON c.company_id = ua.company_id
         LEFT JOIN company_user cu ON cu.user_id = ua.company_user_id
+        <if test="param.includeCourseRating != null and param.includeCourseRating">
+        LEFT JOIN (
+            SELECT a.user_id, a.question_json AS course_rating
+            FROM fs_course_answer_logs a
+            INNER JOIN (
+                SELECT user_id, MAX(log_id) AS max_log_id
+                FROM fs_course_answer_logs
+                WHERE video_id = #{param.videoId} AND period_id = #{param.periodId}
+                GROUP BY user_id
+            ) latest ON latest.user_id = a.user_id AND latest.max_log_id = a.log_id
+        ) ans ON ans.user_id = ua.user_id
+        </if>
         ORDER BY ua.max_create_time DESC
         LIMIT 50000
     </select>
 
+    <!--
+        实际看课数据(按看课记录口径,与 countDistinctWatchUsers / countDistinctCompleteUsers 一致):
+        仅统计 duration > 0 的记录参与时长汇总;到课/时长均以正时长行为准。
+        i.1 实际到课人数 = 去重 user_id,且存在至少一条本视频本营期 duration>0 的看课记录(进入点播产生有效观看)
+        i.2 实际完课人数 = 去重 user_id,且存在至少一条 log_type=2 且 duration>0 的看课记录(完课记录)
+        ii.3 实际完课率 = i.2 / i.1(百分比)
+        iv.4 人均看课时长(分钟) = 到课用户各自的「仅 duration>0 记录时长之和」汇总结秒 / 到课人数
+        v.5 人均完课时长(分钟) = 完课用户各自的同上累计秒数之和 / 完课人数
+        vi.6 人均完课完播率(%) = (v.5 对应的人均秒数) / 素材时长(秒) * 100,即 (完课用户总秒数/完课人数) / video.duration * 100
+    -->
     <select id="selectActualCompletionList" resultType="com.fs.course.vo.FSActualCompletionVO">
         SELECT
-        a.totalStudents,
-        a.completedCount,
-        ROUND((a.completedCount / a.totalStudents) * 100, 2) AS actualCompletionRate,
-        ROUND((a.totalViewingDuration / a.totalStudents), 2) AS avgWatchDurationMinutes,
-        ROUND((a.totalDurationOfCompleters/a.completedCount),2) AS avgCompletedDuration,
-        ROUND(((a.totalDurationOfCompleters/a.completedCount)/a.duration) * 100,2) AS avgCompletionPlaybackRate
-        FROM
-        (
-        SELECT
-        COUNT(*) AS totalStudents,
-        SUM(CASE WHEN wl.log_type = 2 THEN 1 ELSE 0 END) AS completedCount,
-        SUM(wl.duration) AS totalViewingDuration,
-        SUM(CASE WHEN wl.log_type = 2 THEN wl.duration ELSE 0 END) AS totalDurationOfCompleters,
-        cv.duration
-        FROM
-        fs_user_course_period_days pd
-        INNER JOIN fs_course_watch_log wl ON pd.period_id = wl.period_id
-        INNER JOIN fs_user_course_video cv ON pd.video_id = cv.video_id
-        AND pd.video_id = wl.video_id
-        <where>
-            pd.period_id = #{periodId}
-            AND pd.video_id = #{videoId}
-            <if test="companyId != null">
-                AND wl.company_id = #{companyId}
-            </if>
-            <if test="companyUserId != null">
-                AND wl.company_user_id = #{companyUserId}
-            </if>
-        </where>
-        GROUP BY
-        pd.period_id
-        ) a
+            COALESCE(agg.total_students, 0) AS totalStudents,
+            COALESCE(agg.completed_count, 0) AS completedCount,
+            CASE
+                WHEN COALESCE(agg.total_students, 0) > 0 THEN
+                    ROUND((agg.completed_count * 100.0 / agg.total_students), 2)
+                ELSE 0
+            END AS actualCompletionRate,
+            CASE
+                WHEN COALESCE(agg.total_students, 0) > 0 THEN
+                    ROUND((agg.total_viewing_seconds / agg.total_students) , 2)
+                ELSE 0
+            END AS avgWatchDurationMinutes,
+            CASE
+                WHEN COALESCE(agg.completed_count, 0) > 0 THEN
+                    ROUND((agg.total_duration_completers_seconds / agg.completed_count) , 2)
+                ELSE 0
+            END AS avgCompletedDuration,
+            CASE
+                WHEN COALESCE(agg.completed_count, 0) > 0
+                     AND COALESCE(vid.video_duration_sec, 0) > 0 THEN
+                    ROUND(((agg.total_duration_completers_seconds / agg.completed_count) / vid.video_duration_sec) * 100, 2)
+                ELSE 0
+            END AS avgCompletionPlaybackRate
+        FROM (
+            SELECT
+                COUNT(*) AS total_students,
+                COALESCE(SUM(CASE WHEN u.has_complete_log = 1 THEN 1 ELSE 0 END), 0) AS completed_count,
+                COALESCE(SUM(u.sum_pos_duration_sec), 0) AS total_viewing_seconds,
+                COALESCE(SUM(CASE WHEN u.has_complete_log = 1 THEN u.sum_pos_duration_sec ELSE 0 END), 0) AS total_duration_completers_seconds
+            FROM (
+                SELECT
+                    wl.user_id,
+                    SUM(CASE WHEN COALESCE(wl.duration, 0) > 0 THEN COALESCE(wl.duration, 0) ELSE 0 END) AS sum_pos_duration_sec,
+                    MAX(CASE WHEN wl.log_type = 2 AND COALESCE(wl.duration, 0) > 0 THEN 1 ELSE 0 END) AS has_complete_log
+                FROM fs_course_watch_log wl
+                WHERE wl.video_id = #{videoId}
+                  AND wl.period_id = #{periodId}
+                  AND wl.user_id IS NOT NULL
+                <if test="companyId != null">
+                    AND wl.company_id = #{companyId}
+                </if>
+                <if test="companyUserId != null">
+                    AND wl.company_user_id = #{companyUserId}
+                </if>
+                GROUP BY wl.user_id
+                HAVING SUM(CASE WHEN COALESCE(wl.duration, 0) > 0 THEN COALESCE(wl.duration, 0) ELSE 0 END) > 0
+            ) u
+        ) agg
+        CROSS JOIN (
+            SELECT COALESCE(MAX(v.duration), 0) AS video_duration_sec
+            FROM fs_user_course_video v
+            WHERE v.video_id = #{videoId} AND v.is_del = 0
+        ) vid
     </select>
 
     <!-- 记录类型 1看课中 2完课 3待看课 4看课中断   -->

+ 2 - 1
fs-service/src/main/resources/mapper/hisStore/FsStoreOrderScrmMapper.xml

@@ -7,6 +7,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <resultMap type="FsStoreOrderScrm" id="FsStoreOrderResult">
         <result property="id"    column="id"    />
         <result property="orderCode"    column="order_code"    />
+        <result property="outerOiId"    column="outer_oi_id"    />
         <result property="extendOrderId"    column="extend_order_id"    />
         <result property="payOrderId"    column="pay_order_id"    />
         <result property="bankOrderId"    column="bank_order_id"    />
@@ -94,7 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectFsStoreOrderVo">
-        select id, order_code,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,backend_edit_product_type,video_id,course_id,project_id,period_id,virtual_phone from fs_store_order_scrm
+        select id, order_code,outer_oi_id,service_fee, extend_order_id,pay_order_id,bank_order_id, user_id,order_visit, real_name, user_phone, user_address, cart_id, freight_price, total_num, total_price, total_postage, pay_price, pay_postage,pay_delivery,pay_money, deduction_price, coupon_id, coupon_price, paid, pay_time, pay_type, create_time, update_time, status, refund_status, refund_reason_wap_img, refund_reason_wap_explain, refund_reason_time, refund_reason_wap, refund_reason, refund_price, delivery_sn, delivery_name, delivery_type, delivery_id, gain_integral, use_integral, pay_integral, back_integral, mark, is_del, remark, cost, verify_code, store_id, shipping_type, is_channel, is_remind, is_sys_del,is_prescribe,prescribe_id ,company_id,company_user_id,is_package,package_json,item_json,order_type,package_id,finish_time,delivery_status,delivery_pay_status,delivery_time,delivery_pay_time,delivery_pay_money,tui_money,tui_money_status,delivery_import_time,tui_user_id,tui_user_money_status,order_create_type,store_house_code,dept_id,is_edit_money,customer_id,is_pay_remain,delivery_send_time,certificates,schedule_id,backend_edit_product_type,video_id,course_id,project_id,period_id,virtual_phone from fs_store_order_scrm
     </sql>
 
     <select id="selectFsStoreOrderList" parameterType="FsStoreOrderScrm" resultMap="FsStoreOrderResult">

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

@@ -77,6 +77,7 @@
         <result property="domesticImported"    column="domestic_imported"    />
         <result property="appIds"    column="app_ids"    />
         <result property="purchaseLimit"    column="purchase_limit"    />
+        <result property="singlePurchaseLimit"    column="single_purchase_limit"    />
     </resultMap>
 
     <sql id="selectFsStoreProductVo">
@@ -88,7 +89,7 @@
                is_display,tui_cate_id,company_ids,is_drug,drug_image,drug_reg_cert_no,common_name,dosage_form,
                unit_price,batch_number,mah,mah_address,manufacturer,manufacturer_address,indications,dosage,
                adverse_reactions,contraindications,precautions,is_audit,store_id,return_address,brand,food_production_license_code,
-               origin_place,net_content,shelf_life,domestic_imported,app_ids,purchase_limit
+               origin_place,net_content,shelf_life,domestic_imported,app_ids,purchase_limit,single_purchase_limit
         from fs_store_product_scrm
     </sql>
 
@@ -101,7 +102,7 @@
                p.is_display,p.tui_cate_id,p.company_ids,p.is_drug,p.drug_image,p.drug_reg_cert_no,p.common_name,p.dosage_form,
                p.unit_price,p.batch_number,p.mah,p.mah_address,p.manufacturer,p.manufacturer_address,p.indications,p.dosage,
                p.adverse_reactions,p.contraindications,p.precautions,p.is_audit,p.store_id,p.return_address,p.brand,p.food_production_license_code,
-               p.origin_place,p.net_content,p.shelf_life,p.domestic_imported,app_ids,p.purchase_limit
+               p.origin_place,p.net_content,p.shelf_life,p.domestic_imported,app_ids,p.purchase_limit,p.single_purchase_limit
         from fs_store_product_scrm p
     </sql>
 
@@ -280,6 +281,7 @@
             <if test="domesticImported != null and domesticImported != ''">domestic_imported,</if>
             <if test="appIds != null and appIds != ''">app_ids, </if>
             <if test="purchaseLimit != null">purchase_limit,</if>
+            <if test="singlePurchaseLimit != null">single_purchase_limit,</if>
         </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="image != null and image != ''">#{image},</if>
@@ -353,6 +355,7 @@
             <if test="domesticImported != null and domesticImported != ''">#{domesticImported},</if>
             <if test="appIds != null and appIds != ''">#{appIds}, </if>
             <if test="purchaseLimit != null">#{purchaseLimit},</if>
+            <if test="singlePurchaseLimit != null">#{singlePurchaseLimit},</if>
         </trim>
     </insert>
 
@@ -430,6 +433,7 @@
             <if test="domesticImported != null">domestic_imported = #{domesticImported},</if>
             <if test="appIds != null and appIds != ''">app_ids = #{appIds}, </if>
             <if test="purchaseLimit != null">purchase_limit = #{purchaseLimit},</if>
+            <if test="singlePurchaseLimit != null">single_purchase_limit = #{singlePurchaseLimit},</if>
         </trim>
         where product_id = #{productId}
     </update>

+ 1 - 1
fs-service/src/main/resources/mapper/live/LiveMapper.xml

@@ -112,7 +112,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 
     <select id="selectLiveByLiveId" parameterType="Long" resultMap="LiveResult">
         <include refid="selectLiveVo"/>
-        where live_id = #{liveId}
+        where live_id = #{liveId} and is_del = 0
     </select>
 
     <select id="selectLiveByLiveIdAndCompanyIdAndCompanyUserId" resultMap="LiveResult">