Bladeren bron

直播营期相关代码

yuhongqi 2 dagen geleden
bovenliggende
commit
44460ad74a
34 gewijzigde bestanden met toevoegingen van 1898 en 75 verwijderingen
  1. 89 0
      fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java
  2. 224 0
      fs-admin/src/main/java/com/fs/live/controller/LiveTrainingCampAdminController.java
  3. 12 7
      fs-company/src/main/java/com/fs/company/controller/live/LiveController.java
  4. 58 58
      fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java
  5. 69 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveQuestionLiveController.java
  6. 182 0
      fs-company/src/main/java/com/fs/company/controller/live/LiveTrainingCampController.java
  7. 94 0
      fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java
  8. 30 0
      fs-service/src/main/java/com/fs/course/param/LiveQuizSubmitUParam.java
  9. 6 0
      fs-service/src/main/java/com/fs/course/service/IFsCourseQuestionBankService.java
  10. 82 0
      fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java
  11. 14 1
      fs-service/src/main/java/com/fs/live/domain/Live.java
  12. 22 0
      fs-service/src/main/java/com/fs/live/domain/LiveCourseQuestionRel.java
  13. 37 0
      fs-service/src/main/java/com/fs/live/domain/LiveTrainingCamp.java
  14. 52 0
      fs-service/src/main/java/com/fs/live/domain/LiveTrainingPeriod.java
  15. 28 0
      fs-service/src/main/java/com/fs/live/mapper/LiveCourseQuestionRelMapper.java
  16. 8 0
      fs-service/src/main/java/com/fs/live/mapper/LiveMapper.java
  17. 25 0
      fs-service/src/main/java/com/fs/live/mapper/LiveTrainingCampMapper.java
  18. 27 0
      fs-service/src/main/java/com/fs/live/mapper/LiveTrainingPeriodMapper.java
  19. 18 0
      fs-service/src/main/java/com/fs/live/param/LiveTrainingLiveAuditBody.java
  20. 21 0
      fs-service/src/main/java/com/fs/live/service/ILiveCourseQuestionRelService.java
  21. 6 0
      fs-service/src/main/java/com/fs/live/service/ILiveService.java
  22. 27 0
      fs-service/src/main/java/com/fs/live/service/ILiveTrainingCampService.java
  23. 34 0
      fs-service/src/main/java/com/fs/live/service/ILiveTrainingPeriodService.java
  24. 90 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveCourseQuestionRelServiceImpl.java
  25. 4 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveServiceImpl.java
  26. 88 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveTrainingCampServiceImpl.java
  27. 185 0
      fs-service/src/main/java/com/fs/live/service/impl/LiveTrainingPeriodServiceImpl.java
  28. 34 0
      fs-service/src/main/java/com/fs/live/vo/TrainingLiveAuditVO.java
  29. 12 7
      fs-service/src/main/resources/mapper/company/CompanyWithdrawDetailMapper.xml
  30. 57 0
      fs-service/src/main/resources/mapper/live/LiveCourseQuestionRelMapper.xml
  31. 34 2
      fs-service/src/main/resources/mapper/live/LiveMapper.xml
  32. 83 0
      fs-service/src/main/resources/mapper/live/LiveTrainingCampMapper.xml
  33. 97 0
      fs-service/src/main/resources/mapper/live/LiveTrainingPeriodMapper.xml
  34. 49 0
      fs-user-app/src/main/java/com/fs/app/controller/live/LiveQuizController.java

+ 89 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveQuestionLiveController.java

@@ -0,0 +1,89 @@
+package com.fs.live.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.live.domain.Live;
+import com.fs.live.service.ILiveCourseQuestionRelService;
+import com.fs.live.service.ILiveService;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 总后台:直播间答题配置(关联课程题库),与企业端能力一致,按直播间解析所属企业。
+ */
+@RestController
+@RequestMapping("/live/liveQuestionLive")
+public class LiveQuestionLiveController extends BaseController {
+
+    @Autowired
+    private ILiveCourseQuestionRelService liveCourseQuestionRelService;
+    @Autowired
+    private ILiveService liveService;
+
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam Long liveId) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectLinkedByLiveId(liveId, companyId);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/optionList")
+    public TableDataInfo optionList(@RequestParam Long liveId,
+                                    @RequestParam(required = false) String title,
+                                    @RequestParam(required = false) Integer type) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectOptionQuestionBank(
+                liveId, companyId, title, type);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestParam Long liveId, @RequestParam String questionIds) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return AjaxResult.error("直播间不存在");
+        }
+        int rows = liveCourseQuestionRelService.batchAdd(liveId, companyId, questionIds);
+        return rows > 0 ? AjaxResult.success("添加成功") : AjaxResult.success("无新增(可能已全部存在)");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{liveId}")
+    public AjaxResult remove(@PathVariable Long liveId, @RequestParam String ids) {
+        Long companyId = requireCompanyIdByLiveId(liveId);
+        if (companyId == null) {
+            return AjaxResult.error("直播间不存在");
+        }
+        int rows = liveCourseQuestionRelService.deleteByRelIds(liveId, companyId, ids);
+        return toAjax(rows);
+    }
+
+    private Long requireCompanyIdByLiveId(Long liveId) {
+        if (liveId == null) {
+            return null;
+        }
+        Live live = liveService.selectLiveDbByLiveId(liveId);
+        return live != null ? live.getCompanyId() : null;
+    }
+}

+ 224 - 0
fs-admin/src/main/java/com/fs/live/controller/LiveTrainingCampAdminController.java

