Merge pull request #1 from PlayEdu/dev

v1.0-beta.4
This commit is contained in:
Teng 2023-05-04 15:22:11 +08:00 committed by GitHub
commit b8f06a3bdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 355 additions and 37 deletions

View File

@ -10,7 +10,7 @@
</parent>
<groupId>xyz.playedu</groupId>
<artifactId>playedu-api</artifactId>
<version>0.1-beta.1</version>
<version>1.0-beta.4</version>
<name>playedu-api</name>
<description>playedu-api</description>
<properties>

View File

@ -60,6 +60,7 @@ public class UserBus {
return CollectionUtils.intersection(courseDepIds, userDepIds).size() > 0;
}
// 注意调用该方法需要考虑到并发写入问题
public void userLearnDurationRecord(User user, Course course, CourseHour hour) {
Long curTime = System.currentTimeMillis();

View File

@ -162,6 +162,15 @@ public class AdminPermissionCheck implements ApplicationRunner {
setSlug(BPermissionConstant.USER_LEARN);
}
},
new AdminPermission() {
{
setSort(50);
setName("学习-删除");
setSlug(
BPermissionConstant
.USER_LEARN_DESTROY);
}
},
});
// 线上课
put(

View File

@ -42,6 +42,7 @@ public class BPermissionConstant {
public static final String USER_UPDATE = "user-update";
public static final String USER_DESTROY = "user-destroy";
public static final String USER_LEARN = "user-learn";
public static final String USER_LEARN_DESTROY = "user-learn-destroy";
public static final String COURSE = "course";
public static final String COURSE_USER = "course-user";

View File

@ -190,6 +190,9 @@ public class DepartmentController {
String idCard = MapUtils.getString(params, "id_card");
String depIds = String.valueOf(id);
String courseIdsStr = MapUtils.getString(params, "course_ids");
String showMode = MapUtils.getString(params, "show_mode");
UserPaginateFilter filter =
new UserPaginateFilter() {
{
@ -204,21 +207,48 @@ public class DepartmentController {
PaginationResult<User> users = userService.paginate(page, size, filter);
// 部门关联线上课
List<Course> courses =
courseService.getDepCoursesAndShow(
new ArrayList<>() {
{
add(id);
}
});
List<Course> courses;
if (courseIdsStr != null && courseIdsStr.trim().length() > 0) {
// 指定了需要显示的线上课
courses =
courseService.chunks(
Arrays.stream(courseIdsStr.split(",")).map(Integer::valueOf).toList());
} else {
if ("only_open".equals(showMode)) {
// 公开(无关联部门)线上课
courses = courseService.getOpenCoursesAndShow(10000);
} else if ("only_dep".equals(showMode)) {
// 部门关联线上课
courses =
courseService.getDepCoursesAndShow(
new ArrayList<>() {
{
add(id);
}
});
} else {
// 部门关联线上课
courses =
courseService.getDepCoursesAndShow(
new ArrayList<>() {
{
add(id);
}
});
List<Course> openCourses = courseService.getOpenCoursesAndShow(10000);
;
if (openCourses != null) {
courses.addAll(openCourses);
}
}
}
List<Integer> courseIds = courses.stream().map(Course::getId).toList();
// 学员的课程学习进度
Map<Integer, List<UserCourseRecord>> userCourseRecords =
userCourseRecordService
.chunk(
users.getData().stream().map(User::getId).toList(),
courses.stream().map(Course::getId).toList())
.chunk(users.getData().stream().map(User::getId).toList(), courseIds)
.stream()
.collect(Collectors.groupingBy(UserCourseRecord::getUserId));
Map<Integer, Map<Integer, UserCourseRecord>> userCourseRecordsMap = new HashMap<>();

View File

@ -33,6 +33,8 @@ import xyz.playedu.api.constant.BPermissionConstant;
import xyz.playedu.api.constant.CConfig;
import xyz.playedu.api.constant.SystemConstant;
import xyz.playedu.api.domain.*;
import xyz.playedu.api.event.UserCourseHourRecordDestroyEvent;
import xyz.playedu.api.event.UserCourseRecordDestroyEvent;
import xyz.playedu.api.event.UserDestroyEvent;
import xyz.playedu.api.exception.NotFoundException;
import xyz.playedu.api.middleware.BackendPermissionMiddleware;
@ -78,6 +80,8 @@ public class UserController {
@Autowired private UserLearnDurationStatsService userLearnDurationStatsService;
@Autowired private ApplicationContext ctx;
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_INDEX)
@GetMapping("/index")
public JsonResponse index(@RequestParam HashMap<String, Object> params) {
@ -370,7 +374,7 @@ public class UserController {
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_LEARN)
@GetMapping("/{id}/learn-hours")
@SneakyThrows
public JsonResponse latestLearnHours(
public JsonResponse learnHours(
@PathVariable(name = "id") Integer id, @RequestParam HashMap<String, Object> params) {
Integer page = MapUtils.getInteger(params, "page", 1);
Integer size = MapUtils.getInteger(params, "size", 10);
@ -438,6 +442,79 @@ public class UserController {
return JsonResponse.data(data);
}
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_LEARN)
@GetMapping("/{id}/all-courses")
public JsonResponse allCourses(@PathVariable(name = "id") Integer id) {
// 读取学员关联的部门
List<Integer> depIds = userService.getDepIdsByUserId(id);
List<Department> departments = new ArrayList<>();
HashMap<Integer, List<Course>> depCourses = new HashMap<>();
List<Integer> courseIds = new ArrayList<>();
if (depIds != null && depIds.size() > 0) {
departments = departmentService.chunk(depIds);
depIds.forEach(
(depId) -> {
List<Course> tmpCourses =
courseService.getDepCoursesAndShow(
new ArrayList<>() {
{
add(depId);
}
});
depCourses.put(depId, tmpCourses);
if (tmpCourses != null && tmpCourses.size() > 0) {
courseIds.addAll(tmpCourses.stream().map(Course::getId).toList());
}
});
}
// 未关联部门课程
List<Course> openCourses = courseService.getOpenCoursesAndShow(1000);
if (openCourses != null && openCourses.size() > 0) {
courseIds.addAll(openCourses.stream().map(Course::getId).toList());
}
// 读取学员的线上课学习记录
List<UserCourseRecord> userCourseRecords = new ArrayList<>();
if (courseIds.size() > 0) {
userCourseRecords = userCourseRecordService.chunk(id, courseIds);
}
HashMap<String, Object> data = new HashMap<>();
data.put("open_courses", openCourses);
data.put("departments", departments);
data.put("dep_courses", depCourses);
data.put(
"user_course_records",
userCourseRecords.stream()
.collect(Collectors.toMap(UserCourseRecord::getCourseId, e -> e)));
return JsonResponse.data(data);
}
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_LEARN)
@GetMapping("/{id}/learn-course/{courseId}")
@SneakyThrows
public JsonResponse learnCourseDetail(
@PathVariable(name = "id") Integer id,
@PathVariable(name = "courseId") Integer courseId) {
// 读取线上课下的所有课时
List<CourseHour> hours = courseHourService.getHoursByCourseId(courseId);
// 读取学员的课时学习记录
List<UserCourseHourRecord> records = userCourseHourRecordService.getRecords(id, courseId);
HashMap<String, Object> data = new HashMap<>();
data.put("hours", hours);
data.put(
"learn_records",
records.stream()
.collect(Collectors.toMap(UserCourseHourRecord::getHourId, e -> e)));
return JsonResponse.data(data);
}
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_LEARN)
@GetMapping("/{id}/learn-stats")
@SneakyThrows
@ -484,4 +561,27 @@ public class UserController {
return JsonResponse.data(data);
}
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_LEARN_DESTROY)
@DeleteMapping("/{id}/learn-course/{courseId}")
@SneakyThrows
public JsonResponse destroyUserCourse(
@PathVariable(name = "id") Integer id,
@PathVariable(name = "courseId") Integer courseId) {
userCourseRecordService.destroy(id, courseId);
ctx.publishEvent(new UserCourseRecordDestroyEvent(this, id, courseId));
return JsonResponse.success();
}
@BackendPermissionMiddleware(slug = BPermissionConstant.USER_LEARN_DESTROY)
@DeleteMapping("/{id}/learn-course/{courseId}/hour/{hourId}")
@SneakyThrows
public JsonResponse destroyUserHour(
@PathVariable(name = "id") Integer id,
@PathVariable(name = "courseId") Integer courseId,
@PathVariable(name = "hourId") Integer hourId) {
userCourseHourRecordService.remove(id, courseId, hourId);
ctx.publishEvent(new UserCourseHourRecordDestroyEvent(this, id, courseId, hourId));
return JsonResponse.success();
}
}

View File

@ -28,11 +28,14 @@ import xyz.playedu.api.caches.UserCanSeeCourseCache;
import xyz.playedu.api.domain.*;
import xyz.playedu.api.request.frontend.CourseHourRecordRequest;
import xyz.playedu.api.service.CourseHourService;
import xyz.playedu.api.service.CourseService;
import xyz.playedu.api.service.ResourceService;
import xyz.playedu.api.service.UserCourseHourRecordService;
import xyz.playedu.api.types.JsonResponse;
import xyz.playedu.api.util.RedisDistributedLock;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
/**
* @Author 杭州白书科技有限公司
@ -43,6 +46,8 @@ import java.util.HashMap;
@RequestMapping("/api/v1/course/{courseId}/hour")
public class HourController {
@Autowired private CourseService courseService;
@Autowired private CourseHourService hourService;
@Autowired private ResourceService resourceService;
@ -55,6 +60,30 @@ public class HourController {
@Autowired private UserCanSeeCourseCache userCanSeeCourseCache;
@Autowired private CourseCache courseCache;
@Autowired private RedisDistributedLock redisDistributedLock;
@GetMapping("/{id}")
@SneakyThrows
public JsonResponse detail(
@PathVariable(name = "courseId") Integer courseId,
@PathVariable(name = "id") Integer id) {
Course course = courseService.findOrFail(courseId);
CourseHour courseHour = hourService.findOrFail(id, courseId);
UserCourseHourRecord userCourseHourRecord = null;
if (FCtx.getId() != null && FCtx.getId() > 0) {
// 学员已登录
userCourseHourRecord = userCourseHourRecordService.find(FCtx.getId(), courseId, id);
}
HashMap<String, Object> data = new HashMap<>();
data.put("course", course);
data.put("hour", courseHour);
data.put("user_hour_record", userCourseHourRecord);
return JsonResponse.data(data);
}
@GetMapping("/{id}/play")
@SneakyThrows
public JsonResponse play(
@ -83,13 +112,23 @@ public class HourController {
if (duration <= 0) {
return JsonResponse.error("duration参数错误");
}
User user = FCtx.getUser();
Course course = courseCache.findOrFail(courseId);
userCanSeeCourseCache.check(user, course, true);
CourseHour hour = hourService.findOrFail(id, courseId);
userCanSeeCourseCache.check(FCtx.getUser(), course, true);
// 获取锁
String lockKey = String.format("record:%d", FCtx.getId());
boolean tryLock = redisDistributedLock.tryLock(lockKey, 5, TimeUnit.SECONDS);
if (!tryLock) {
return JsonResponse.error("请稍后再试");
}
userCourseHourRecordService.storeOrUpdate(
user.getId(), course.getId(), hour.getId(), duration, hour.getDuration());
FCtx.getId(), course.getId(), hour.getId(), duration, hour.getDuration());
// 此处未考虑上面代码执行失败释放锁
redisDistributedLock.releaseLock(lockKey);
return JsonResponse.success();
}
@ -102,7 +141,19 @@ public class HourController {
Course course = courseCache.findOrFail(courseId);
CourseHour hour = hourService.findOrFail(id, courseId);
userCanSeeCourseCache.check(FCtx.getUser(), course, true);
// 获取锁
String lockKey = String.format("ping:%d", FCtx.getId());
boolean tryLock = redisDistributedLock.tryLock(lockKey, 5, TimeUnit.SECONDS);
if (!tryLock) {
return JsonResponse.error("请稍后再试");
}
userBus.userLearnDurationRecord(FCtx.getUser(), course, hour);
// 此处未考虑上面代码执行失败释放锁
redisDistributedLock.releaseLock(lockKey);
return JsonResponse.success();
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.event;
import lombok.Getter;
import lombok.Setter;
import org.springframework.context.ApplicationEvent;
import java.util.Date;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/4/23 14:48
*/
@Getter
@Setter
public class UserCourseHourRecordDestroyEvent extends ApplicationEvent {
private Integer userId;
private Integer courseId;
private Integer hourId;
private Date at;
public UserCourseHourRecordDestroyEvent(
Object source, Integer userId, Integer courseId, Integer hourId) {
super(source);
this.userId = userId;
this.courseId = courseId;
this.hourId = hourId;
this.at = new Date();
}
}

View File

@ -15,11 +15,8 @@
*/
package xyz.playedu.api.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import xyz.playedu.api.event.UserCourseHourFinishedEvent;
@ -33,7 +30,6 @@ import xyz.playedu.api.service.UserCourseRecordService;
* @create 2023/3/20 17:41
*/
@Component
@Slf4j
public class UserCourseHourFinishedListener {
@Autowired private UserCourseRecordService userCourseRecordService;
@ -42,20 +38,12 @@ public class UserCourseHourFinishedListener {
@Autowired private CourseHourService hourService;
@Async
@EventListener
public void userCourseProgressUpdate(UserCourseHourFinishedEvent evt) {
Integer hourCount = hourService.getCountByCourseId(evt.getCourseId());
Integer finishedCount =
userCourseHourRecordService.getFinishedHourCount(
evt.getUserId(), evt.getCourseId());
log.info(
"UserCourseHourFinishedListener courseId={} userId={} hourCount={}"
+ " finishedCount={}",
evt.getCourseId(),
evt.getUserId(),
hourCount,
finishedCount);
userCourseRecordService.storeOrUpdate(
evt.getUserId(), evt.getCourseId(), hourCount, finishedCount);
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2023 杭州白书科技有限公司
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package xyz.playedu.api.listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import xyz.playedu.api.event.UserCourseHourRecordDestroyEvent;
import xyz.playedu.api.service.UserCourseRecordService;
/**
* @Author 杭州白书科技有限公司
*
* @create 2023/4/23 14:51
*/
@Component
@Slf4j
public class UserCourseHourRecordDestroyListener {
@Autowired private UserCourseRecordService userCourseRecordService;
@EventListener
public void updateUserCourseRecord(UserCourseHourRecordDestroyEvent e) {
userCourseRecordService.decrease(e.getUserId(), e.getCourseId(), 1);
}
}

View File

@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import xyz.playedu.api.event.UserLearnCourseUpdateEvent;
@ -39,7 +38,6 @@ public class UserLearnCourseUpdateListener {
@Autowired private UserLearnDurationStatsService userLearnDurationStatsService;
@Async
@EventListener
public void storeLearnDuration(UserLearnCourseUpdateEvent event) {
// 观看时长统计

View File

@ -64,4 +64,6 @@ public interface DepartmentService extends IService<Department> {
Long total();
Map<Integer, Integer> getDepartmentsUserCount();
List<Department> chunk(List<Integer> ids);
}

View File

@ -49,6 +49,8 @@ public interface UserCourseHourRecordService extends IService<UserCourseHourReco
void remove(Integer userId, Integer courseId);
void remove(Integer userId, Integer courseId, Integer hourId);
List<UserCourseHourRecordCountMapper> getUserCourseHourCount(
Integer userId, List<Integer> courseIds, Integer isFinished);

View File

@ -43,7 +43,11 @@ public interface UserCourseRecordService extends IService<UserCourseRecord> {
void destroy(Integer courseId, List<Integer> ids);
void destroy(Integer userId, Integer courseId);
void removeByCourseId(Integer courseId);
List<UserCourseRecord> chunks(List<Integer> ids, List<String> fields);
void decrease(Integer userId, Integer courseId, int count);
}

View File

@ -214,6 +214,9 @@ public class CourseServiceImpl extends ServiceImpl<CourseMapper, Course> impleme
@Override
public List<Course> getDepCoursesAndShow(List<Integer> depIds) {
if (depIds == null || depIds.size() == 0) {
return new ArrayList<>();
}
List<Integer> courseIds = courseDepartmentService.getCourseIdsByDepIds(depIds);
if (courseIds == null || courseIds.size() == 0) {
return new ArrayList<>();

View File

@ -268,4 +268,12 @@ public class DepartmentServiceImpl extends ServiceImpl<DepartmentMapper, Departm
DepartmentsUserCountMapRes::getDepId,
DepartmentsUserCountMapRes::getTotal));
}
@Override
public List<Department> chunk(List<Integer> ids) {
if (ids == null || ids.size() == 0) {
return new ArrayList<>();
}
return list(query().getWrapper().in("id", ids));
}
}

View File

@ -81,7 +81,7 @@ public class ImageCaptchaServiceImpl implements ImageCaptchaService {
return false;
}
String cacheValue = (String) queryResult;
boolean verifyResult = cacheValue.equals(code);
boolean verifyResult = cacheValue.equalsIgnoreCase(code);
if (verifyResult) { // 验证成功删除缓存->防止多次使用
RedisUtil.del(cacheKey);

View File

@ -153,4 +153,13 @@ public class UserCourseHourRecordServiceImpl
return pageResult;
}
@Override
public void remove(Integer userId, Integer courseId, Integer hourId) {
remove(
query().getWrapper()
.eq("user_id", userId)
.eq("course_id", courseId)
.eq("hour_id", hourId));
}
}

View File

@ -60,7 +60,7 @@ public class UserCourseRecordServiceImpl
boolean isFinished = finishedCount >= hourCount;
Date finishedAt = isFinished ? new Date() : null;
Integer progress = finishedCount * 100 / hourCount * 100;
Integer progress = finishedCount * 10000 / hourCount;
if (record == null) {
UserCourseRecord insertRecord = new UserCourseRecord();
@ -132,4 +132,28 @@ public class UserCourseRecordServiceImpl
public List<UserCourseRecord> chunks(List<Integer> ids, List<String> fields) {
return list(query().getWrapper().in("id", ids).select(fields));
}
@Override
public void destroy(Integer userId, Integer courseId) {
remove(query().getWrapper().in("user_id", userId).eq("course_id", courseId));
}
@Override
public void decrease(Integer userId, Integer courseId, int count) {
UserCourseRecord record = find(userId, courseId);
if (record == null) {
return;
}
int finishedCount = record.getFinishedCount() - count;
UserCourseRecord newRecord = new UserCourseRecord();
newRecord.setId(record.getId());
newRecord.setFinishedCount(finishedCount);
newRecord.setFinishedAt(null);
newRecord.setProgress(finishedCount * 10000 / record.getHourCount());
newRecord.setIsFinished(0);
updateById(newRecord);
}
}

View File

@ -15,6 +15,8 @@
*/
package xyz.playedu.api.service.impl;
import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.SneakyThrows;
@ -42,10 +44,7 @@ public class UserLearnDurationStatsServiceImpl
@Override
@SneakyThrows
public void storeOrUpdate(Integer userId, Long startTime, Long endTime) {
// 处理日期
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
String date = simpleDateFormat.format(new Date(endTime));
// duration
String date = new DateTime().toDateStr();
Long duration = endTime - startTime;
UserLearnDurationStats stats =
@ -54,7 +53,7 @@ public class UserLearnDurationStatsServiceImpl
UserLearnDurationStats newStats = new UserLearnDurationStats();
newStats.setUserId(userId);
newStats.setDuration(duration);
newStats.setCreatedDate(simpleDateFormat.parse(date));
newStats.setCreatedDate(new DateTime(date));
save(newStats);
return;
}