feat: 🎸 课程接口&用户登录

This commit is contained in:
GoCo 2025-08-13 04:46:50 +08:00
parent c08fc2bb79
commit 801e48b660
9 changed files with 396 additions and 12 deletions

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "automatic"
}

View File

@ -21,6 +21,11 @@
<groupId>org.jeecgframework.boot</groupId> <groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId> <artifactId>jeecg-system-local-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-biz</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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";
}
}

View File

@ -6,10 +6,12 @@ import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.DictModel;
import org.jeecg.common.system.vo.LoginUser; import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.config.shiro.IgnoreAuth; import org.jeecg.config.shiro.IgnoreAuth;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.common.system.api.ISysBaseAPI;
import org.jeecg.modules.biz.service.CourseBizService; import org.jeecg.modules.biz.service.CourseBizService;
import org.jeecg.modules.gen.course.entity.Course; 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.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -35,7 +41,7 @@ public class CourseBizController {
private ISysBaseAPI sysBaseApi; private ISysBaseAPI sysBaseApi;
@GetMapping("/list") @GetMapping("/list")
@Operation(summary = "根据分类、难度、专题查询课程列表") @Operation(summary = "查询课程列表", description = "根据分类、难度、专题进行检索,三个参数可任意传递其中之一、之二或全部,不传参则查询所有课程")
@IgnoreAuth @IgnoreAuth
public Result<List<Course>> queryCourseList( public Result<List<Course>> queryCourseList(
@RequestParam(value = "categoryId", required = false) String categoryId, @RequestParam(value = "categoryId", required = false) String categoryId,
@ -45,15 +51,72 @@ public class CourseBizController {
return Result.OK(list); return Result.OK(list);
} }
@GetMapping("/detail")
@Operation(summary = "查询课程详情", description = "根据课程ID查询课程详情")
@IgnoreAuth
public Result<Course> queryCourseDetail(@RequestParam(value = "id") String id) {
Course course = courseBizService.getById(id);
return Result.OK(course);
}
@GetMapping("/subject/list")
@Operation(summary = "查询课程专题列表", description = "返回字典值")
@IgnoreAuth
public Result<List<LabelValue>> querySubjectList() {
List<DictModel> list = sysBaseApi.getDictItems("course_subject");
List<LabelValue> 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<List<LabelValue>> queryDifficultyList() {
List<DictModel> list = sysBaseApi.getDictItems("course_difficulty");
List<LabelValue> 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<List<CourseCategory>> queryCategoryList() {
List<CourseCategory> 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<List<CourseSection>> querySectionList(@PathVariable(value = "courseId") String courseId) {
List<CourseSection> list = courseBizService.getCourseSectionList(courseId);
return Result.OK(list);
}
@GetMapping("/{courseId}/section_video/{sectionId}")
@Operation(summary = "查询视频章节详情", description = "该接口需要携带用户登录token。根据章节id查询章节详情不同类型的章节返回的内容不同")
public Result<List<Resource>> querySectionDetail(@PathVariable(value = "courseId") String courseId, @PathVariable(value = "sectionId") String sectionId) {
// TODO 获取用户id根据courseId判断当前用户是否报名课程只有已报名的课程才能查看章节详情
List<Resource> list = courseBizService.getCourseSectionDetail(0, sectionId, Resource.class);
return Result.OK(list);
}
@GetMapping("/test") @GetMapping("/test")
@Operation(summary = "测试")
@IgnoreAuth @IgnoreAuth
public Result<String> test() { public Result<String> test() {
return Result.OK("test"); return Result.OK("test");
} }
@GetMapping("/test2") @GetMapping("/test2")
@Operation(summary = "测试2")
public Result<String> test2(HttpServletRequest request, HttpServletResponse response) { public Result<String> test2(HttpServletRequest request, HttpServletResponse response) {
String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN);
String username = JwtUtil.getUsername(token); String username = JwtUtil.getUsername(token);
@ -62,7 +125,6 @@ public class CourseBizController {
} }
@GetMapping("/test3") @GetMapping("/test3")
@Operation(summary = "测试3")
@IgnoreAuth @IgnoreAuth
public Result<Long> test3() { public Result<Long> test3() {
long count = courseBizService.count(); long count = courseBizService.count();

View File

@ -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<JSONObject> login(@RequestBody Map<String, String> user, HttpServletRequest request) {
Result<JSONObject> result = new Result<JSONObject>();
String username = user.get("username");
String password = user.get("password");
if (isLoginFailOvertimes(username)) {
return result.error500("该用户登录失败次数过多请于10分钟后再次登录");
}
// step.2 校验用户是否存在且有效
LambdaQueryWrapper<SysUser> 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);
}
}

View File

@ -5,14 +5,16 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.jeecg.modules.gen.course.entity.Course; 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 org.springframework.web.multipart.MultipartFile;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
/** /**
* 课程业务 * 课程业务
*/ */
public interface CourseBizService extends IService<TestTable> { public interface CourseBizService extends IService<Course> {
/** /**
* 上传视频并切片为 HLSm3u8+ts按配置(local|minio|alioss)上传返回 m3u8 的路径/URL * 上传视频并切片为 HLSm3u8+ts按配置(local|minio|alioss)上传返回 m3u8 的路径/URL
@ -31,6 +33,28 @@ public interface CourseBizService extends IService<TestTable> {
* @return * @return
*/ */
List<Course> getCourseList(String categoryId, String difficulty, String topic); List<Course> getCourseList(String categoryId, String difficulty, String topic);
/**
* 查询课程分类列表
* @return
*/
List<CourseCategory> getCourseCategoryList();
/**
* 查询指定课程下的章节列表
* @param courseId
* @return
*/
List<CourseSection> getCourseSectionList(String courseId);
/**
* 查询章节详情泛型
* @param type 章节类型0=视频1=资料2=考试3=作业
* @param sectionId 章节ID
* @param clazz 期望返回的实体类型
* @return 指定类型的列表
*/
<T> List<T> getCourseSectionDetail(Integer type, String sectionId, Class<T> clazz);
} }

View File

@ -1,7 +1,17 @@
package org.jeecg.modules.biz.service; 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<EntityLink>{
/**
* 根据主体与内容类型查询绑定的 target_id 列表
* @param sourceType 主体类型
* @param sourceId 主体ID
* @param targetType 内容类型
* @return target_id 列表去重
*/
List<String> listTargetIds(String sourceType, String sourceId, String targetType);
} }

View File

@ -5,11 +5,16 @@ import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.MinioUtil; import org.jeecg.common.util.MinioUtil;
import org.jeecg.common.util.SpringContextUtils; import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oss.OssBootUtil; 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.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.entity.Course;
import org.jeecg.modules.gen.course.mapper.CourseMapper; 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.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@ -29,11 +34,102 @@ import java.util.stream.Stream;
@Slf4j @Slf4j
@Service @Service
public class CourseBizServiceImpl extends ServiceImpl<TestTableMapper, TestTable> implements CourseBizService { public class CourseBizServiceImpl extends ServiceImpl<CourseMapper, Course> implements CourseBizService {
@Autowired @Autowired
private CourseMapper courseMapper; private CourseMapper courseMapper;
@Autowired
private CourseCategoryMapper courseCategoryMapper;
@Autowired
private CourseSectionMapper courseSectionMapper;
@Autowired
private EntityLinkBizService entityLinkBizService;
@Autowired
private ResourceMapper resourceMapper;
@Override
public <T> List<T> getCourseSectionDetail(Integer type, String sectionId, Class<T> 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<String> targetIds = entityLinkBizService.listTargetIds(sourceType, sourceId, targetType);
if (targetIds.isEmpty()) {
throw new RuntimeException("章节没有关联的实体");
}
// 3. 根据实体id查询实体表获取实体详情
List<T> 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<CourseSection> getCourseSectionList(String courseId) {
return courseSectionMapper.selectList(new QueryWrapper<CourseSection>().eq("course_id", courseId));
}
@Override
public List<CourseCategory> getCourseCategoryList() {
return courseCategoryMapper.selectList(null);
}
@Override @Override
public List<Course> getCourseList(String categoryId, String difficulty, String topic) { public List<Course> getCourseList(String categoryId, String difficulty, String topic) {
QueryWrapper<Course> queryWrapper = new QueryWrapper<>(); QueryWrapper<Course> queryWrapper = new QueryWrapper<>();

View File

@ -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<EntityLinkMapper, EntityLink> implements EntityLinkBizService {
@Override
public List<String> listTargetIds(String sourceType, String sourceId, String targetType) {
LambdaQueryWrapper<EntityLink> 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());
}
}