@@ -0,0 +1,224 @@
+package com.fs.live.controller;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveTrainingCamp;
+import com.fs.live.domain.LiveTrainingPeriod;
+import com.fs.live.param.LiveTrainingLiveAuditBody;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveTrainingCampService;
+import com.fs.live.service.ILiveTrainingPeriodService;
+import com.fs.live.vo.TrainingLiveAuditVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 总后台:直播训练营 / 营期 / 营期直播间(跨企业)及训练营直播间审核
+ */
+@RestController
+@RequestMapping("/live/trainingCamp/admin")
+public class LiveTrainingCampAdminController extends BaseController {
+
+    @Autowired
+    private ILiveTrainingCampService liveTrainingCampService;
+    @Autowired
+    private ILiveTrainingPeriodService liveTrainingPeriodService;
+    @Autowired
+    private ILiveService liveService;
+    @Autowired
+    private CompanyUserMapper companyUserMapper;
+
+    // ---------- 训练营 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:list')")
+    @GetMapping("/camp/list")
+    public TableDataInfo campList(LiveTrainingCamp query) {
+        startPage();
+        List<LiveTrainingCamp> list = liveTrainingCampService.selectLiveTrainingCampListAdmin(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:query')")
+    @GetMapping("/camp/{campId}")
+    public AjaxResult getCamp(@PathVariable Long campId, @RequestParam(required = false) Long companyId) {
+        LiveTrainingCamp c = liveTrainingCampService.selectLiveTrainingCampByIdForAdmin(campId, companyId);
+        return c != null ? AjaxResult.success(c) : AjaxResult.error("训练营不存在或无权限");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:add')")
+    @Log(title = "总后台直播训练营", businessType = BusinessType.INSERT)
+    @PostMapping("/camp")
+    public AjaxResult addCamp(@RequestBody LiveTrainingCamp camp) {
+        if (camp.getCompanyId() == null) {
+            return AjaxResult.error("请选择企业");
+        }
+        camp.setCreateBy(getUsername());
+        camp.setCreateTime(new java.util.Date());
+        if (camp.getSortOrder() == null) {
+            camp.setSortOrder(0);
+        }
+        if (camp.getStatus() == null) {
+            camp.setStatus(0);
+        }
+        return toAjax(liveTrainingCampService.insertLiveTrainingCamp(camp));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:edit')")
+    @Log(title = "总后台直播训练营", businessType = BusinessType.UPDATE)
+    @PutMapping("/camp")
+    public AjaxResult editCamp(@RequestBody LiveTrainingCamp camp) {
+        if (camp.getCampId() == null || camp.getCompanyId() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        camp.setUpdateBy(getUsername());
+        camp.setUpdateTime(new java.util.Date());
+        return toAjax(liveTrainingCampService.updateLiveTrainingCamp(camp));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:remove')")
+    @Log(title = "总后台直播训练营", businessType = BusinessType.DELETE)
+    @DeleteMapping("/camp/{campIds}")
+    public AjaxResult removeCamp(@PathVariable Long[] campIds, @RequestParam Long companyId) {
+        return toAjax(liveTrainingCampService.deleteLiveTrainingCampByIds(campIds, companyId, null));
+    }
+
+    // ---------- 营期 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:list')")
+    @GetMapping("/period/list")
+    public TableDataInfo periodList(LiveTrainingPeriod query) {
+        startPage();
+        List<LiveTrainingPeriod> list = liveTrainingPeriodService.selectLiveTrainingPeriodListAdmin(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:query')")
+    @GetMapping("/period/{periodId}")
+    public AjaxResult getPeriod(@PathVariable Long periodId, @RequestParam(required = false) Long companyId) {
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodByIdForAdmin(periodId, companyId);
+        if (p == null) {
+            return AjaxResult.error("营期不存在或无权限");
+        }
+        LiveTrainingCamp camp = liveTrainingCampService.selectLiveTrainingCampByIdForAdmin(p.getCampId(), null);
+        if (camp != null) {
+            p.setCompanyId(camp.getCompanyId());
+        }
+        return AjaxResult.success(p);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:add')")
+    @Log(title = "总后台直播营期", businessType = BusinessType.INSERT)
+    @PostMapping("/period")
+    public AjaxResult addPeriod(@RequestBody LiveTrainingPeriod period) {
+        if (period.getCampId() == null) {
+            return AjaxResult.error("请选择训练营");
+        }
+        LiveTrainingCamp camp = liveTrainingCampService.selectLiveTrainingCampByIdForAdmin(period.getCampId(), null);
+        if (camp == null) {
+            return AjaxResult.error("训练营不存在");
+        }
+        period.setCompanyId(camp.getCompanyId());
+        period.setCreateBy(camp.getCreateBy());
+        period.setCreateTime(new java.util.Date());
+        if (period.getSortOrder() == null) {
+            period.setSortOrder(0);
+        }
+        if (period.getStatus() == null) {
+            period.setStatus(0);
+        }
+        return toAjax(liveTrainingPeriodService.insertLiveTrainingPeriod(period));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:edit')")
+    @Log(title = "总后台直播营期", businessType = BusinessType.UPDATE)
+    @PutMapping("/period")
+    public AjaxResult editPeriod(@RequestBody LiveTrainingPeriod period) {
+        if (period.getPeriodId() == null || period.getCompanyId() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        period.setUpdateBy(getUsername());
+        period.setUpdateTime(new java.util.Date());
+        return toAjax(liveTrainingPeriodService.updateLiveTrainingPeriod(period));
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:remove')")
+    @Log(title = "总后台直播营期", businessType = BusinessType.DELETE)
+    @DeleteMapping("/period/{periodIds}")
+    public AjaxResult removePeriod(@PathVariable Long[] periodIds, @RequestParam Long companyId) {
+        return toAjax(liveTrainingPeriodService.deleteLiveTrainingPeriodByIds(periodIds, companyId, null));
+    }
+
+    // ---------- 营期下直播间 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:list')")
+    @GetMapping("/live/list")
+    public TableDataInfo trainingLiveList(Live query, @RequestParam Long companyId) {
+        if (query.getTrainingPeriodId() == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodByIdForAdmin(
+                query.getTrainingPeriodId(), companyId);
+        if (p == null) {
+            return getDataTable(Collections.emptyList());
+        }
+        query.setCompanyId(companyId);
+        List<CompanyUser> users = companyUserMapper.selectCompanyUserByCompanyId(companyId);
+        if (users != null && !users.isEmpty()) {
+            query.setCompanyUserId(users.get(0).getUserId());
+        }
+        startPage();
+        List<Live> list = liveService.selectLiveList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:live:add')")
+    @Log(title = "总后台训练营直播间", businessType = BusinessType.INSERT)
+    @PostMapping("/live")
+    public AjaxResult addTrainingLive(@RequestBody Live live, @RequestParam Long companyId) {
+        if (StringUtils.isEmpty(live.getCreateBy())) {
+            live.setCreateBy(String.valueOf(getUserId()));
+        }
+        return toAjax(liveTrainingPeriodService.insertTrainingLiveForAdmin(live, companyId));
+    }
+
+    // ---------- 训练营直播间审核 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:audit:list')")
+    @GetMapping("/live/auditList")
+    public TableDataInfo auditList(Live query) {
+        startPage();
+        List<TrainingLiveAuditVO> list = liveService.selectTrainingLiveAuditList(query);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCampAdmin:audit:edit')")
+    @Log(title = "训练营直播间审核", businessType = BusinessType.UPDATE)
+    @PutMapping("/live/audit")
+    public AjaxResult audit(@RequestBody LiveTrainingLiveAuditBody body) {
+        if (body == null || body.getLiveId() == null || body.getPassed() == null) {
+            return AjaxResult.error("参数不完整");
+        }
+        Live db = liveService.selectLiveDbByLiveId(body.getLiveId());
+        if (db == null || db.getTrainingPeriodId() == null) {
+            return AjaxResult.error("仅支持训练营直播间审核");
+        }
+        Live upd = new Live();
+        upd.setLiveId(body.getLiveId());
+        upd.setIsAudit(Boolean.TRUE.equals(body.getPassed()) ? 1 : 2);
+        if (StringUtils.isNotEmpty(body.getRemark())) {
+            upd.setRemark(body.getRemark());
+        }
+        return toAjax(liveService.updateLive(upd));
+    }
+}

+ 12 - 7
fs-company/src/main/java/com/fs/company/controller/live/LiveController.java

@@ -89,6 +89,8 @@ public class LiveController extends BaseController
     {
         // 设置企业ID和企业用户ID
         setCompanyId(live);
+        // 普通直播列表不包含「训练营-营期」下的直播间
+        live.setExcludeCampLive(true);
 
         startPage();
         List<Live> list = liveService.selectLiveList(live);
@@ -121,7 +123,7 @@ public class LiveController extends BaseController
     {
         // 设置企业ID和企业用户ID
         setCompanyId(live);
-
+        live.setExcludeCampLive(true);
 
         List<Live> list = liveService.selectLiveList(live);
         ExcelUtil<Live> util = new ExcelUtil<Live>(Live.class);
@@ -202,12 +204,15 @@ public class LiveController extends BaseController
     @PutMapping
     public AjaxResult edit(@RequestBody Live live)
     {
-        return AjaxResult.success();
-//        CompanyUser user = SecurityUtils.getLoginUser().getUser();
-//        live.setCompanyUserId(user.getUserId());
-//        live.setCompanyId(user.getCompanyId());
-//
-//        return toAjax(liveService.updateLive(live));
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        live.setCompanyUserId(user.getUserId());
+        live.setCompanyId(user.getCompanyId());
+        Live db = liveService.selectLiveByLiveIdAndCompanyIdAndCompanyUserId(
+                live.getLiveId(), user.getCompanyId(), user.getUserId());
+        if (db != null && db.getTrainingPeriodId() != null) {
+            live.setIsAudit(0);
+        }
+        return toAjax(liveService.updateLive(live));
     }
 
     /**

+ 58 - 58
fs-company/src/main/java/com/fs/company/controller/live/LiveDataController.java

@@ -49,64 +49,64 @@ public class LiveDataController extends BaseController
     @Autowired
     private TokenService tokenService;
 
-//    /**
-//     * 直播数据统计-数据概览(12项指标)
-//     * @param liveIds 直播间ID列表,前端传入
-//     */
-//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
-//    @PostMapping("/getLiveStatisticsOverview")
-//    public R getLiveStatisticsOverview(@RequestBody List<Long> liveIds) {
-//
-//        return R.ok().put("data",liveDataService.getLiveStatisticsOverview(liveIds != null ? liveIds : Collections.emptyList()));
-//    }
-//
-//    /**
-//     * 直播趋势-进入人数折线图
-//     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
-//     * @param liveIds 直播间ID列表
-//     */
-//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
-//    @PostMapping("/getLiveEntryTrend")
-//    public R getLiveEntryTrend(@RequestBody List<Long> liveIds) {
-//        return R.ok().put("data", liveDataService.getLiveEntryTrend(liveIds != null ? liveIds : Collections.emptyList()));
-//    }
-//
-//    /**
-//     * 直播间学员列表(分页,基于 live_user_first_entry)
-//     * 筛选:直播名称(liveIds)、首次访问时间范围
-//     */
-//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
-//    @PostMapping("/listLiveRoomStudents")
-//    public R listLiveRoomStudents(@RequestBody LiveRoomStudentParam param) {
-//        return liveDataService.listLiveRoomStudents(param);
-//    }
-//
-//    /**
-//     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
-//     */
-//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
-//    @PostMapping("/listProductCompareStats")
-//    public R listProductCompareStats(@RequestBody ProductCompareParam param) {
-//        return liveDataService.listProductCompareStats(param);
-//    }
-//
-//    /**
-//     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
-//     */
-//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
-//    @PostMapping("/listInviteSalesOptions")
-//    public R listInviteSalesOptions(@RequestBody List<Long> liveIds) {
-//        return R.ok().put("data", liveDataService.listInviteSalesOptions(liveIds != null ? liveIds : Collections.emptyList()));
-//    }
-//
-//    /**
-//     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
-//     */
-//    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
-//    @PostMapping("/listInviteCompareStats")
-//    public R listInviteCompareStats(@RequestBody InviteCompareParam param) {
-//        return liveDataService.listInviteCompareStats(param);
-//    }
+    /**
+     * 直播数据统计-数据概览(12项指标)
+     * @param liveIds 直播间ID列表,前端传入
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/getLiveStatisticsOverview")
+    public R getLiveStatisticsOverview(@RequestBody List<Long> liveIds) {
+
+        return R.ok().put("data",liveDataService.getLiveStatisticsOverview(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 直播趋势-进入人数折线图
+     * 基于 live_user_first_entry 与 live.start_time 计算相对时间,开播前进入的归为"开播前"
+     * @param liveIds 直播间ID列表
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/getLiveEntryTrend")
+    public R getLiveEntryTrend(@RequestBody List<Long> liveIds) {
+        return R.ok().put("data", liveDataService.getLiveEntryTrend(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 直播间学员列表(分页,基于 live_user_first_entry)
+     * 筛选:直播名称(liveIds)、首次访问时间范围
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listLiveRoomStudents")
+    public R listLiveRoomStudents(@RequestBody LiveRoomStudentParam param) {
+        return liveDataService.listLiveRoomStudents(param);
+    }
+
+    /**
+     * 商品对比统计(商品名称、下单未支付人数、成交人数、成交金额)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listProductCompareStats")
+    public R listProductCompareStats(@RequestBody ProductCompareParam param) {
+        return liveDataService.listProductCompareStats(param);
+    }
+
+    /**
+     * 邀课对比-分享人选项列表(基于 live_user_first_entry 中存在的销售)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listInviteSalesOptions")
+    public R listInviteSalesOptions(@RequestBody List<Long> liveIds) {
+        return R.ok().put("data", liveDataService.listInviteSalesOptions(liveIds != null ? liveIds : Collections.emptyList()));
+    }
+
+    /**
+     * 邀课对比统计(归属公司、销售名称、邀请人数、已支付订单数、订单总金额)
+     */
+    @PreAuthorize("@ss.hasPermi('liveData:liveData:query')")
+    @PostMapping("/listInviteCompareStats")
+    public R listInviteCompareStats(@RequestBody InviteCompareParam param) {
+        return liveDataService.listInviteCompareStats(param);
+    }
 
     /**
      * 查询直播间详情数据(SQL方式)

+ 69 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveQuestionLiveController.java

@@ -0,0 +1,69 @@
+package com.fs.company.controller.live;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.common.enums.BusinessType;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.live.service.ILiveCourseQuestionRelService;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 直播间答题配置:关联课程题库 fs_course_question_bank
+ */
+@RestController
+@RequestMapping("/live/liveQuestionLive")
+public class LiveQuestionLiveController extends BaseController {
+
+    @Autowired
+    private ILiveCourseQuestionRelService liveCourseQuestionRelService;
+
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/list")
+    public TableDataInfo list(@RequestParam Long liveId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectLinkedByLiveId(liveId, user.getCompanyId());
+        return getDataTable(list);
+    }
+
+    /**
+     * 待选:课程题库中尚未关联到本直播间的试题(status=1)
+     */
+    @PreAuthorize("@ss.hasPermi('live:live:query')")
+    @GetMapping("/optionList")
+    public TableDataInfo optionList(@RequestParam Long liveId,
+                                    @RequestParam(required = false) String title,
+                                    @RequestParam(required = false) Integer type) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        startPage();
+        List<LiveQuestionLiveVO> list = liveCourseQuestionRelService.selectOptionQuestionBank(
+                liveId, user.getCompanyId(), title, type);
+        return getDataTable(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestParam Long liveId, @RequestParam String questionIds) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        int rows = liveCourseQuestionRelService.batchAdd(liveId, user.getCompanyId(), questionIds);
+        return rows > 0 ? AjaxResult.success("添加成功") : AjaxResult.success("无新增(可能已全部存在)");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:live:edit')")
+    @Log(title = "直播间试题", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{liveId}")
+    public AjaxResult remove(@PathVariable Long liveId, @RequestParam String ids) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        int rows = liveCourseQuestionRelService.deleteByRelIds(liveId, user.getCompanyId(), ids);
+        return toAjax(rows);
+    }
+}

+ 182 - 0
fs-company/src/main/java/com/fs/company/controller/live/LiveTrainingCampController.java

@@ -0,0 +1,182 @@
+package com.fs.company.controller.live;
+
+import com.fs.common.annotation.Log;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.enums.BusinessType;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.framework.security.SecurityUtils;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveTrainingCamp;
+import com.fs.live.domain.LiveTrainingPeriod;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveTrainingCampService;
+import com.fs.live.service.ILiveTrainingPeriodService;
+import com.github.pagehelper.PageInfo;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 训练营-营期-直播间(与普通直播列表区分,live.training_period_id 非空)
+ */
+@RestController
+@RequestMapping("/live/trainingCamp")
+public class LiveTrainingCampController extends BaseController {
+
+    @Autowired
+    private ILiveTrainingCampService liveTrainingCampService;
+    @Autowired
+    private ILiveTrainingPeriodService liveTrainingPeriodService;
+    @Autowired
+    private ILiveService liveService;
+
+    private static R pageR(List<?> list) {
+        return R.ok().put("rows", list).put("total", new PageInfo(list).getTotal());
+    }
+
+    private static R toR(int rows, String okMsg) {
+        return rows > 0 ? R.ok(okMsg) : R.error("操作失败");
+    }
+
+    // ---------- 训练营 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:list')")
+    @GetMapping("/camp/list")
+    public R campList(LiveTrainingCamp query) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        if (StringUtils.isEmpty(user.getUserName())) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        query.setCompanyId(user.getCompanyId());
+        query.setCreateBy(user.getUserName());
+        startPage();
+        List<LiveTrainingCamp> list = liveTrainingCampService.selectLiveTrainingCampList(query);
+        return pageR(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:query')")
+    @GetMapping("/camp/{campId}")
+    public R getCamp(@PathVariable Long campId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        LiveTrainingCamp c = liveTrainingCampService.selectLiveTrainingCampById(campId, user.getCompanyId(), user.getUserName());
+        return R.ok().put("data", c);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:add')")
+    @Log(title = "直播训练营", businessType = BusinessType.INSERT)
+    @PostMapping("/camp")
+    public R addCamp(@RequestBody LiveTrainingCamp camp) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        camp.setCompanyId(user.getCompanyId());
+        camp.setCreateBy(user.getUserName());
+        return toR(liveTrainingCampService.insertLiveTrainingCamp(camp), "新增成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:edit')")
+    @Log(title = "直播训练营", businessType = BusinessType.UPDATE)
+    @PutMapping("/camp")
+    public R editCamp(@RequestBody LiveTrainingCamp camp) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        camp.setCompanyId(user.getCompanyId());
+        camp.setUpdateBy(user.getUserName());
+        camp.getParams().put("ownerCreateBy", user.getUserName());
+        return toR(liveTrainingCampService.updateLiveTrainingCamp(camp), "修改成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:remove')")
+    @Log(title = "直播训练营", businessType = BusinessType.DELETE)
+    @DeleteMapping("/camp/{campIds}")
+    public R removeCamp(@PathVariable Long[] campIds) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return toR(liveTrainingCampService.deleteLiveTrainingCampByIds(campIds, user.getCompanyId(), user.getUserName()), "删除成功");
+    }
+
+    // ---------- 营期 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:list')")
+    @GetMapping("/period/list")
+    public R periodList(LiveTrainingPeriod query) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        if (StringUtils.isEmpty(user.getUserName())) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        query.setCompanyId(user.getCompanyId());
+        query.setCreateBy(user.getUserName());
+        startPage();
+        List<LiveTrainingPeriod> list = liveTrainingPeriodService.selectLiveTrainingPeriodList(query);
+        return pageR(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:query')")
+    @GetMapping("/period/{periodId}")
+    public R getPeriod(@PathVariable Long periodId) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodById(periodId, user.getCompanyId(), user.getUserName());
+        return R.ok().put("data", p);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:add')")
+    @Log(title = "直播训练营营期", businessType = BusinessType.INSERT)
+    @PostMapping("/period")
+    public R addPeriod(@RequestBody LiveTrainingPeriod period) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        period.setCompanyId(user.getCompanyId());
+        period.setCreateBy(user.getUserName());
+        return toR(liveTrainingPeriodService.insertLiveTrainingPeriod(period), "新增成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:edit')")
+    @Log(title = "直播训练营营期", businessType = BusinessType.UPDATE)
+    @PutMapping("/period")
+    public R editPeriod(@RequestBody LiveTrainingPeriod period) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        period.setCompanyId(user.getCompanyId());
+        period.setUpdateBy(user.getUserName());
+        period.getParams().put("ownerCreateBy", user.getUserName());
+        return toR(liveTrainingPeriodService.updateLiveTrainingPeriod(period), "修改成功");
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:remove')")
+    @Log(title = "直播训练营营期", businessType = BusinessType.DELETE)
+    @DeleteMapping("/period/{periodIds}")
+    public R removePeriod(@PathVariable Long[] periodIds) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        return toR(liveTrainingPeriodService.deleteLiveTrainingPeriodByIds(periodIds, user.getCompanyId(), user.getUserName()), "删除成功");
+    }
+
+    // ---------- 营期下直播间 ----------
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:list')")
+    @GetMapping("/live/list")
+    public R trainingLiveList(Live query) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        query.setCompanyId(user.getCompanyId());
+        query.setCompanyUserId(user.getUserId());
+        if (query.getTrainingPeriodId() == null) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        LiveTrainingPeriod p = liveTrainingPeriodService.selectLiveTrainingPeriodById(
+                query.getTrainingPeriodId(), user.getCompanyId(), user.getUserName());
+        if (p == null) {
+            return R.ok().put("rows", Collections.emptyList()).put("total", 0L);
+        }
+        startPage();
+        List<Live> list = liveService.selectLiveList(query);
+        return pageR(list);
+    }
+
+    @PreAuthorize("@ss.hasPermi('live:trainingCamp:live:add')")
+    @Log(title = "训练营直播间", businessType = BusinessType.INSERT)
+    @PostMapping("/live")
+    public R addTrainingLive(@RequestBody Live live) {
+        CompanyUser user = SecurityUtils.getLoginUser().getUser();
+        // live.create_by 列为数值类型(创建人用户ID),不能使用昵称/登录名
+        live.setCreateBy(String.valueOf(user.getUserId()));
+        return toR(liveTrainingPeriodService.insertTrainingLive(live, user.getCompanyId(), user.getUserId(), user.getUserName()), "新增成功");
+    }
+}

+ 94 - 0
fs-live-app/src/main/java/com/fs/live/websocket/service/WebSocketServer.java

@@ -2,6 +2,7 @@ package com.fs.live.websocket.service;
 
 
 import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.fs.common.constant.LiveKeysConstant;
 import com.fs.common.core.redis.RedisCacheT;
@@ -23,7 +24,10 @@ import com.fs.common.core.domain.R;
 import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.StringUtils;
 import com.fs.common.utils.spring.SpringUtils;
+import com.fs.course.domain.FsCourseQuestionBank;
+import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.live.domain.*;
+import com.fs.live.mapper.LiveCourseQuestionRelMapper;
 import com.fs.live.service.*;
 import com.fs.live.vo.LiveGoodsVo;
 import com.fs.newAdv.service.ILeadService;
@@ -626,12 +630,102 @@ public class WebSocketServer {
                         delAutoTask(liveId, DateUtils.parseDate(msg.getData(),"yyyy-MM-dd'T'HH:mm:ss.SSSZ").getTime());
                     }
                     break;
+                case "liveQuizStart":
+                    if (userType == 1) {
+                        processLiveQuizStart(liveId, msg);
+                    }
+                    break;
+                case "liveQuizClose":
+                    if (userType == 1) {
+                        processLiveQuizClose(liveId, msg);
+                    }
+                    break;
             }
         } catch (Exception e) {
             log.error("webSocket 消息处理失败 msg: {}", e.getMessage(), e);
         }
     }
 
+    /**
+     * 管理端:向直播间观众广播「开始答题」及题目内容(不含正确答案,仅选项文案)
+     */
+    private void processLiveQuizStart(long liveId, SendMsgVo msg) {
+        try {
+            if (StringUtils.isEmpty(msg.getData())) {
+                return;
+            }
+            JSONObject body = JSON.parseObject(msg.getData());
+            Long relId = body.getLong("relId");
+            if (relId == null) {
+                return;
+            }
+            LiveCourseQuestionRelMapper relMapper = SpringUtils.getBean(LiveCourseQuestionRelMapper.class);
+            Long questionBankId = relMapper.selectQuestionBankIdByLiveAndRel(liveId, relId);
+            if (questionBankId == null) {
+                log.warn("liveQuizStart: 未找到关联 liveId={} relId={}", liveId, relId);
+                return;
+            }
+            IFsCourseQuestionBankService bankService = SpringUtils.getBean(IFsCourseQuestionBankService.class);
+            FsCourseQuestionBank bank = bankService.selectFsCourseQuestionBankById(questionBankId);
+            if (bank == null) {
+                return;
+            }
+            JSONArray optionsOut = new JSONArray();
+            String qStr = bank.getQuestion();
+            if (StringUtils.isNotEmpty(qStr)) {
+                JSONArray arr = JSON.parseArray(qStr);
+                String alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+                for (int i = 0; i < arr.size(); i++) {
+                    JSONObject opt = arr.getJSONObject(i);
+                    JSONObject row = new JSONObject();
+                    row.put("key", i < alphabet.length() ? String.valueOf(alphabet.charAt(i)) : String.valueOf(i + 1));
+                    row.put("name", opt.getString("name"));
+                    optionsOut.add(row);
+                }
+            }
+            JSONObject data = new JSONObject();
+            data.put("relId", relId);
+            data.put("questionBankId", bank.getId());
+            data.put("title", bank.getTitle());
+            data.put("type", bank.getType());
+            data.put("options", optionsOut);
+            SendMsgVo out = new SendMsgVo();
+            out.setLiveId(liveId);
+            out.setUserType(1L);
+            out.setCmd("liveQuizStart");
+            out.setOn(true);
+            out.setData(data.toJSONString());
+            enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", out)), true);
+        } catch (Exception e) {
+            log.error("liveQuizStart 处理异常 liveId={}", liveId, e);
+        }
+    }
+
+    /**
+     * 管理端:广播「结束答题」
+     */
+    private void processLiveQuizClose(long liveId, SendMsgVo msg) {
+        try {
+            JSONObject data = new JSONObject();
+            if (StringUtils.isNotEmpty(msg.getData())) {
+                JSONObject body = JSON.parseObject(msg.getData());
+                Long relId = body.getLong("relId");
+                if (relId != null) {
+                    data.put("relId", relId);
+                }
+            }
+            SendMsgVo out = new SendMsgVo();
+            out.setLiveId(liveId);
+            out.setUserType(1L);
+            out.setCmd("liveQuizClose");
+            out.setOn(true);
+            out.setData(data.toJSONString());
+            enqueueMessage(liveId, JSONObject.toJSONString(R.ok().put("data", out)), true);
+        } catch (Exception e) {
+            log.error("liveQuizClose 处理异常 liveId={}", liveId, e);
+        }
+    }
+
     private void deleteMsg(long liveId,SendMsgVo msg) {
         SendMsgVo sendMsgVo = new SendMsgVo();
         sendMsgVo.setLiveId(liveId);

+ 30 - 0
fs-service/src/main/java/com/fs/course/param/LiveQuizSubmitUParam.java

@@ -0,0 +1,30 @@
+package com.fs.course.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 直播答题提交(观众端)
+ */
+@Data
+public class LiveQuizSubmitUParam implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 当前登录用户(由 Controller 写入) */
+    private Long userId;
+
+    private Long liveId;
+
+    private Long relId;
+
+    private Long questionBankId;
+
+    /** 与题库一致:1 单选 2 多选 */
+    private Integer type;
+
+    /** 用户选择的选项 key,如 A、B;多选为多个 */
+    private List<String> answerKeys;
+}

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

@@ -5,6 +5,7 @@ import com.fs.course.domain.FsCourseQuestionBank;
 import com.fs.course.dto.FsCourseQuestionBankImportDTO;
 import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
+import com.fs.course.param.LiveQuizSubmitUParam;
 
 import javax.validation.constraints.Size;
 import java.util.List;
@@ -93,4 +94,9 @@ public interface IFsCourseQuestionBankService
     R courseAnswerByFsUser(FsCourseQuestionAnswerUParam param);
 
     R courseAnswerIsOpen(FsCourseQuestionAnswerUParam param, boolean isH5User);
+
+    /**
+     * 直播答题提交:校验直播间-题目关联、题库是否存在,按 course.config 决定是否跳过对错校验(与课程答题 submit 逻辑一致)。
+     */
+    R submitLiveQuiz(LiveQuizSubmitUParam param);
 }

+ 82 - 0
fs-service/src/main/java/com/fs/course/service/impl/FsCourseQuestionBankServiceImpl.java

@@ -14,8 +14,10 @@ import com.fs.course.dto.FsCourseQuestionBankImportDTO;
 import com.fs.course.dto.ImportResultDTO;
 import com.fs.course.mapper.*;
 import com.fs.course.param.FsCourseQuestionAnswerUParam;
+import com.fs.course.param.LiveQuizSubmitUParam;
 import com.fs.course.service.IFsCourseQuestionBankService;
 import com.fs.course.util.CourseConfigUserAnswerExpose;
+import com.fs.live.mapper.LiveCourseQuestionRelMapper;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.service.IFsStorePaymentService;
@@ -59,6 +61,8 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
     @Autowired
     private FsCourseWatchLogMapper courseWatchLogMapper;
     @Autowired
+    private LiveCourseQuestionRelMapper liveCourseQuestionRelMapper;
+    @Autowired
     private FsUserCourseCategoryMapper courseCategoryMapper;
     @Value("${cloud_host.company_name}")
     private String signProjectName;
@@ -506,6 +510,84 @@ public class FsCourseQuestionBankServiceImpl implements IFsCourseQuestionBankSer
         }
     }
 
+    @Override
+    public R submitLiveQuiz(LiveQuizSubmitUParam param) {
+        if (param == null) {
+            return R.error("参数不能为空");
+        }
+        if (param.getLiveId() == null || param.getRelId() == null || param.getQuestionBankId() == null) {
+            return R.error("参数不完整");
+        }
+        List<String> keys = param.getAnswerKeys();
+        if (keys == null || keys.isEmpty()) {
+            return R.error("请选择答案");
+        }
+        Long questionBankIdFromRel = liveCourseQuestionRelMapper.selectQuestionBankIdByLiveAndRel(
+                param.getLiveId(), param.getRelId());
+        if (questionBankIdFromRel == null) {
+            return R.error("题目与直播间关联无效");
+        }
+        if (!questionBankIdFromRel.equals(param.getQuestionBankId())) {
+            return R.error("题目信息不一致");
+        }
+        FsCourseQuestionBank bank = fsCourseQuestionBankMapper.selectFsCourseQuestionBankById(param.getQuestionBankId());
+        if (bank == null) {
+            return R.error("题目不存在");
+        }
+        if (param.getType() != null && bank.getType() != null
+                && bank.getType().intValue() != param.getType()) {
+            return R.error("题目类型不匹配");
+        }
+        if (bank.getType() == null) {
+            return R.error("题目数据异常");
+        }
+
+        String json = configService.selectConfigByKey("course.config");
+        boolean skipAnswerValidation = CourseConfigUserAnswerExpose.skipValidateAnswerOnSubmit(json);
+
+        boolean correct;
+        if (skipAnswerValidation) {
+            correct = true;
+        } else {
+            correct = matchLiveQuizUserAnswer(bank, keys);
+        }
+
+        if (correct) {
+            return R.ok("回答正确").put("correct", true);
+        }
+        return R.ok("回答错误").put("correct", false);
+    }
+
+    /**
+     * 与 {@link #courseAnswer} 中单选/多选判分规则一致。
+     */
+    private boolean matchLiveQuizUserAnswer(FsCourseQuestionBank bank, List<String> answerKeys) {
+        if (bank.getAnswer() == null) {
+            return false;
+        }
+        long t = bank.getType();
+        if (t == 1L) {
+            if (answerKeys.size() != 1) {
+                return false;
+            }
+            String u = answerKeys.get(0) == null ? "" : answerKeys.get(0).trim();
+            String c = bank.getAnswer().trim();
+            return u.equals(c);
+        }
+        if (t == 2L) {
+            String[] userAnswers = answerKeys.stream()
+                    .filter(Objects::nonNull)
+                    .map(String::trim)
+                    .filter(s -> !s.isEmpty())
+                    .toArray(String[]::new);
+            String[] correctAnswers = convertStringToArray(bank.getAnswer());
+            Arrays.sort(userAnswers);
+            Arrays.sort(correctAnswers);
+            return Arrays.equals(userAnswers, correctAnswers);
+        }
+        return false;
+    }
+
     /**
      * 题目导入
      *

+ 14 - 1
fs-service/src/main/java/com/fs/live/domain/Live.java

@@ -39,6 +39,11 @@ public class   Live extends BaseEntity {
      */
     private Long companyUserId;
 
+    /**
+     * 营期ID(非空表示归属「训练营-营期」直播间,与普通直播列表区分)
+     */
+    private Long trainingPeriodId;
+
     /**
      * 达人ID
      */
@@ -121,7 +126,7 @@ public class   Live extends BaseEntity {
     /** 直播配置 */
     private String configJson;
 
-    /** 直播审核状态,销售端修改后需要总后台审核 0未审核 1已审核*/
+    /** 直播审核状态:0待审核 1已通过 2已驳回(训练营直播间新建/企业修改后需总后台审核;C 端仅展示已通过) */
     private Integer isAudit;
     /** 创建时间 */
     private Date createTime;
@@ -136,4 +141,12 @@ public class   Live extends BaseEntity {
 
     private Integer pageNum;
     private Integer pageSize;
+
+    /** 查询用:为 true 时仅查普通直播(training_period_id 为空),企业端默认排除训练营直播间 */
+    @TableField(exist = false)
+    private Boolean excludeCampLive;
+
+    /** 查询用:为 true 时仅查训练营直播间(training_period_id 非空),总后台审核列表等 */
+    @TableField(exist = false)
+    private Boolean onlyTrainingCampLive;
 }

+ 22 - 0
fs-service/src/main/java/com/fs/live/domain/LiveCourseQuestionRel.java

@@ -0,0 +1,22 @@
+package com.fs.live.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 直播间与课程题库试题关联
+ */
+@Data
+public class LiveCourseQuestionRel {
+
+    private Long relId;
+
+    private Long liveId;
+
+    private Long questionBankId;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 37 - 0
fs-service/src/main/java/com/fs/live/domain/LiveTrainingCamp.java

@@ -0,0 +1,37 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播训练营
+ *
+ * @author fs
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveTrainingCamp extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long campId;
+
+    private Long companyId;
+
+    private String campName;
+
+    private Integer sortOrder;
+
+    /** 0正常 1停用 */
+    private Integer status;
+
+    private Integer isDel;
+
+    private String description;
+
+    /** 关联展示:企业名称(总后台列表) */
+    @TableField(exist = false)
+    private String companyName;
+}

+ 52 - 0
fs-service/src/main/java/com/fs/live/domain/LiveTrainingPeriod.java

@@ -0,0 +1,52 @@
+package com.fs.live.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Date;
+
+/**
+ * 直播训练营营期
+ *
+ * @author fs
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LiveTrainingPeriod extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long periodId;
+
+    private Long campId;
+
+    private String periodName;
+
+    /** 营期封面图 URL */
+    private String periodImgUrl;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String endTime;
+
+    private Integer sortOrder;
+
+    /** 0正常 1停用 */
+    private Integer status;
+
+    private Integer isDel;
+
+    private String description;
+
+    /** 查询展示:训练营名称 */
+    private String campName;
+
+    /** 列表/更新权限校验:当前企业 */
+    @TableField(exist = false)
+    private Long companyId;
+}

+ 28 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveCourseQuestionRelMapper.java

@@ -0,0 +1,28 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveCourseQuestionRel;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播间-课程题库关联
+ */
+public interface LiveCourseQuestionRelMapper {
+
+    List<LiveQuestionLiveVO> selectLinkedByLiveId(@Param("liveId") Long liveId);
+
+    List<LiveQuestionLiveVO> selectOptionQuestionBank(@Param("liveId") Long liveId,
+                                                      @Param("title") String title,
+                                                      @Param("type") Integer type);
+
+    int insertIgnore(@Param("liveId") Long liveId, @Param("questionBankId") Long questionBankId);
+
+    int deleteByRelIds(@Param("liveId") Long liveId, @Param("relIds") Long[] relIds);
+
+    /**
+     * 校验关联是否属于该直播间,并解析课程题库主键
+     */
+    Long selectQuestionBankIdByLiveAndRel(@Param("liveId") Long liveId, @Param("relId") Long relId);
+}

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

@@ -6,6 +6,7 @@ import com.fs.common.enums.DataSourceType;
 import com.fs.live.domain.Live;
 import com.fs.live.param.LiveDataParam;
 import com.fs.live.vo.LiveListVo;
+import com.fs.live.vo.TrainingLiveAuditVO;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import org.apache.ibatis.annotations.Update;
@@ -50,6 +51,8 @@ public interface LiveMapper
      */
     public List<Live> selectLiveList(Live live);
 
+    List<TrainingLiveAuditVO> selectTrainingLiveAuditList(Live live);
+
     /**
      * 新增直播
      *
@@ -235,6 +238,7 @@ public interface LiveMapper
 
     @Select({"<script>" +
             " SELECT * FROM live WHERE is_audit = 1 and is_del = 0 and status in (1,2,4) and live_type in (2,3) " +
+            " and training_period_id is null " +
             "  <if test='live.liveName!=null' > and live_name like concat('%',#{live.liveName},'%') </if> " +
             " order by create_time desc" +
             " </script>"})
@@ -254,8 +258,12 @@ public interface LiveMapper
 
     @Select({"<script>" +
             " SELECT * FROM live WHERE is_audit = 1 and is_del = 0 and status in (1,2,4)" +
+            " and training_period_id is null " +
             "  <if test='live.liveName!=null' > and live_name like concat('%',#{live.liveName},'%') </if> " +
             " order by create_time desc" +
             " </script>"})
     List<Live> listToLiveNoEndNew(@Param("live") Live live);
+
+    @Select("SELECT COUNT(1) FROM live WHERE is_del = 0 AND training_period_id = #{periodId}")
+    int countLiveByTrainingPeriodId(@Param("periodId") Long periodId);
 }

+ 25 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveTrainingCampMapper.java

@@ -0,0 +1,25 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveTrainingCamp;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播训练营 Mapper
+ */
+public interface LiveTrainingCampMapper {
+
+    LiveTrainingCamp selectLiveTrainingCampById(@Param("campId") Long campId);
+
+    List<LiveTrainingCamp> selectLiveTrainingCampList(LiveTrainingCamp query);
+
+    int insertLiveTrainingCamp(LiveTrainingCamp camp);
+
+    int updateLiveTrainingCamp(LiveTrainingCamp camp);
+
+    int logicDeleteLiveTrainingCampByIds(@Param("campIds") Long[] campIds, @Param("companyId") Long companyId,
+                                         @Param("createBy") String createBy);
+
+    List<LiveTrainingCamp> selectLiveTrainingCampListAdmin(LiveTrainingCamp query);
+}

+ 27 - 0
fs-service/src/main/java/com/fs/live/mapper/LiveTrainingPeriodMapper.java

@@ -0,0 +1,27 @@
+package com.fs.live.mapper;
+
+import com.fs.live.domain.LiveTrainingPeriod;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 直播训练营营期 Mapper
+ */
+public interface LiveTrainingPeriodMapper {
+
+    LiveTrainingPeriod selectLiveTrainingPeriodById(@Param("periodId") Long periodId);
+
+    List<LiveTrainingPeriod> selectLiveTrainingPeriodList(LiveTrainingPeriod query);
+
+    int countPeriodByCampId(@Param("campId") Long campId);
+
+    int insertLiveTrainingPeriod(LiveTrainingPeriod period);
+
+    int updateLiveTrainingPeriod(LiveTrainingPeriod period);
+
+    int logicDeleteLiveTrainingPeriodByIds(@Param("periodIds") Long[] periodIds, @Param("companyId") Long companyId,
+                                          @Param("createBy") String createBy);
+
+    List<LiveTrainingPeriod> selectLiveTrainingPeriodListAdmin(LiveTrainingPeriod query);
+}

+ 18 - 0
fs-service/src/main/java/com/fs/live/param/LiveTrainingLiveAuditBody.java

@@ -0,0 +1,18 @@
+package com.fs.live.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class LiveTrainingLiveAuditBody implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long liveId;
+
+    /** true=审核通过(1) false=驳回(2) */
+    private Boolean passed;
+
+    private String remark;
+}

+ 21 - 0
fs-service/src/main/java/com/fs/live/service/ILiveCourseQuestionRelService.java

@@ -0,0 +1,21 @@
+package com.fs.live.service;
+
+import com.fs.live.vo.LiveQuestionLiveVO;
+
+import java.util.List;
+
+/**
+ * 直播间与课程题库关联
+ */
+public interface ILiveCourseQuestionRelService {
+
+    void checkLiveCompany(Long liveId, Long companyId);
+
+    List<LiveQuestionLiveVO> selectLinkedByLiveId(Long liveId, Long companyId);
+
+    List<LiveQuestionLiveVO> selectOptionQuestionBank(Long liveId, Long companyId, String title, Integer type);
+
+    int batchAdd(Long liveId, Long companyId, String questionIdsCsv);
+
+    int deleteByRelIds(Long liveId, Long companyId, String relIdsCsv);
+}

+ 6 - 0
fs-service/src/main/java/com/fs/live/service/ILiveService.java

@@ -10,6 +10,7 @@ import com.fs.common.core.domain.R;
 import com.fs.live.domain.Live;
 import com.fs.live.vo.LiveConfigVo;
 import com.fs.live.vo.LiveListVo;
+import com.fs.live.vo.TrainingLiveAuditVO;
 
 import java.util.HashMap;
 import java.util.List;
@@ -232,4 +233,9 @@ public interface ILiveService
     R createAppLink(CompanyUser user, Long liveId, String corpId);
 
     List<Live> listToLiveNoEndNew(Live live);
+
+    /**
+     * 训练营直播间审核列表(含营期/训练营/企业名称)
+     */
+    List<TrainingLiveAuditVO> selectTrainingLiveAuditList(Live query);
 }

+ 27 - 0
fs-service/src/main/java/com/fs/live/service/ILiveTrainingCampService.java

@@ -0,0 +1,27 @@
+package com.fs.live.service;
+
+import com.fs.live.domain.LiveTrainingCamp;
+
+import java.util.List;
+
+/**
+ * 直播训练营
+ */
+public interface ILiveTrainingCampService {
+
+    List<LiveTrainingCamp> selectLiveTrainingCampList(LiveTrainingCamp query);
+
+    LiveTrainingCamp selectLiveTrainingCampById(Long campId, Long companyId, String operatorUserName);
+
+    int insertLiveTrainingCamp(LiveTrainingCamp camp);
+
+    int updateLiveTrainingCamp(LiveTrainingCamp camp);
+
+    int deleteLiveTrainingCampByIds(Long[] campIds, Long companyId, String createBy);
+
+    /** 总后台列表(不按创建人隔离,含企业名称) */
+    List<LiveTrainingCamp> selectLiveTrainingCampListAdmin(LiveTrainingCamp query);
+
+    /** 总后台详情;companyId 非空时校验归属 */
+    LiveTrainingCamp selectLiveTrainingCampByIdForAdmin(Long campId, Long companyId);
+}

+ 34 - 0
fs-service/src/main/java/com/fs/live/service/ILiveTrainingPeriodService.java

@@ -0,0 +1,34 @@
+package com.fs.live.service;
+
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveTrainingPeriod;
+
+import java.util.List;
+
+/**
+ * 直播训练营营期
+ */
+public interface ILiveTrainingPeriodService {
+
+    List<LiveTrainingPeriod> selectLiveTrainingPeriodList(LiveTrainingPeriod query);
+
+    LiveTrainingPeriod selectLiveTrainingPeriodById(Long periodId, Long companyId, String operatorUserName);
+
+    int insertLiveTrainingPeriod(LiveTrainingPeriod period);
+
+    int updateLiveTrainingPeriod(LiveTrainingPeriod period);
+
+    int deleteLiveTrainingPeriodByIds(Long[] periodIds, Long companyId, String createBy);
+
+    /**
+     * 在当前营期下创建直播间(写入 live.training_period_id)
+     */
+    int insertTrainingLive(Live live, Long companyId, Long companyUserId, String operatorUserName);
+
+    List<LiveTrainingPeriod> selectLiveTrainingPeriodListAdmin(LiveTrainingPeriod query);
+
+    LiveTrainingPeriod selectLiveTrainingPeriodByIdForAdmin(Long periodId, Long companyId);
+
+    /** 总后台在营期下创建直播间(不校验营期创建人;默认绑定企业下首个账号为 company_user_id) */
+    int insertTrainingLiveForAdmin(Live live, Long companyId);
+}

+ 90 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveCourseQuestionRelServiceImpl.java

@@ -0,0 +1,90 @@
+package com.fs.live.service.impl;
+
+import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.StringUtils;
+import com.fs.live.domain.Live;
+import com.fs.live.mapper.LiveCourseQuestionRelMapper;
+import com.fs.live.mapper.LiveMapper;
+import com.fs.live.service.ILiveCourseQuestionRelService;
+import com.fs.live.vo.LiveQuestionLiveVO;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class LiveCourseQuestionRelServiceImpl implements ILiveCourseQuestionRelService {
+
+    private final LiveCourseQuestionRelMapper liveCourseQuestionRelMapper;
+    private final LiveMapper liveMapper;
+
+    @Override
+    public void checkLiveCompany(Long liveId, Long companyId) {
+        if (liveId == null || companyId == null) {
+            throw new BaseException("参数错误");
+        }
+        Live live = liveMapper.selectLiveByLiveIdAndCompanyId(liveId, companyId);
+        if (live == null) {
+            throw new BaseException("直播间不存在或无权限");
+        }
+    }
+
+    @Override
+    public List<LiveQuestionLiveVO> selectLinkedByLiveId(Long liveId, Long companyId) {
+        checkLiveCompany(liveId, companyId);
+        return liveCourseQuestionRelMapper.selectLinkedByLiveId(liveId);
+    }
+
+    @Override
+    public List<LiveQuestionLiveVO> selectOptionQuestionBank(Long liveId, Long companyId, String title, Integer type) {
+        checkLiveCompany(liveId, companyId);
+        return liveCourseQuestionRelMapper.selectOptionQuestionBank(liveId, title, type);
+    }
+
+    @Override
+    public int batchAdd(Long liveId, Long companyId, String questionIdsCsv) {
+        checkLiveCompany(liveId, companyId);
+        if (StringUtils.isEmpty(questionIdsCsv)) {
+            return 0;
+        }
+        String[] parts = questionIdsCsv.split(",");
+        int n = 0;
+        for (String p : parts) {
+            if (StringUtils.isEmpty(p)) {
+                continue;
+            }
+            try {
+                Long qid = Long.parseLong(p.trim());
+                n += liveCourseQuestionRelMapper.insertIgnore(liveId, qid);
+            } catch (NumberFormatException ignored) {
+                // skip invalid
+            }
+        }
+        return n;
+    }
+
+    @Override
+    public int deleteByRelIds(Long liveId, Long companyId, String relIdsCsv) {
+        checkLiveCompany(liveId, companyId);
+        if (StringUtils.isEmpty(relIdsCsv)) {
+            return 0;
+        }
+        List<Long> ids = new ArrayList<>();
+        for (String p : relIdsCsv.split(",")) {
+            if (StringUtils.isEmpty(p)) {
+                continue;
+            }
+            try {
+                ids.add(Long.parseLong(p.trim()));
+            } catch (NumberFormatException ignored) {
+                // skip
+            }
+        }
+        if (ids.isEmpty()) {
+            return 0;
+        }
+        return liveCourseQuestionRelMapper.deleteByRelIds(liveId, ids.toArray(new Long[0]));
+    }
+}

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

@@ -626,6 +626,10 @@ public class LiveServiceImpl implements ILiveService
         return baseMapper.selectLiveList(live);
     }
 
+    @Override
+    public List<TrainingLiveAuditVO> selectTrainingLiveAuditList(Live query) {
+        return baseMapper.selectTrainingLiveAuditList(query);
+    }
 
     /**
      * 查询直播列表

+ 88 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveTrainingCampServiceImpl.java

@@ -0,0 +1,88 @@
+package com.fs.live.service.impl;
+
+import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.DateUtils;
+import com.fs.live.domain.LiveTrainingCamp;
+import com.fs.live.mapper.LiveTrainingCampMapper;
+import com.fs.live.mapper.LiveTrainingPeriodMapper;
+import com.fs.live.service.ILiveTrainingCampService;
+import com.fs.common.utils.StringUtils;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+public class LiveTrainingCampServiceImpl implements ILiveTrainingCampService {
+
+    private final LiveTrainingCampMapper liveTrainingCampMapper;
+    private final LiveTrainingPeriodMapper liveTrainingPeriodMapper;
+
+    @Override
+    public List<LiveTrainingCamp> selectLiveTrainingCampList(LiveTrainingCamp query) {
+        return liveTrainingCampMapper.selectLiveTrainingCampList(query);
+    }
+
+    @Override
+    public LiveTrainingCamp selectLiveTrainingCampById(Long campId, Long companyId, String operatorUserName) {
+        LiveTrainingCamp c = liveTrainingCampMapper.selectLiveTrainingCampById(campId);
+        if (c == null || !companyId.equals(c.getCompanyId())) {
+            return null;
+        }
+        if (StringUtils.isNotEmpty(operatorUserName)
+                && (c.getCreateBy() == null || !operatorUserName.equals(c.getCreateBy()))) {
+            return null;
+        }
+        return c;
+    }
+
+    @Override
+    public int insertLiveTrainingCamp(LiveTrainingCamp camp) {
+        if (camp.getSortOrder() == null) {
+            camp.setSortOrder(0);
+        }
+        if (camp.getStatus() == null) {
+            camp.setStatus(0);
+        }
+        camp.setCreateTime(DateUtils.getNowDate());
+        return liveTrainingCampMapper.insertLiveTrainingCamp(camp);
+    }
+
+    @Override
+    public int updateLiveTrainingCamp(LiveTrainingCamp camp) {
+        camp.setUpdateTime(DateUtils.getNowDate());
+        return liveTrainingCampMapper.updateLiveTrainingCamp(camp);
+    }
+
+    @Override
+    public List<LiveTrainingCamp> selectLiveTrainingCampListAdmin(LiveTrainingCamp query) {
+        return liveTrainingCampMapper.selectLiveTrainingCampListAdmin(query);
+    }
+
+    @Override
+    public LiveTrainingCamp selectLiveTrainingCampByIdForAdmin(Long campId, Long companyId) {
+        LiveTrainingCamp c = liveTrainingCampMapper.selectLiveTrainingCampById(campId);
+        if (c == null) {
+            return null;
+        }
+        if (companyId != null && !companyId.equals(c.getCompanyId())) {
+            return null;
+        }
+        return c;
+    }
+
+    @Override
+    public int deleteLiveTrainingCampByIds(Long[] campIds, Long companyId, String createBy) {
+        if (campIds == null || campIds.length == 0 || companyId == null) {
+            return 0;
+        }
+        for (Long campId : campIds) {
+            int n = liveTrainingPeriodMapper.countPeriodByCampId(campId);
+            if (n > 0) {
+                throw new BaseException("训练营下仍有营期,无法删除");
+            }
+        }
+        return liveTrainingCampMapper.logicDeleteLiveTrainingCampByIds(campIds, companyId, createBy);
+    }
+}

+ 185 - 0
fs-service/src/main/java/com/fs/live/service/impl/LiveTrainingPeriodServiceImpl.java

@@ -0,0 +1,185 @@
+package com.fs.live.service.impl;
+
+import com.fs.common.exception.base.BaseException;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.mapper.CompanyUserMapper;
+import com.fs.live.domain.Live;
+import com.fs.live.domain.LiveTrainingCamp;
+import com.fs.live.domain.LiveTrainingPeriod;
+import com.fs.live.mapper.LiveMapper;
+import com.fs.live.mapper.LiveTrainingCampMapper;
+import com.fs.live.mapper.LiveTrainingPeriodMapper;
+import com.fs.live.service.ILiveService;
+import com.fs.live.service.ILiveTrainingPeriodService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Objects;
+
+@Service
+@RequiredArgsConstructor
+public class LiveTrainingPeriodServiceImpl implements ILiveTrainingPeriodService {
+
+    private final LiveTrainingPeriodMapper liveTrainingPeriodMapper;
+    private final LiveTrainingCampMapper liveTrainingCampMapper;
+    private final LiveMapper liveMapper;
+    private final ILiveService liveService;
+    private final CompanyUserMapper companyUserMapper;
+
+    @Override
+    public List<LiveTrainingPeriod> selectLiveTrainingPeriodList(LiveTrainingPeriod query) {
+        return liveTrainingPeriodMapper.selectLiveTrainingPeriodList(query);
+    }
+
+    @Override
+    public List<LiveTrainingPeriod> selectLiveTrainingPeriodListAdmin(LiveTrainingPeriod query) {
+        return liveTrainingPeriodMapper.selectLiveTrainingPeriodListAdmin(query);
+    }
+
+    @Override
+    public LiveTrainingPeriod selectLiveTrainingPeriodByIdForAdmin(Long periodId, Long companyId) {
+        LiveTrainingPeriod p = liveTrainingPeriodMapper.selectLiveTrainingPeriodById(periodId);
+        if (p == null || (p.getIsDel() != null && p.getIsDel() == 1)) {
+            return null;
+        }
+        LiveTrainingCamp camp = liveTrainingCampMapper.selectLiveTrainingCampById(p.getCampId());
+        if (camp == null || camp.getIsDel() != null && camp.getIsDel() == 1) {
+            return null;
+        }
+        if (companyId != null && !Objects.equals(companyId, camp.getCompanyId())) {
+            return null;
+        }
+        return p;
+    }
+
+    @Override
+    public LiveTrainingPeriod selectLiveTrainingPeriodById(Long periodId, Long companyId, String operatorUserName) {
+        LiveTrainingPeriod p = liveTrainingPeriodMapper.selectLiveTrainingPeriodById(periodId);
+        if (p == null) {
+            return null;
+        }
+        LiveTrainingCamp camp = liveTrainingCampMapper.selectLiveTrainingCampById(p.getCampId());
+        if (camp == null || !companyId.equals(camp.getCompanyId())) {
+            return null;
+        }
+        if (StringUtils.isNotEmpty(operatorUserName)
+                && (p.getCreateBy() == null || !operatorUserName.equals(p.getCreateBy()))) {
+            return null;
+        }
+        return p;
+    }
+
+    @Override
+    public int insertLiveTrainingPeriod(LiveTrainingPeriod period) {
+        LiveTrainingCamp camp = liveTrainingCampMapper.selectLiveTrainingCampById(period.getCampId());
+        if (camp == null || period.getCompanyId() == null || !period.getCompanyId().equals(camp.getCompanyId())) {
+            throw new BaseException("训练营不存在或无权限");
+        }
+        if (StringUtils.isEmpty(period.getCreateBy())
+                || camp.getCreateBy() == null
+                || !period.getCreateBy().equals(camp.getCreateBy())) {
+            throw new BaseException("只能在本人创建的训练营下新增营期");
+        }
+        if (period.getSortOrder() == null) {
+            period.setSortOrder(0);
+        }
+        if (period.getStatus() == null) {
+            period.setStatus(0);
+        }
+        period.setCreateTime(DateUtils.getNowDate());
+        return liveTrainingPeriodMapper.insertLiveTrainingPeriod(period);
+    }
+
+    @Override
+    public int updateLiveTrainingPeriod(LiveTrainingPeriod period) {
+        period.setUpdateTime(DateUtils.getNowDate());
+        return liveTrainingPeriodMapper.updateLiveTrainingPeriod(period);
+    }
+
+    @Override
+    public int deleteLiveTrainingPeriodByIds(Long[] periodIds, Long companyId, String createBy) {
+        if (periodIds == null || periodIds.length == 0 || companyId == null) {
+            return 0;
+        }
+        for (Long pid : periodIds) {
+            int n = liveMapper.countLiveByTrainingPeriodId(pid);
+            if (n > 0) {
+                throw new BaseException("营期下仍有直播间,无法删除");
+            }
+        }
+        return liveTrainingPeriodMapper.logicDeleteLiveTrainingPeriodByIds(periodIds, companyId, createBy);
+    }
+
+    @Override
+    public int insertTrainingLive(Live live, Long companyId, Long companyUserId, String operatorUserName) {
+        if (live.getTrainingPeriodId() == null) {
+            throw new BaseException("请选择营期");
+        }
+        LiveTrainingPeriod p = liveTrainingPeriodMapper.selectLiveTrainingPeriodById(live.getTrainingPeriodId());
+        if (p == null || p.getIsDel() != null && p.getIsDel() == 1) {
+            throw new BaseException("营期不存在");
+        }
+        LiveTrainingCamp camp = liveTrainingCampMapper.selectLiveTrainingCampById(p.getCampId());
+        if (camp == null || !companyId.equals(camp.getCompanyId())) {
+            throw new BaseException("无权限在该营期下创建直播间");
+        }
+        if (StringUtils.isEmpty(operatorUserName)
+                || p.getCreateBy() == null
+                || !operatorUserName.equals(p.getCreateBy())) {
+            throw new BaseException("无权限在该营期下创建直播间");
+        }
+        if (live.getStartTime() == null) {
+            throw new BaseException("请填写计划开播时间");
+        }
+        if (live.getLiveType() == null) {
+            live.setLiveType(1);
+        }
+        if (live.getLiveType() == 2 && StringUtils.isEmpty(live.getVideoUrl())) {
+            throw new BaseException("录播必须上传视频");
+        }
+        live.setCompanyId(companyId);
+        live.setCompanyUserId(companyUserId);
+        if (live.getIsShow() == null) {
+            live.setIsShow(1);
+        }
+        live.setIsAudit(0);
+        return liveService.insertLive(live);
+    }
+
+    @Override
+    public int insertTrainingLiveForAdmin(Live live, Long companyId) {
+        if (live.getTrainingPeriodId() == null || companyId == null) {
+            throw new BaseException("参数错误");
+        }
+        LiveTrainingPeriod p = liveTrainingPeriodMapper.selectLiveTrainingPeriodById(live.getTrainingPeriodId());
+        if (p == null || p.getIsDel() != null && p.getIsDel() == 1) {
+            throw new BaseException("营期不存在");
+        }
+        LiveTrainingCamp camp = liveTrainingCampMapper.selectLiveTrainingCampById(p.getCampId());
+        if (camp == null || !companyId.equals(camp.getCompanyId())) {
+            throw new BaseException("无权限在该营期下创建直播间");
+        }
+        if (live.getStartTime() == null) {
+            throw new BaseException("请填写计划开播时间");
+        }
+        if (live.getLiveType() == null) {
+            live.setLiveType(1);
+        }
+        if (live.getLiveType() == 2 && StringUtils.isEmpty(live.getVideoUrl())) {
+            throw new BaseException("录播必须上传视频");
+        }
+        live.setCompanyId(companyId);
+        List<CompanyUser> users = companyUserMapper.selectCompanyUserByCompanyId(companyId);
+        if (users != null && !users.isEmpty()) {
+            live.setCompanyUserId(users.get(0).getUserId());
+        }
+        if (live.getIsShow() == null) {
+            live.setIsShow(1);
+        }
+        live.setIsAudit(0);
+        return liveService.insertLive(live);
+    }
+}

+ 34 - 0
fs-service/src/main/java/com/fs/live/vo/TrainingLiveAuditVO.java

@@ -0,0 +1,34 @@
+package com.fs.live.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 总后台:训练营直播间审核列表
+ */
+@Data
+public class TrainingLiveAuditVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Long liveId;
+    private String liveName;
+    private Long companyId;
+    private String companyName;
+    private Long trainingPeriodId;
+    private String periodName;
+    private String campName;
+    /** 0待审核 1已通过 2已驳回 */
+    private Integer isAudit;
+    private Integer status;
+    private Integer liveType;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+}

+ 12 - 7
fs-service/src/main/resources/mapper/company/CompanyWithdrawDetailMapper.xml

@@ -73,10 +73,15 @@
                         OR (l.logs_type = 4 AND l.money &lt; 0)
                     )
                 THEN
-                    CASE a.service_type
-                        WHEN 0 THEN '仅退款'
-                        WHEN 1 THEN '退货退款'
-                        ELSE '-'
+                    CASE IFNULL(a.status, -1)
+                        WHEN 0 THEN '用户提交售后'
+                        WHEN 1 THEN '平台已审核'
+                        WHEN 2 THEN '用户已发货'
+                        WHEN 3 THEN '仓库已审核'
+                        WHEN 4 THEN '财务已审核'
+                        WHEN 5 THEN '用户取消售后'
+                        WHEN 6 THEN '平台取消售后'
+                        ELSE CAST(a.status AS CHAR)
                     END
                 ELSE '-'
             END AS afterSalesStatusText,
@@ -113,10 +118,10 @@
         LEFT JOIN company_user cu ON cu.user_id = o.company_user_id
         LEFT JOIN company_user cul ON cul.user_id = lo.company_user_id
         LEFT JOIN fs_store_payment_scrm p ON (
-            p.business_code = o.order_code AND p.status = 1 AND IFNULL(l.type, 0) = 0
+            p.business_code = o.order_code  AND IFNULL(l.type, 0) = 0
         )
         LEFT JOIN live_order_payment lop ON (
-            lop.business_code = lo.order_code AND lop.status = 1 AND IFNULL(l.type, 0) = 1
+            lop.business_code = lo.order_code  AND IFNULL(l.type, 0) = 1
         )
         LEFT JOIN (
             SELECT a1.*
@@ -124,7 +129,7 @@
             INNER JOIN (
                 SELECT order_code AS oc, MAX(id) AS mid
                 FROM fs_store_after_sales_scrm
-                WHERE IFNULL(is_del, 0) = 0 AND status = 3
+                WHERE IFNULL(is_del, 0) = 0
                 GROUP BY order_code
             ) am ON a1.id = am.mid
         ) a ON a.order_code = COALESCE(o.order_code, lo.order_code)

+ 57 - 0
fs-service/src/main/resources/mapper/live/LiveCourseQuestionRelMapper.xml

@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveCourseQuestionRelMapper">
+
+    <select id="selectLinkedByLiveId" resultType="com.fs.live.vo.LiveQuestionLiveVO">
+        select r.rel_id as id,
+               qb.title,
+               qb.type,
+               qb.create_time as createTime,
+               qb.create_by as createBy
+        from live_course_question_rel r
+        inner join fs_course_question_bank qb on qb.id = r.question_bank_id
+        where r.live_id = #{liveId}
+        order by r.rel_id desc
+    </select>
+
+    <!-- 可选:尚未关联到本直播间的课程题库试题(仅启用状态) -->
+    <select id="selectOptionQuestionBank" resultType="com.fs.live.vo.LiveQuestionLiveVO">
+        select qb.id,
+               qb.title,
+               qb.type,
+               qb.create_time as createTime,
+               qb.create_by as createBy
+        from fs_course_question_bank qb
+        where qb.status = 1
+          and qb.id not in (
+              select r.question_bank_id from live_course_question_rel r where r.live_id = #{liveId}
+          )
+        <if test="title != null and title != ''">
+            and qb.title like concat('%', #{title}, '%')
+        </if>
+        <if test="type != null">
+            and qb.type = #{type}
+        </if>
+        order by qb.sort asc, qb.id desc
+    </select>
+
+    <insert id="insertIgnore">
+        INSERT IGNORE INTO live_course_question_rel (live_id, question_bank_id)
+        VALUES (#{liveId}, #{questionBankId})
+    </insert>
+
+    <delete id="deleteByRelIds">
+        delete from live_course_question_rel
+        where live_id = #{liveId}
+        and rel_id in
+        <foreach collection="relIds" item="rid" open="(" separator="," close=")">#{rid}</foreach>
+    </delete>
+
+    <select id="selectQuestionBankIdByLiveAndRel" resultType="java.lang.Long">
+        select r.question_bank_id
+        from live_course_question_rel r
+        where r.live_id = #{liveId}
+          and r.rel_id = #{relId}
+        limit 1
+    </select>
+</mapper>

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

@@ -8,6 +8,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="liveId"    column="live_id"    />
         <result property="companyId"    column="company_id"    />
         <result property="companyUserId"    column="company_user_id"    />
+        <result property="trainingPeriodId"    column="training_period_id"    />
         <result property="talentId"    column="talent_id"    />
         <result property="liveName"    column="live_name"    />
         <result property="liveDesc"    column="live_desc"    />
@@ -36,7 +37,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectLiveVo">
-        select live_id, company_id, company_user_id,talent_id, live_name, is_audit, live_desc, show_type, status, anchor_id, live_type, start_time, finish_time,
+        select live_id, company_id, company_user_id, training_period_id, talent_id, live_name, is_audit, live_desc, show_type, status, anchor_id, live_type, start_time, finish_time,
                live_img_url, live_config, id_card_url, is_show, is_del, qw_qr_code, rtmp_url, flv_hls_url,
                create_time, create_by, update_by, update_time, remark,config_json,global_visible from live
     </sql>
@@ -68,7 +69,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </select>
 
     <select id="selectLiveList" parameterType="com.fs.live.domain.Live" resultMap="LiveResult">
-        select a.live_id, a.company_id, a.company_user_id,talent_id, a.live_name, a.is_audit, a.live_desc, a.show_type, a.status, a.anchor_id,
+        select a.live_id, a.company_id, a.company_user_id, a.training_period_id, a.talent_id, a.live_name, a.is_audit, a.live_desc, a.show_type, a.status, a.anchor_id,
                a.live_type, a.start_time, a.finish_time, a.live_img_url, a.live_config, a.id_card_url, a.is_show, a.is_del, a.qw_qr_code, a.rtmp_url,
                a.flv_hls_url, a.create_time, a.create_by, a.update_by, a.update_time, a.remark,config_json, b.video_url,a.global_visible
                 ,c.live_code_url,IFNULL(d.company_name, '总台') AS company_name,b.file_size
@@ -80,6 +81,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             and c.company_user_id = #{companyUserId}
         left join company d on a.company_id = d.company_id
         where 1=1 and a.is_del = 0 and  b.video_type in (1,2)
+        <if test="excludeCampLive != null and excludeCampLive == true"> and a.training_period_id is null </if>
+        <if test="onlyTrainingCampLive != null and onlyTrainingCampLive == true"> and a.training_period_id is not null </if>
+        <if test="trainingPeriodId != null"> and a.training_period_id = #{trainingPeriodId}</if>
         <if test="companyId != null "> and (a.company_id = #{companyId} or a.company_id is null )</if>
         <if test="companyName != null and companyName != ''">
             and (
@@ -110,11 +114,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         order by create_time desc
     </select>
 
+    <select id="selectTrainingLiveAuditList" parameterType="com.fs.live.domain.Live" resultType="com.fs.live.vo.TrainingLiveAuditVO">
+        select a.live_id as liveId, a.live_name as liveName, a.company_id as companyId,
+               IFNULL(d.company_name,'总台') as companyName,
+               a.training_period_id as trainingPeriodId,
+               p.period_name as periodName, c.camp_name as campName,
+               a.is_audit as isAudit, a.status, a.live_type as liveType,
+               a.start_time as startTime, a.create_time as createTime
+        from live a
+        left join live_training_period p on a.training_period_id = p.period_id and p.is_del = 0
+        left join live_training_camp c on p.camp_id = c.camp_id and c.is_del = 0
+        left join company d on a.company_id = d.company_id
+        where a.is_del = 0 and a.training_period_id is not null
+        <if test="companyId != null">and a.company_id = #{companyId}</if>
+        <if test="isAudit != null">and a.is_audit = #{isAudit}</if>
+        <if test="liveName != null and liveName != ''">and a.live_name like concat('%', #{liveName}, '%')</if>
+        order by a.create_time desc
+    </select>
+
     <select id="selectLiveByLiveId" parameterType="Long" resultMap="LiveResult">
         <include refid="selectLiveVo"/>
         where live_id = #{liveId} and is_del = 0
     </select>
 
+    <select id="selectLiveByLiveIdAndCompanyId" resultMap="LiveResult">
+        <include refid="selectLiveVo"/>
+        where live_id = #{liveId} and is_del = 0
+        and (company_id = #{companyId} or company_id is null)
+    </select>
+
     <select id="selectLiveByLiveIdAndCompanyIdAndCompanyUserId" resultMap="LiveResult">
         <include refid="selectLiveVo"/>
         where live_id = #{liveId} and company_id = #{companyId} and company_user_id = #{companyUserId}
@@ -138,6 +166,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="companyId != null">company_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
+            <if test="trainingPeriodId != null">training_period_id,</if>
             <if test="talentId != null">talent_id,</if>
             <if test="liveName != null">live_name,</if>
             <if test="liveDesc != null">live_desc,</if>
@@ -167,6 +196,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="companyId != null">#{companyId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
+            <if test="trainingPeriodId != null">#{trainingPeriodId},</if>
             <if test="talentId != null">#{talentId},</if>
             <if test="liveName != null">#{liveName},</if>
             <if test="liveDesc != null">#{liveDesc},</if>
@@ -199,6 +229,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         update live
         <trim prefix="SET" suffixOverrides=",">
             <if test="talentId != null">talent_id = #{talentId},</if>
+            <if test="trainingPeriodId != null">training_period_id = #{trainingPeriodId},</if>
             <if test="liveName != null">live_name = #{liveName},</if>
             <if test="liveDesc != null">live_desc = #{liveDesc},</if>
             <if test="showType != null">show_type = #{showType},</if>
@@ -368,6 +399,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectLiveListNew" resultType="com.fs.live.domain.Live">
         <include refid="selectLiveVo"/>
         where is_audit = 1 and is_del = 0 and is_show = 1 and status != 3
+                               and training_period_id is null
                                and (company_id = #{companyId} or company_id is null)
         <if test="liveName != null">
             and live_name like concat('%',#{liveName},'%')

+ 83 - 0
fs-service/src/main/resources/mapper/live/LiveTrainingCampMapper.xml

@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveTrainingCampMapper">
+
+    <resultMap type="com.fs.live.domain.LiveTrainingCamp" id="LiveTrainingCampResult">
+        <id property="campId" column="camp_id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="campName" column="camp_name"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+        <result property="isDel" column="is_del"/>
+        <result property="description" column="description"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="companyName" column="company_name"/>
+    </resultMap>
+
+    <sql id="selectCampVo">
+        select camp_id, company_id, camp_name, sort_order, status, is_del, description,
+               create_by, create_time, update_by, update_time
+        from live_training_camp
+    </sql>
+
+    <select id="selectLiveTrainingCampById" resultMap="LiveTrainingCampResult">
+        <include refid="selectCampVo"/>
+        where camp_id = #{campId} and is_del = 0
+    </select>
+
+    <select id="selectLiveTrainingCampList" resultMap="LiveTrainingCampResult">
+        <include refid="selectCampVo"/>
+        where is_del = 0
+        <if test="companyId != null">and company_id = #{companyId}</if>
+        <if test="createBy != null and createBy != ''">and create_by = #{createBy}</if>
+        <if test="campName != null and campName != ''">and camp_name like concat('%', #{campName}, '%')</if>
+        <if test="status != null">and status = #{status}</if>
+        order by sort_order asc, camp_id desc
+    </select>
+
+    <select id="selectLiveTrainingCampListAdmin" resultMap="LiveTrainingCampResult">
+        select c.camp_id, c.company_id, co.company_name as company_name, c.camp_name, c.sort_order, c.status, c.is_del, c.description,
+               c.create_by, c.create_time, c.update_by, c.update_time
+        from live_training_camp c
+        left join company co on c.company_id = co.company_id
+        where c.is_del = 0
+        <if test="companyId != null">and c.company_id = #{companyId}</if>
+        <if test="campName != null and campName != ''">and c.camp_name like concat('%', #{campName}, '%')</if>
+        <if test="status != null">and c.status = #{status}</if>
+        order by c.sort_order asc, c.camp_id desc
+    </select>
+
+    <insert id="insertLiveTrainingCamp" useGeneratedKeys="true" keyProperty="campId">
+        insert into live_training_camp
+        (company_id, camp_name, sort_order, status, is_del, description, create_by, create_time)
+        values
+        (#{companyId}, #{campName}, #{sortOrder}, #{status}, 0, #{description}, #{createBy}, #{createTime})
+    </insert>
+
+    <update id="updateLiveTrainingCamp">
+        update live_training_camp
+        <set>
+            <if test="campName != null">camp_name = #{campName},</if>
+            <if test="sortOrder != null">sort_order = #{sortOrder},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="description != null">description = #{description},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            update_time = #{updateTime}
+        </set>
+        where camp_id = #{campId} and company_id = #{companyId} and is_del = 0
+        <if test="params != null and params.ownerCreateBy != null and params.ownerCreateBy != ''">
+            and create_by = #{params.ownerCreateBy}
+        </if>
+    </update>
+
+    <update id="logicDeleteLiveTrainingCampByIds">
+        update live_training_camp set is_del = 1, update_time = now()
+        where company_id = #{companyId}
+        <if test="createBy != null and createBy != ''">and create_by = #{createBy}</if>
+        and camp_id in
+        <foreach collection="campIds" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 97 - 0
fs-service/src/main/resources/mapper/live/LiveTrainingPeriodMapper.xml

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.live.mapper.LiveTrainingPeriodMapper">
+
+    <resultMap type="com.fs.live.domain.LiveTrainingPeriod" id="LiveTrainingPeriodResult">
+        <id property="periodId" column="period_id"/>
+        <result property="campId" column="camp_id"/>
+        <result property="periodName" column="period_name"/>
+        <result property="periodImgUrl" column="period_img_url"/>
+        <result property="startTime" column="start_time"/>
+        <result property="endTime" column="end_time"/>
+        <result property="sortOrder" column="sort_order"/>
+        <result property="status" column="status"/>
+        <result property="isDel" column="is_del"/>
+        <result property="description" column="description"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="campName" column="camp_name"/>
+    </resultMap>
+
+    <sql id="selectPeriodVo">
+        select p.period_id, p.camp_id, p.period_name, p.period_img_url, p.start_time, p.end_time, p.sort_order, p.status, p.is_del, p.description,
+               p.create_by, p.create_time, p.update_by, p.update_time,
+               c.camp_name
+        from live_training_period p
+        inner join live_training_camp c on p.camp_id = c.camp_id and c.is_del = 0
+    </sql>
+
+    <select id="selectLiveTrainingPeriodById" resultMap="LiveTrainingPeriodResult">
+        <include refid="selectPeriodVo"/>
+        where p.period_id = #{periodId} and p.is_del = 0
+    </select>
+
+    <select id="selectLiveTrainingPeriodList" resultMap="LiveTrainingPeriodResult">
+        <include refid="selectPeriodVo"/>
+        where p.is_del = 0
+        <if test="companyId != null">and c.company_id = #{companyId}</if>
+        <if test="createBy != null and createBy != ''">and p.create_by = #{createBy}</if>
+        <if test="campId != null">and p.camp_id = #{campId}</if>
+        <if test="periodName != null and periodName != ''">and p.period_name like concat('%', #{periodName}, '%')</if>
+        <if test="status != null">and p.status = #{status}</if>
+        order by p.sort_order asc, p.period_id desc
+    </select>
+
+    <select id="selectLiveTrainingPeriodListAdmin" resultMap="LiveTrainingPeriodResult">
+        <include refid="selectPeriodVo"/>
+        where p.is_del = 0
+        <if test="companyId != null">and c.company_id = #{companyId}</if>
+        <if test="campId != null">and p.camp_id = #{campId}</if>
+        <if test="periodName != null and periodName != ''">and p.period_name like concat('%', #{periodName}, '%')</if>
+        <if test="status != null">and p.status = #{status}</if>
+        order by p.sort_order asc, p.period_id desc
+    </select>
+
+    <select id="countPeriodByCampId" resultType="int">
+        select count(1) from live_training_period where is_del = 0 and camp_id = #{campId}
+    </select>
+
+    <insert id="insertLiveTrainingPeriod" useGeneratedKeys="true" keyProperty="periodId">
+        insert into live_training_period
+        (camp_id, period_name, period_img_url, start_time, end_time, sort_order, status, is_del, description, create_by, create_time)
+        values
+        (#{campId}, #{periodName}, #{periodImgUrl}, #{startTime}, #{endTime}, #{sortOrder}, #{status}, 0, #{description}, #{createBy}, #{createTime})
+    </insert>
+
+    <update id="updateLiveTrainingPeriod">
+        update live_training_period p
+        inner join live_training_camp c on p.camp_id = c.camp_id and c.is_del = 0
+        <set>
+            <if test="periodName != null">p.period_name = #{periodName},</if>
+            <if test="periodImgUrl != null">p.period_img_url = #{periodImgUrl},</if>
+            <if test="startTime != null">p.start_time = #{startTime},</if>
+            <if test="endTime != null">p.end_time = #{endTime},</if>
+            <if test="sortOrder != null">p.sort_order = #{sortOrder},</if>
+            <if test="status != null">p.status = #{status},</if>
+            <if test="description != null">p.description = #{description},</if>
+            <if test="updateBy != null">p.update_by = #{updateBy},</if>
+            p.update_time = #{updateTime}
+        </set>
+        where p.period_id = #{periodId} and c.company_id = #{companyId} and p.is_del = 0
+        <if test="params != null and params.ownerCreateBy != null and params.ownerCreateBy != ''">
+            and p.create_by = #{params.ownerCreateBy}
+        </if>
+    </update>
+
+    <update id="logicDeleteLiveTrainingPeriodByIds">
+        update live_training_period p
+        inner join live_training_camp c on p.camp_id = c.camp_id and c.is_del = 0
+        set p.is_del = 1, p.update_time = now()
+        where c.company_id = #{companyId}
+        <if test="createBy != null and createBy != ''">and p.create_by = #{createBy}</if>
+        and p.period_id in
+        <foreach collection="periodIds" item="id" open="(" separator="," close=")">#{id}</foreach>
+    </update>
+</mapper>

+ 49 - 0
fs-user-app/src/main/java/com/fs/app/controller/live/LiveQuizController.java

@@ -0,0 +1,49 @@
+package com.fs.app.controller.live;
+
+import com.fs.app.annotation.Login;
+import com.fs.app.controller.AppBaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.course.param.LiveQuizSubmitUParam;
+import com.fs.course.service.IFsCourseQuestionBankService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 直播答题(观众端)
+ */
+@Api(tags = "直播答题")
+@RestController
+@RequestMapping("/app/live/liveQuiz")
+public class LiveQuizController extends AppBaseController {
+
+    private static final Logger log = LoggerFactory.getLogger(LiveQuizController.class);
+
+    @Autowired
+    private IFsCourseQuestionBankService questionBankService;
+
+    @Login
+    @PostMapping("/submit")
+    @ApiOperation("直播答题提交:校验题目关联、标准答案;course.config 中关闭校验时跳过对错判断")
+    public R submit(@RequestBody LiveQuizSubmitUParam param) {
+        String uid = getUserId();
+        if (StringUtils.isEmpty(uid)) {
+            return R.error("未登录");
+        }
+        if (param == null) {
+            return R.error("参数不能为空");
+        }
+        param.setUserId(Long.parseLong(uid));
+        log.info("【直播答题】userId={} liveId={} relId={} questionBankId={} type={} keys={}",
+                uid, param.getLiveId(), param.getRelId(), param.getQuestionBankId(),
+                param.getType(), param.getAnswerKeys());
+        return questionBankService.submitLiveQuiz(param);
+    }
+}