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()); + } +}