|
@@ -4,18 +4,23 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.train.common.Result;
|
|
import com.train.common.Result;
|
|
|
import com.train.entity.TrainClass;
|
|
import com.train.entity.TrainClass;
|
|
|
import com.train.entity.TrainCourse;
|
|
import com.train.entity.TrainCourse;
|
|
|
|
|
+import com.train.entity.TrainEnrollment;
|
|
|
import com.train.entity.TrainUser;
|
|
import com.train.entity.TrainUser;
|
|
|
import com.train.mapper.TrainClassMapper;
|
|
import com.train.mapper.TrainClassMapper;
|
|
|
import com.train.mapper.TrainCourseMapper;
|
|
import com.train.mapper.TrainCourseMapper;
|
|
|
|
|
+import com.train.mapper.TrainEnrollmentMapper;
|
|
|
import com.train.mapper.TrainUserMapper;
|
|
import com.train.mapper.TrainUserMapper;
|
|
|
|
|
+import com.train.service.CertService;
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
import org.springframework.web.bind.annotation.RequestAttribute;
|
|
import org.springframework.web.bind.annotation.RequestAttribute;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RequestBody;
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
|
|
+import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
@@ -23,10 +28,16 @@ import java.util.Map;
|
|
|
/**
|
|
/**
|
|
|
* 训练营课程(L0/L1/L2/L3/L4)相关接口。
|
|
* 训练营课程(L0/L1/L2/L3/L4)相关接口。
|
|
|
*
|
|
*
|
|
|
- * - /api/course/list 列出所有课程
|
|
|
|
|
|
|
+ * - /api/course/list 列出所有课程(含按时间推导的有效状态 effectiveStatus)
|
|
|
* - /api/course/current 当前学员所修课程(按 user.class_id -> train_class.course_id 解析)
|
|
* - /api/course/current 当前学员所修课程(按 user.class_id -> train_class.course_id 解析)
|
|
|
|
|
+ * - /api/course/enter 学员从课程列表申请进班(置 enrollment reviewing,待管理员审核)
|
|
|
|
|
+ * - /api/course/progress 课程内学习进度(复用离场验收 6 项)
|
|
|
*
|
|
*
|
|
|
- * 注意:当前学员所修课程未写入 user,而是从 class_id 反查,因此响应里附带 class 信息。
|
|
|
|
|
|
|
+ * 有效状态规则(复用 status 字段):
|
|
|
|
|
+ * - status=draft → draft(管理员手动下架,学员端不可见)
|
|
|
|
|
+ * - status=finished → finished(管理员手动结束)
|
|
|
|
|
+ * - 配置了 start_time / end_time → 按当前时间推导 upcoming/active/finished(自动切换)
|
|
|
|
|
+ * - 未配置时间 → 回落 status 原值
|
|
|
*/
|
|
*/
|
|
|
@Tag(name = "课程", description = "训练营课程(L0/L1/.../L4)")
|
|
@Tag(name = "课程", description = "训练营课程(L0/L1/.../L4)")
|
|
|
@RestController
|
|
@RestController
|
|
@@ -42,11 +53,22 @@ public class CourseController {
|
|
|
@Resource
|
|
@Resource
|
|
|
private TrainUserMapper trainUserMapper;
|
|
private TrainUserMapper trainUserMapper;
|
|
|
|
|
|
|
|
- @Operation(summary = "列出所有课程")
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private TrainEnrollmentMapper trainEnrollmentMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private CertService certService;
|
|
|
|
|
+
|
|
|
|
|
+ @Operation(summary = "列出所有课程(含有效状态与我的进班状态)")
|
|
|
@PostMapping("/list")
|
|
@PostMapping("/list")
|
|
|
- public Result<List<TrainCourse>> list() {
|
|
|
|
|
|
|
+ public Result<List<TrainCourse>> list(@RequestAttribute("userId") Long userId) {
|
|
|
List<TrainCourse> courses = trainCourseMapper.selectList(
|
|
List<TrainCourse> courses = trainCourseMapper.selectList(
|
|
|
new LambdaQueryWrapper<TrainCourse>().orderByAsc(TrainCourse::getLevel));
|
|
new LambdaQueryWrapper<TrainCourse>().orderByAsc(TrainCourse::getLevel));
|
|
|
|
|
+ for (TrainCourse c : courses) {
|
|
|
|
|
+ c.setEffectiveStatus(effectiveStatus(c));
|
|
|
|
|
+ Map<String, Object> en = myEnrollmentStatus(userId, c.getId());
|
|
|
|
|
+ c.setMyStatus(en == null ? "none" : en.get("status").toString());
|
|
|
|
|
+ }
|
|
|
return Result.success(courses);
|
|
return Result.success(courses);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -70,14 +92,18 @@ public class CourseController {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (course == null) {
|
|
if (course == null) {
|
|
|
- // 兜底:未进班或课程未关联时,返回首个 active 课程(一般是 L0)
|
|
|
|
|
|
|
+ // 兜底:未进班或课程未关联时,返回首个非下架课程(一般是 L0)
|
|
|
course = trainCourseMapper.selectOne(
|
|
course = trainCourseMapper.selectOne(
|
|
|
new LambdaQueryWrapper<TrainCourse>()
|
|
new LambdaQueryWrapper<TrainCourse>()
|
|
|
- .eq(TrainCourse::getStatus, "active")
|
|
|
|
|
|
|
+ .ne(TrainCourse::getStatus, "draft")
|
|
|
.orderByAsc(TrainCourse::getLevel)
|
|
.orderByAsc(TrainCourse::getLevel)
|
|
|
.last("LIMIT 1"));
|
|
.last("LIMIT 1"));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (course == null) {
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ course.setEffectiveStatus(effectiveStatus(course));
|
|
|
data.put("course", course);
|
|
data.put("course", course);
|
|
|
if (trainClass != null) {
|
|
if (trainClass != null) {
|
|
|
data.put("classId", trainClass.getId());
|
|
data.put("classId", trainClass.getId());
|
|
@@ -86,6 +112,130 @@ public class CourseController {
|
|
|
data.put("classTime", trainClass.getTime());
|
|
data.put("classTime", trainClass.getTime());
|
|
|
data.put("classPlace", trainClass.getPlace());
|
|
data.put("classPlace", trainClass.getPlace());
|
|
|
}
|
|
}
|
|
|
|
|
+ // 学员在该课程下的报名/进班状态
|
|
|
|
|
+ data.put("enrollment", myEnrollmentStatus(userId, course.getId()));
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 学员从当前课程列表申请进班。
|
|
|
|
|
+ * 流程:已有 paid 报名单 → 置 reviewing(待管理员审核);无已支付报名单 → 引导先报名。
|
|
|
|
|
+ * 重复申请幂等:已是 reviewing/confirmed 直接返回当前状态。
|
|
|
|
|
+ */
|
|
|
|
|
+ @Operation(summary = "申请进班(从课程列表)")
|
|
|
|
|
+ @PostMapping("/enter")
|
|
|
|
|
+ public Result<Map<String, Object>> enter(@RequestBody Map<String, Object> body,
|
|
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
|
|
+ Object courseIdObj = body.get("courseId");
|
|
|
|
|
+ if (courseIdObj == null) {
|
|
|
|
|
+ return Result.error("缺少课程ID");
|
|
|
|
|
+ }
|
|
|
|
|
+ Long courseId = Long.valueOf(courseIdObj.toString());
|
|
|
|
|
+ TrainCourse course = trainCourseMapper.selectById(courseId);
|
|
|
|
|
+ if (course == null) {
|
|
|
|
|
+ return Result.error("课程不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("draft".equals(course.getStatus()) || "finished".equals(effectiveStatus(course))) {
|
|
|
|
|
+ return Result.error("该课程当前不可申请进班");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ TrainEnrollment en = latestEnrollmentOf(userId, courseId);
|
|
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
|
|
+ if (en == null) {
|
|
|
|
|
+ return Result.error("请先完成该课程报名并支付,再申请进班");
|
|
|
|
|
+ }
|
|
|
|
|
+ data.put("enrollmentId", en.getId());
|
|
|
|
|
+ switch (en.getStatus()) {
|
|
|
|
|
+ case "pending":
|
|
|
|
|
+ return Result.error("报名单尚未支付,请先完成支付再申请进班");
|
|
|
|
|
+ case "paid":
|
|
|
|
|
+ en.setStatus("reviewing");
|
|
|
|
|
+ en.setUpdatedAt(new Date());
|
|
|
|
|
+ trainEnrollmentMapper.updateById(en);
|
|
|
|
|
+ data.put("status", "reviewing");
|
|
|
|
|
+ data.put("message", "进班申请已提交,等待班主任审核");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ case "reviewing":
|
|
|
|
|
+ data.put("status", "reviewing");
|
|
|
|
|
+ data.put("message", "进班申请已提交,等待班主任审核");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ case "confirmed":
|
|
|
|
|
+ data.put("status", "confirmed");
|
|
|
|
|
+ data.put("message", "已进班");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ case "rejected":
|
|
|
|
|
+ // 被拒后可重新申请
|
|
|
|
|
+ en.setStatus("reviewing");
|
|
|
|
|
+ en.setUpdatedAt(new Date());
|
|
|
|
|
+ trainEnrollmentMapper.updateById(en);
|
|
|
|
|
+ data.put("status", "reviewing");
|
|
|
|
|
+ data.put("message", "已重新提交进班申请,等待班主任审核");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ default:
|
|
|
|
|
+ return Result.error("报名单状态异常,请联系客服");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Operation(summary = "课程内学习进度(离场验收 6 项)")
|
|
|
|
|
+ @PostMapping("/progress")
|
|
|
|
|
+ public Result<Map<String, Object>> progress(@RequestAttribute("userId") Long userId) {
|
|
|
|
|
+ List<Map<String, Object>> items = certService.checkItemsOf(userId);
|
|
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
|
|
+ data.put("items", items);
|
|
|
|
|
+ data.put("done", items.stream().filter(i -> Boolean.TRUE.equals(i.get("ok"))).count());
|
|
|
|
|
+ data.put("total", items.size());
|
|
|
return Result.success(data);
|
|
return Result.success(data);
|
|
|
}
|
|
}
|
|
|
-}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 有效状态推导:draft/finished 为管理员手动覆盖;配置时间则按时间自动切换;否则回落原值。
|
|
|
|
|
+ */
|
|
|
|
|
+ private String effectiveStatus(TrainCourse c) {
|
|
|
|
|
+ if (c == null) {
|
|
|
|
|
+ return "draft";
|
|
|
|
|
+ }
|
|
|
|
|
+ if ("draft".equals(c.getStatus()) || "finished".equals(c.getStatus())) {
|
|
|
|
|
+ return c.getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+ Date now = new Date();
|
|
|
|
|
+ if (c.getStartTime() != null && now.before(c.getStartTime())) {
|
|
|
|
|
+ return "upcoming";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (c.getEndTime() != null && now.after(c.getEndTime())) {
|
|
|
|
|
+ return "finished";
|
|
|
|
|
+ }
|
|
|
|
|
+ if (c.getStartTime() != null || c.getEndTime() != null) {
|
|
|
|
|
+ return "active";
|
|
|
|
|
+ }
|
|
|
|
|
+ return c.getStatus() == null ? "draft" : c.getStatus();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 学员在指定课程下的最新一条报名单(按该课程全部班次查找) */
|
|
|
|
|
+ private TrainEnrollment latestEnrollmentOf(Long uid, Long courseId) {
|
|
|
|
|
+ List<Long> classIds = trainClassMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<TrainClass>().eq(TrainClass::getCourseId, courseId))
|
|
|
|
|
+ .stream().map(TrainClass::getId).collect(java.util.stream.Collectors.toList());
|
|
|
|
|
+ if (classIds.isEmpty()) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return trainEnrollmentMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<TrainEnrollment>()
|
|
|
|
|
+ .eq(TrainEnrollment::getUid, uid)
|
|
|
|
|
+ .in(TrainEnrollment::getClassId, classIds)
|
|
|
|
|
+ .orderByDesc(TrainEnrollment::getId)
|
|
|
|
|
+ .last("LIMIT 1"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 学员在指定课程下的报名/进班状态:{enrollmentId, status} 或 null */
|
|
|
|
|
+ private Map<String, Object> myEnrollmentStatus(Long uid, Long courseId) {
|
|
|
|
|
+ TrainEnrollment en = latestEnrollmentOf(uid, courseId);
|
|
|
|
|
+ if (en == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
|
|
+ m.put("enrollmentId", en.getId());
|
|
|
|
|
+ m.put("classId", en.getClassId());
|
|
|
|
|
+ m.put("status", en.getStatus());
|
|
|
|
|
+ return m;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|