diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..e0f15db2
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "automatic"
+}
\ No newline at end of file
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/pom.xml b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/pom.xml
index 60242de7..3d08b662 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/pom.xml
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/pom.xml
@@ -21,6 +21,11 @@
org.jeecgframework.boot
jeecg-system-local-api
+
+
+ org.jeecgframework.boot
+ jeecg-system-biz
+
\ No newline at end of file
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/constant/EntityLinkConst.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/constant/EntityLinkConst.java
new file mode 100644
index 00000000..bc31381e
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/constant/EntityLinkConst.java
@@ -0,0 +1,39 @@
+package org.jeecg.modules.biz.constant;
+
+/**
+ * entity_link 表类型常量定义
+ * source_type:主体类型
+ * target_type:内容类型
+ */
+public final class EntityLinkConst {
+
+ private EntityLinkConst() {}
+
+ /** 主体类型 */
+ public static final class SourceType {
+ private SourceType() {}
+ // 课程
+ public static final String COURSE = "course";
+ // 课程分类
+ public static final String COURSE_CATEGORY = "course_category";
+ // 课程章节
+ public static final String COURSE_SECTION = "course_section";
+ // 专题(字典/专题)
+ public static final String SUBJECT = "subject";
+ // 活动
+ public static final String ACTIVITY = "activity";
+ }
+
+ /** 内容类型 */
+ public static final class TargetType {
+ private TargetType() {}
+ // 资源(对应资源表)
+ public static final String RESOURCE = "resource";
+ // 作业(对应作业表)
+ public static final String HOMEWORK = "homework";
+ // 考试(对应考试表)
+ public static final String EXAM = "exam";
+ }
+}
+
+
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/CourseBizController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/CourseBizController.java
index 89189bd2..a1d00cf8 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/CourseBizController.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/CourseBizController.java
@@ -6,10 +6,12 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil;
+import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.config.shiro.IgnoreAuth;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -17,8 +19,12 @@ import org.springframework.web.bind.annotation.RestController;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.biz.service.CourseBizService;
import org.jeecg.modules.gen.course.entity.Course;
+import org.jeecg.modules.gen.coursecategory.entity.CourseCategory;
+import org.jeecg.modules.gen.coursesection.entity.CourseSection;
+import org.jeecg.modules.gen.resource.entity.Resource;
import java.util.List;
+import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -35,7 +41,7 @@ public class CourseBizController {
private ISysBaseAPI sysBaseApi;
@GetMapping("/list")
- @Operation(summary = "根据分类、难度、专题查询课程列表")
+ @Operation(summary = "查询课程列表", description = "可根据分类、难度、专题进行检索,三个参数可任意传递其中之一、之二或全部,不传参则查询所有课程")
@IgnoreAuth
public Result> queryCourseList(
@RequestParam(value = "categoryId", required = false) String categoryId,
@@ -45,15 +51,72 @@ public class CourseBizController {
return Result.OK(list);
}
+ @GetMapping("/detail")
+ @Operation(summary = "查询课程详情", description = "根据课程ID查询课程详情")
+ @IgnoreAuth
+ public Result queryCourseDetail(@RequestParam(value = "id") String id) {
+ Course course = courseBizService.getById(id);
+ return Result.OK(course);
+ }
+
+ @GetMapping("/subject/list")
+ @Operation(summary = "查询课程专题列表", description = "返回字典值")
+ @IgnoreAuth
+ public Result> querySubjectList() {
+ List list = sysBaseApi.getDictItems("course_subject");
+ List simple = list.stream()
+ .map(d -> new LabelValue(d.getValue(), d.getLabel() != null ? d.getLabel() : d.getText()))
+ .collect(Collectors.toList());
+ return Result.OK(simple);
+ }
+
+ /** 仅返回 value、label 的简单对象 */
+ private static record LabelValue(String value, String label) {
+ }
+
+ @GetMapping("/difficulty/list")
+ @Operation(summary = "查询课程难度列表", description = "返回字典值")
+ @IgnoreAuth
+ public Result> queryDifficultyList() {
+ List list = sysBaseApi.getDictItems("course_difficulty");
+ List simple = list.stream()
+ .map(d -> new LabelValue(d.getValue(), d.getLabel() != null ? d.getLabel() : d.getText()))
+ .collect(Collectors.toList());
+ return Result.OK(simple);
+ }
+
+ @GetMapping("/category/list")
+ @Operation(summary = "查询课程分类列表", description = "根据sortOrder降序排序")
+ @IgnoreAuth
+ public Result> queryCategoryList() {
+ List list = courseBizService.getCourseCategoryList();
+ return Result.OK(list);
+ }
+
+ @GetMapping("/{courseId}/section")
+ @Operation(summary = "查询课程章节列表", description = "根据课程id查询章节列表,type表示章节类型:0=视频、1=资料、2=考试、3=作业;level表示章节层级,0=一级章节、1=二级章节,通过parentId记录父子关系,前端需自行组织树形结构;当前层级的顺序通过sortOrder排序")
+ @IgnoreAuth
+ public Result> querySectionList(@PathVariable(value = "courseId") String courseId) {
+ List list = courseBizService.getCourseSectionList(courseId);
+ return Result.OK(list);
+ }
+
+ @GetMapping("/{courseId}/section_video/{sectionId}")
+ @Operation(summary = "查询视频章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情,不同类型的章节,返回的内容不同")
+ public Result> querySectionDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) {
+ // TODO 获取用户id,根据courseId判断当前用户是否报名课程,只有已报名的课程才能查看章节详情
+
+ List list = courseBizService.getCourseSectionDetail(0, sectionId, Resource.class);
+ return Result.OK(list);
+ }
+
@GetMapping("/test")
- @Operation(summary = "测试")
@IgnoreAuth
public Result test() {
return Result.OK("test");
}
@GetMapping("/test2")
- @Operation(summary = "测试2")
public Result test2(HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token);
@@ -62,7 +125,6 @@ public class CourseBizController {
}
@GetMapping("/test3")
- @Operation(summary = "测试3")
@IgnoreAuth
public Result test3() {
long count = courseBizService.count();
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/UserBizController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/UserBizController.java
new file mode 100644
index 00000000..1a2ba246
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/controller/UserBizController.java
@@ -0,0 +1,119 @@
+package org.jeecg.modules.biz.controller;
+
+import org.jeecg.config.shiro.IgnoreAuth;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+
+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;
+import org.jeecg.modules.system.entity.SysUser;
+import org.jeecg.modules.system.service.*;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.jeecg.common.api.vo.Result;
+import org.jeecg.common.constant.CommonConstant;
+import org.jeecg.common.system.util.JwtUtil;
+import org.jeecg.common.system.vo.LoginUser;
+import org.jeecg.common.util.PasswordUtil;
+import org.jeecg.common.util.RedisUtil;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.extern.slf4j.Slf4j;
+
+@Tag(name = "用户")
+@RestController
+@RequestMapping("/biz/user")
+@Slf4j
+public class UserBizController {
+
+ @Autowired
+ private ISysUserService sysUserService;
+ @Autowired
+ private RedisUtil redisUtil;
+
+ @PostMapping("/login")
+ @Operation(summary = "用户登录")
+ @IgnoreAuth
+ public Result login(@RequestBody Map user, HttpServletRequest request) {
+ Result result = new Result();
+ String username = user.get("username");
+ String password = user.get("password");
+ if (isLoginFailOvertimes(username)) {
+ return result.error500("该用户登录失败次数过多,请于10分钟后再次登录!");
+ }
+
+ // step.2 校验用户是否存在且有效
+ LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
+ queryWrapper.eq(SysUser::getUsername,username);
+ SysUser sysUser = sysUserService.getOne(queryWrapper);
+ result = sysUserService.checkUserIsEffective(sysUser);
+ if(!result.isSuccess()) {
+ return result;
+ }
+
+ // step.3 校验用户名或密码是否正确
+ String userpassword = PasswordUtil.encrypt(username, password, sysUser.getSalt());
+ String syspassword = sysUser.getPassword();
+ if (!syspassword.equals(userpassword)) {
+ addLoginFailOvertimes(username);
+ result.error500("用户名或密码错误");
+ return result;
+ }
+
+ // step.4 登录成功获取用户信息
+ JSONObject obj = new JSONObject(new LinkedHashMap<>());
+ //1.生成token
+ String token = JwtUtil.sign(username, syspassword);
+ // 设置token缓存有效时间
+ redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
+ redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME * 2 / 1000);
+ obj.put("token", token);
+
+ // TODO 查询用户信息
+
+ result.setResult(obj);
+ result.success("登录成功");
+ return result;
+ }
+
+ /**
+ * 登录失败超出次数5 返回true
+ *
+ * @param username
+ * @return
+ */
+ private boolean isLoginFailOvertimes(String username) {
+ String key = CommonConstant.LOGIN_FAIL + username;
+ Object failTime = redisUtil.get(key);
+ if (failTime != null) {
+ Integer val = Integer.parseInt(failTime.toString());
+ if (val > 5) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 记录登录失败次数
+ * @param username
+ */
+ private void addLoginFailOvertimes(String username){
+ String key = CommonConstant.LOGIN_FAIL + username;
+ Object failTime = redisUtil.get(key);
+ Integer val = 0;
+ if(failTime!=null){
+ val = Integer.parseInt(failTime.toString());
+ }
+ // 10分钟,一分钟为60s
+ redisUtil.set(key, ++val, 600);
+ }
+}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/CourseBizService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/CourseBizService.java
index d88f2457..828bddc5 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/CourseBizService.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/CourseBizService.java
@@ -5,14 +5,16 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.jeecg.modules.gen.course.entity.Course;
-import org.jeecg.modules.gen.test.entity.TestTable;
+import org.jeecg.modules.gen.coursecategory.entity.CourseCategory;
+import org.jeecg.modules.gen.coursesection.entity.CourseSection;
import org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.extension.service.IService;
+
/**
* 课程业务
*/
-public interface CourseBizService extends IService {
+public interface CourseBizService extends IService {
/**
* 上传视频并切片为 HLS(m3u8+ts),按配置(local|minio|alioss)上传,返回 m3u8 的路径/URL
@@ -31,6 +33,28 @@ public interface CourseBizService extends IService {
* @return
*/
List getCourseList(String categoryId, String difficulty, String topic);
+
+ /**
+ * 查询课程分类列表
+ * @return
+ */
+ List getCourseCategoryList();
+
+ /**
+ * 查询指定课程下的章节列表
+ * @param courseId
+ * @return
+ */
+ List getCourseSectionList(String courseId);
+
+ /**
+ * 查询章节详情(泛型)
+ * @param type 章节类型:0=视频、1=资料、2=考试、3=作业
+ * @param sectionId 章节ID
+ * @param clazz 期望返回的实体类型
+ * @return 指定类型的列表
+ */
+ List getCourseSectionDetail(Integer type, String sectionId, Class clazz);
}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/EntityLinkBizService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/EntityLinkBizService.java
index 2e2eac13..0321433e 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/EntityLinkBizService.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/EntityLinkBizService.java
@@ -1,7 +1,17 @@
package org.jeecg.modules.biz.service;
-import com.baomidou.mybatisplus.extension.service.IService;
+import org.jeecg.modules.gen.entitylink.entity.EntityLink;
-public interface EntityLinkBizService {
-
+import com.baomidou.mybatisplus.extension.service.IService;
+import java.util.List;
+
+public interface EntityLinkBizService extends IService{
+ /**
+ * 根据主体与内容类型查询绑定的 target_id 列表
+ * @param sourceType 主体类型
+ * @param sourceId 主体ID
+ * @param targetType 内容类型
+ * @return target_id 列表(去重)
+ */
+ List listTargetIds(String sourceType, String sourceId, String targetType);
}
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/CourseBizServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/CourseBizServiceImpl.java
index 7da2dcc7..c88ac20b 100644
--- a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/CourseBizServiceImpl.java
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/CourseBizServiceImpl.java
@@ -5,11 +5,16 @@ import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.MinioUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oss.OssBootUtil;
+import org.jeecg.modules.biz.constant.EntityLinkConst;
import org.jeecg.modules.biz.service.CourseBizService;
-import org.jeecg.modules.gen.test.mapper.TestTableMapper;
+import org.jeecg.modules.biz.service.EntityLinkBizService;
import org.jeecg.modules.gen.course.entity.Course;
import org.jeecg.modules.gen.course.mapper.CourseMapper;
-import org.jeecg.modules.gen.test.entity.TestTable;
+import org.jeecg.modules.gen.coursecategory.entity.CourseCategory;
+import org.jeecg.modules.gen.coursecategory.mapper.CourseCategoryMapper;
+import org.jeecg.modules.gen.coursesection.entity.CourseSection;
+import org.jeecg.modules.gen.coursesection.mapper.CourseSectionMapper;
+import org.jeecg.modules.gen.resource.mapper.ResourceMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@@ -29,11 +34,102 @@ import java.util.stream.Stream;
@Slf4j
@Service
-public class CourseBizServiceImpl extends ServiceImpl implements CourseBizService {
+public class CourseBizServiceImpl extends ServiceImpl implements CourseBizService {
@Autowired
private CourseMapper courseMapper;
+ @Autowired
+ private CourseCategoryMapper courseCategoryMapper;
+
+ @Autowired
+ private CourseSectionMapper courseSectionMapper;
+
+ @Autowired
+ private EntityLinkBizService entityLinkBizService;
+
+ @Autowired
+ private ResourceMapper resourceMapper;
+
+ @Override
+ public List getCourseSectionDetail(Integer type, String sectionId, Class clazz) {
+ // 1. 查询章节是否存在
+ // 2. 根据章节类型,查询entitylink表,获取关联的实体id
+ // 3. 根据实体id,查询实体表,获取实体详情
+ // 4. 返回实体详情
+
+ // 1. 查询章节是否存在
+ CourseSection section = courseSectionMapper.selectById(sectionId);
+ if (section == null) {
+ throw new RuntimeException("章节不存在");
+ }
+
+ // 2. 根据章节类型,查询entitylink表,获取关联的实体id
+ String sourceType = EntityLinkConst.SourceType.COURSE_SECTION;
+ String sourceId = section.getId();
+ String targetType = null;
+ // 和数据字典对应
+ // 视频和资料章节的区别在于资料可能存在多份资料,而视频只有一份
+ switch (type) {
+ case 0:
+ // 视频章节
+ targetType = EntityLinkConst.TargetType.RESOURCE;
+ break;
+ case 1:
+ // 资料章节
+ targetType = EntityLinkConst.TargetType.RESOURCE;
+ break;
+ case 2:
+ // 考试章节
+ targetType = EntityLinkConst.TargetType.EXAM;
+ break;
+ case 3:
+ // 作业章节
+ targetType = EntityLinkConst.TargetType.HOMEWORK;
+ break;
+ default:
+ break;
+ }
+ List targetIds = entityLinkBizService.listTargetIds(sourceType, sourceId, targetType);
+ if (targetIds.isEmpty()) {
+ throw new RuntimeException("章节没有关联的实体");
+ }
+
+ // 3. 根据实体id,查询实体表,获取实体详情
+ List result = new ArrayList<>();
+ for (String targetId : targetIds) {
+ switch (type) {
+ case 0:
+ // 视频章节
+ result.add(clazz.cast(resourceMapper.selectById(targetId)));
+ break;
+ case 1:
+ // 资料章节
+ result.add(clazz.cast(resourceMapper.selectById(targetId)));
+ break;
+ case 2:
+ // TODO 考试章节
+ break;
+ case 3:
+ // TODO 作业章节
+ break;
+ }
+ }
+
+ // 4. 返回实体详情
+ return result;
+ }
+
+ @Override
+ public List getCourseSectionList(String courseId) {
+ return courseSectionMapper.selectList(new QueryWrapper().eq("course_id", courseId));
+ }
+
+ @Override
+ public List getCourseCategoryList() {
+ return courseCategoryMapper.selectList(null);
+ }
+
@Override
public List getCourseList(String categoryId, String difficulty, String topic) {
QueryWrapper queryWrapper = new QueryWrapper<>();
diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/EntityLinkBizServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/EntityLinkBizServiceImpl.java
new file mode 100644
index 00000000..7f3a1906
--- /dev/null
+++ b/jeecg-boot/jeecg-boot-module/jeecg-module-learn/src/main/java/org/jeecg/modules/biz/service/impl/EntityLinkBizServiceImpl.java
@@ -0,0 +1,26 @@
+package org.jeecg.modules.biz.service.impl;
+
+import org.jeecg.modules.biz.service.EntityLinkBizService;
+import org.jeecg.modules.gen.entitylink.entity.EntityLink;
+import org.jeecg.modules.gen.entitylink.mapper.EntityLinkMapper;
+import org.springframework.stereotype.Service;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class EntityLinkBizServiceImpl extends ServiceImpl implements EntityLinkBizService {
+ @Override
+ public List listTargetIds(String sourceType, String sourceId, String targetType) {
+ LambdaQueryWrapper qw = new LambdaQueryWrapper<>();
+ qw.eq(EntityLink::getSourceType, sourceType)
+ .eq(EntityLink::getSourceId, sourceId)
+ .eq(EntityLink::getTargetType, targetType)
+ .select(EntityLink::getTargetId);
+ return this.list(qw).stream()
+ .map(EntityLink::getTargetId)
+ .distinct()
+ .collect(Collectors.toList());
+ }
+}