From 9d81114ac296d57535c13bae9df532f81ed7c625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=98=8A?= <948387529@qq.com> Date: Sat, 29 Nov 2025 18:46:05 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../playedu/api/PlayeduApiApplication.java | 1 + .../api/controller/ExceptionController.java | 6 +- .../backend/admin/LoginController.java | 20 +- .../backend/jc/BookChapterController.java | 172 ++++++++++++- .../backend/jc/JCResourceController.java | 58 ++++- .../backend/jc/TextbookController.java | 53 ++-- .../backend/system/LocalFileController.java | 3 + .../controller/frontend/UserController.java | 180 ++++++++++++-- .../playedu/api/interceptor/WebMvcConfig.java | 16 ++ .../xyz/playedu/jc/domain/JCResource.java | 5 + .../java/xyz/playedu/jc/domain/Textbook.java | 6 +- .../playedu/jc/domain/dto/BookChapterDTO.java | 38 +++ .../jc/domain/dto/TextbookRequestDTO.java | 2 +- .../jc/domain/dto/TextbookUserDTO.java | 58 +++++ .../jc/mapper/BookDepartmentUserMapper.java | 5 + .../jc/service/IBookChapterService.java | 33 +++ .../service/IBookDepartmentUserService.java | 2 + .../playedu/jc/service/ITextbookService.java | 6 + .../jc/service/JCIResourceService.java | 6 +- .../service/impl/BookChapterServiceImpl.java | 232 +++++++++++++++++- .../impl/BookDepartmentUserServiceImpl.java | 25 ++ .../service/impl/JCResourceServiceImpl.java | 89 ++++++- .../jc/service/impl/TextbookServiceImpl.java | 99 +++++++- .../mapper/jc/BookDepartmentUserMapper.xml | 19 +- .../ResourceTranscodeInfoServiceImpl.java | 2 +- 25 files changed, 1053 insertions(+), 83 deletions(-) create mode 100644 app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/BookChapterDTO.java create mode 100644 app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookUserDTO.java diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java index 7c1210d..e590b9d 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/PlayeduApiApplication.java @@ -24,6 +24,7 @@ import xyz.playedu.common.config.UniqueNameGeneratorConfig; public class PlayeduApiApplication { public static void main(String[] args) { + System.out.println("start"); // 添加编码设置 System.setProperty("spring.config.encoding", "UTF-8"); ApplicationContext context=SpringApplication.run(PlayeduApiApplication.class, args); diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/ExceptionController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/ExceptionController.java index ab6bdea..3a61c9f 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/ExceptionController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/ExceptionController.java @@ -44,7 +44,7 @@ public class ExceptionController { @ExceptionHandler(HttpMessageNotReadableException.class) public JsonResponse serviceExceptionHandler(HttpMessageNotReadableException e) { log.error(e.getMessage()); - return JsonResponse.error("参数为空", 406); + return JsonResponse.error("参数为空1", 406); } @ExceptionHandler(MethodArgumentNotValidException.class) @@ -62,13 +62,13 @@ public class ExceptionController { @ExceptionHandler(HttpRequestMethodNotSupportedException.class) public JsonResponse serviceExceptionHandler(HttpRequestMethodNotSupportedException e) { log.error(e.getMessage()); - return JsonResponse.error("请求method错误", 400); + return JsonResponse.error("请求method错误1", 400); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public JsonResponse serviceExceptionHandler(MethodArgumentTypeMismatchException e) { log.error(e.getMessage()); - return JsonResponse.error("请求错误", 400); + return JsonResponse.error("请求错误1", 400); } @ExceptionHandler(MissingServletRequestParameterException.class) diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/admin/LoginController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/admin/LoginController.java index 9a39e4b..0897b39 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/admin/LoginController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/admin/LoginController.java @@ -64,16 +64,16 @@ public class LoginController { } String limitKey = "admin-login-limit:" + loginRequest.getEmail(); - Long reqCount = rateLimiterService.current(limitKey, 3600L); - if (reqCount > 5 && !playEduConfig.getTesting()) { - Long exp = RedisUtil.ttlWithoutPrefix(limitKey); - String timeMsg = - exp > 60 - ? exp / 60 + MessageUtils.message("分钟") - : exp + MessageUtils.message("秒"); - String msg = MessageUtils.message("您的账号已被锁定,请") + timeMsg + MessageUtils.message("后重试"); - return JsonResponse.error(msg); - } +// Long reqCount = rateLimiterService.current(limitKey, 3600L); +// if (reqCount > 5 && !playEduConfig.getTesting()) { +// Long exp = RedisUtil.ttlWithoutPrefix(limitKey); +// String timeMsg = +// exp > 60 +// ? exp / 60 + MessageUtils.message("分钟") +// : exp + MessageUtils.message("秒"); +// String msg = MessageUtils.message("您的账号已被锁定,请") + timeMsg + MessageUtils.message("后重试"); +// return JsonResponse.error(msg); +// } String password = HelperUtil.MD5(loginRequest.getPassword() + adminUser.getSalt()).toLowerCase(); diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/BookChapterController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/BookChapterController.java index c5333b9..1d8735c 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/BookChapterController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/BookChapterController.java @@ -1,13 +1,40 @@ package xyz.playedu.api.controller.backend.jc; +import jnr.ffi.annotations.In; +import lombok.SneakyThrows; +import org.apache.commons.collections4.MapUtils; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; +import xyz.playedu.api.avro.DepartmentDestroyAvro; +import xyz.playedu.api.request.backend.DepartmentParentRequest; +import xyz.playedu.api.request.backend.DepartmentRequest; +import xyz.playedu.api.request.backend.DepartmentSortRequest; +import xyz.playedu.common.annotation.BackendPermission; +import xyz.playedu.common.annotation.Log; +import xyz.playedu.common.constant.BPermissionConstant; +import xyz.playedu.common.constant.BusinessTypeConstant; +import xyz.playedu.common.constant.CommonConstant; +import xyz.playedu.common.constant.TopicConstant; +import xyz.playedu.common.context.BCtx; +import xyz.playedu.common.domain.Department; +import xyz.playedu.common.domain.User; +import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.types.JsonResponse; +import xyz.playedu.common.util.DateUtil; +import xyz.playedu.common.util.HelperUtil; +import xyz.playedu.common.util.StringUtil; import xyz.playedu.jc.domain.BookChapter; +import xyz.playedu.jc.domain.dto.BookChapterDTO; import xyz.playedu.jc.domain.dto.ChapterSortDTO; import xyz.playedu.jc.domain.vo.ChapterTreeVO; import xyz.playedu.jc.service.IBookChapterService; +import xyz.playedu.jc.service.IBookDepartmentUserService; +import xyz.playedu.jc.service.ITextbookService; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -22,6 +49,10 @@ public class BookChapterController { @Autowired private IBookChapterService bookChapterService; + @Autowired + private ITextbookService textbookService; + @Autowired + private IBookDepartmentUserService bookDepartmentUserService; /** 获取某教材的章节平铺列表 */ @GetMapping("/list") @@ -56,11 +87,11 @@ public class BookChapterController { } /** 删除章节(可在 Service 里做子节点/内容校验) */ - @DeleteMapping("/{id}") - public JsonResponse delete(@PathVariable("id") Integer id) { - bookChapterService.removeChapter(id); - return JsonResponse.success(); - } +// @DeleteMapping("/{id}") +// public JsonResponse delete(@PathVariable("id") Integer id) { +// bookChapterService.removeChapter(id); +// return JsonResponse.success(); +// } /** 拖拽排序 / 批量调整层级 */ @PostMapping("/sort") @@ -69,4 +100,135 @@ public class BookChapterController { bookChapterService.sort(bookId, sorts); return JsonResponse.success(); } + + + @GetMapping("/create") + @Log(title = "章节-新建", businessType = BusinessTypeConstant.GET) + public JsonResponse create(@RequestParam("bookId") Integer bookId) { + HashMap data = new HashMap<>(); + // 查询所有的本地部门 + data.put("bookChapter", bookChapterService.groupByParentByBookId(bookId)); + return JsonResponse.data(data); + } + + @PostMapping("/create") + @Log(title = "章节-新建", businessType = BusinessTypeConstant.INSERT) + public JsonResponse store(@RequestBody @Validated BookChapterDTO req) + throws NotFoundException { + // 校验同级是否同名部门 + BookChapter existDepartment = + bookChapterService.chunkByNameAndParentId(req.getName(), req.getParentId()); + if (StringUtil.isNotNull(existDepartment)) { + return JsonResponse.error("章节名称已存在"); + } + BookChapter bookChapter = new BookChapter(); + BeanUtils.copyProperties(req, bookChapter); + bookChapterService.create(bookChapter); + return JsonResponse.success(); + } + + @GetMapping("/{id}") + @Log(title = "章节-编辑", businessType = BusinessTypeConstant.GET) + public JsonResponse edit(@PathVariable Integer id) throws NotFoundException { + BookChapter bookChapter = bookChapterService.findOrFail(id); + return JsonResponse.data(bookChapter); + } + + @PutMapping("/{id}") + @Log(title = "章节-编辑", businessType = BusinessTypeConstant.UPDATE) + public JsonResponse update(@PathVariable Integer id, @RequestBody BookChapterDTO req) + throws NotFoundException { + BookChapter bookChapter = bookChapterService.findOrFail(id); + // 校验同级是否同名部门 + BookChapter existDepartment = + bookChapterService.chunkByNameAndParentId(req.getName(), req.getParentId()); + if (StringUtil.isNotNull(existDepartment) && existDepartment.getId() != id) { + return JsonResponse.error("部门名称已存在"); + } + bookChapterService.update(bookChapter, req.getName(), req.getParentId(), req.getSort()); + return JsonResponse.success(); + } + + + @SneakyThrows + @DeleteMapping("/{id}") + public JsonResponse destroy(@PathVariable Integer id) throws NotFoundException { + BookChapter department = bookChapterService.findOrFail(id); + bookChapterService.destroy(department.getId()); + + + return JsonResponse.success(); + } + + + + @GetMapping("/index") + @Log(title = "章节后台-列表", businessType = BusinessTypeConstant.GET) + public JsonResponse index(@RequestParam HashMap params) { + Integer bookId = MapUtils.getInteger(params, "bookId"); + if (bookId == null && StringUtil.isNull(bookId)) { + return JsonResponse.error("请传入教材id"); + } + HashMap data = new HashMap<>(); + // 只返回树形章节结构 + data.put("chapters", bookChapterService.groupByParentByBookId(bookId)); + return JsonResponse.data(data); +// +// HashMap data = new HashMap<>(); +// data.put("departments", bookChapterService.groupByParentByFromScene(fromScene)); +// +// HashMap depUserCount = new HashMap<>(); +// List allDepartmentList = bookChapterService.allByFromScene(fromScene); +// if (StringUtil.isNotEmpty(allDepartmentList)) { +// for (BookChapter dep : allDepartmentList) { +// List depIds = new ArrayList<>(); +// depIds.add(dep.getId()); +// String parentChain = ""; +// if (StringUtil.isEmpty(dep.getChapterCode())) { +// parentChain = dep.getId() + ""; +// } else { +// parentChain = dep.getChapterCode() + "," + dep.getId(); +// } +// // 获取所有子部门ID +// List childDepartmentList = +// bookChapterService.getChildDepartmentsByParentChain( +// dep.getId(), parentChain); +// if (StringUtil.isNotEmpty(childDepartmentList)) { +// depIds.addAll(childDepartmentList.stream().map(BookChapter::getId).toList()); +// } +//// List departmentUserIds = bookChapterService.getUserIdsByDepIds(depIds); +//// depUserCount.put( +//// dep.getId(), departmentUserIds.stream().distinct().toList().size()); +// } +// } +//// data.put("dep_user_count", depUserCount); +//// data.put("user_total", userService.total()); +// return JsonResponse.data(data); + } + + + @PutMapping("/update/sort") + @Log(title = "章节-更新排序", businessType = BusinessTypeConstant.UPDATE) + public JsonResponse resort(@RequestBody @Validated DepartmentSortRequest req) { + bookChapterService.resetSort(req.getIds()); + return JsonResponse.success(); + } + + @PutMapping("/update/parent") + @Log(title = "章节-更新父级", businessType = BusinessTypeConstant.UPDATE) + public JsonResponse updateParent(@RequestBody @Validated DepartmentParentRequest req) + throws NotFoundException { + bookChapterService.changeParent(req.getId(), req.getParentId(), req.getIds()); + return JsonResponse.success(); + } + + + @GetMapping("/{id}/destroy") + @Log(title = "章节-批量删除", businessType = BusinessTypeConstant.DELETE) + public JsonResponse preDestroy(@PathVariable Integer id) { + HashMap data = new HashMap<>(); + data.put("children", bookChapterService.listByParentId(id)); + return JsonResponse.data(data); + } + } \ No newline at end of file diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/JCResourceController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/JCResourceController.java index a2e98b8..40023f4 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/JCResourceController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/JCResourceController.java @@ -3,12 +3,24 @@ package xyz.playedu.api.controller.backend.jc; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import xyz.playedu.common.config.ServerConfig; +import xyz.playedu.common.constant.CommonConstant; +import xyz.playedu.common.domain.Department; +import xyz.playedu.common.domain.User; +import xyz.playedu.common.domain.UserGroup; import xyz.playedu.common.types.JsonResponse; +import xyz.playedu.common.types.paginate.PaginationResult; +import xyz.playedu.common.util.StringUtil; +import xyz.playedu.common.util.StringUtils; +import xyz.playedu.course.constants.CourseConstant; +import xyz.playedu.jc.domain.BookDepartmentUser; import xyz.playedu.jc.domain.JCResource; +import xyz.playedu.jc.domain.Textbook; import xyz.playedu.jc.domain.dto.ResourcePageRequestDTO; import xyz.playedu.jc.service.JCIResourceService; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; @RestController @RequestMapping("/backend/v1/jc/resource") @@ -16,6 +28,28 @@ public class JCResourceController { @Autowired private JCIResourceService resourceService; + @Autowired + private ServerConfig serverConfig; + + @GetMapping("/index") + public JsonResponse index(@RequestParam HashMap params) { + + /** 调用服务层分页查询方法,Service 层已包含完整的分页信息 */ + PaginationResult result = resourceService.paginate(params); + HashMap data = new HashMap<>(); + data.put("data", result.getData()); + data.put("total", result.getTotal()); + + String url = serverConfig.getUrl(); + + result.getData() + .forEach(item -> item.setAllUrl(url+item.getPath())); + result.getData() + .forEach(item -> item.setUrl(url)); + + /** 直接返回 Service 层的结果 */ + return JsonResponse.data(data); + } @GetMapping("/list") public JsonResponse list(@RequestParam(value = "bookId", required = false) Integer bookId, @@ -50,9 +84,25 @@ public class JCResourceController { } /** 删除资源 */ - @DeleteMapping("/{id}") - public JsonResponse delete(@PathVariable("id") Integer id) { - resourceService.removeById(id); +// @DeleteMapping("/{id}") +// public JsonResponse delete(@PathVariable("id") Integer id) { +// resourceService.removeById(id); +// return JsonResponse.success(); +// } + + + /** 批量删除资源 */ + @DeleteMapping("/delIds") + public JsonResponse delete(@RequestParam(value = "idList", required = false) String idListStr) { + if (StringUtils.isNotBlank(idListStr)) { + List idList = Arrays.stream(idListStr.split(",")) + .map(String::trim) + .filter(str -> !str.isEmpty()) + .map(Integer::parseInt) + .collect(Collectors.toList()); + + resourceService.removeByIds(idList); + } return JsonResponse.success(); } diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java index 9ced289..9314fc3 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java @@ -1,6 +1,7 @@ package xyz.playedu.api.controller.backend.jc; import cn.hutool.core.util.ObjectUtil; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -8,8 +9,10 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import xyz.playedu.common.annotation.Log; +import xyz.playedu.common.config.ServerConfig; import xyz.playedu.common.constant.BusinessTypeConstant; import xyz.playedu.common.constant.CommonConstant; +import xyz.playedu.common.context.FCtx; import xyz.playedu.common.domain.Department; import xyz.playedu.common.domain.Group; import xyz.playedu.common.domain.User; @@ -17,11 +20,14 @@ import xyz.playedu.common.domain.UserGroup; import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.service.*; import xyz.playedu.common.types.JsonResponse; +import xyz.playedu.common.types.mapper.UserCourseHourRecordCourseCountMapper; import xyz.playedu.common.types.paginate.PaginationResult; import xyz.playedu.common.util.StringUtil; import xyz.playedu.course.constants.CourseConstant; import xyz.playedu.course.domain.Course; import xyz.playedu.course.domain.CourseDepartmentUser; +import xyz.playedu.course.domain.CourseHour; +import xyz.playedu.course.domain.UserCourseRecord; import xyz.playedu.jc.domain.BookDepartmentUser; import xyz.playedu.jc.domain.JCResource; import xyz.playedu.jc.domain.Textbook; @@ -31,10 +37,7 @@ import xyz.playedu.jc.service.ITextbookService; import xyz.playedu.jc.service.JCIResourceService; import xyz.playedu.knowledge.domain.KnowledgeMessages; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; /** @@ -58,7 +61,8 @@ public class TextbookController { @Autowired private UserService userService; @Autowired private GroupService groupService; - + @Autowired + private ServerConfig serverConfig; @GetMapping("/index") public JsonResponse index(@RequestParam HashMap params) { @@ -68,24 +72,16 @@ public class TextbookController { data.put("data", result.getData()); data.put("total", result.getTotal()); - // 课程封面资源ID - List rids = new ArrayList<>(); - rids.addAll(result.getData().stream().map(Textbook::getThumb).toList()); + String url = serverConfig.getUrl(); + + result.getData() + .forEach(item -> item.setAllUrl(url+item.getThumb())); + result.getData() + .forEach(item -> item.setUrl(url)); + + List bookIds = result.getData().stream().map(Textbook::getId).toList(); -// data.put("course_category_ids", courseService.getCategoryIdsGroup(courseIds)); -// data.put("categories", categoryService.id2name()); -// data.put("departments", departmentService.id2name()); - -// Map courseIdRecordCountMap = new HashMap<>(); -// doGetRecords(courseIds) -// .forEach( -// (key, value) -> { -// courseIdRecordCountMap.put(key, value.size()); -// }); -// data.put("records", courseIdRecordCountMap); - // 获取签名url - data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids)); // 指派范围 Map book_user_count = new HashMap<>(); @@ -203,13 +199,17 @@ public class TextbookController { public JsonResponse edit(@PathVariable(name = "id") Integer id) throws NotFoundException { Textbook textbook = textbookService.findOrFail(id); + String url = serverConfig.getUrl(); + textbook.setAllUrl(url + textbook.getThumb()); + textbook.setUrl(url); + HashMap data = new HashMap<>(); data.put("textbook", textbook); - List rids = new ArrayList<>(); - rids.add(textbook.getThumb()); - // 获取签名url - data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids)); +// List rids = new ArrayList<>(); +// rids.add(textbook.getThumb()); +// 获取签名url +// data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids)); // 指派范围 List courseDepartmentUserList = @@ -347,4 +347,7 @@ public class TextbookController { textbookService.updateById(textbook); return JsonResponse.success(); } + + + } \ No newline at end of file diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/system/LocalFileController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/system/LocalFileController.java index 5fbaf60..bbbf5a9 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/system/LocalFileController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/system/LocalFileController.java @@ -45,12 +45,15 @@ public class LocalFileController { // 上传并返回新文件名称 String fileName = FileUploadUtils.upload(filePath, file); try { + long size = file.getSize(); // 这里拿到文件大小,单位 bytes + String url = serverConfig.getUrl() + fileName; JSONObject ajax = new JSONObject(); ajax.put("url", url); ajax.put("path", fileName); ajax.put("newFileName", FileUploadUtils.getName(fileName)); ajax.put("originalFilename", file.getOriginalFilename()); + ajax.put("size", size); return JsonResponse.data(ajax); } catch (Exception e) { return JsonResponse.error(e.getMessage()); diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/UserController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/UserController.java index 7b43c89..d5e5645 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/UserController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/UserController.java @@ -5,9 +5,11 @@ package xyz.playedu.api.controller.frontend; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; + import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; + import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; @@ -17,12 +19,15 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import xyz.playedu.api.request.frontend.ChangePasswordRequest; +import xyz.playedu.common.annotation.Log; import xyz.playedu.common.caches.CurrentDepIdCache; import xyz.playedu.common.caches.LoginCreditCache; +import xyz.playedu.common.constant.BusinessTypeConstant; import xyz.playedu.common.constant.CommonConstant; import xyz.playedu.common.constant.FrontendConstant; import xyz.playedu.common.context.FCtx; import xyz.playedu.common.domain.*; +import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.exception.ServiceException; import xyz.playedu.common.service.*; import xyz.playedu.common.types.JsonResponse; @@ -40,6 +45,10 @@ import xyz.playedu.exam.service.ExamTaskDepartmentUserService; import xyz.playedu.exam.service.ExamTaskUserService; import xyz.playedu.exam.service.StudyTaskDepartmentUserService; import xyz.playedu.exam.service.StudyTaskUserService; +import xyz.playedu.jc.domain.Textbook; +import xyz.playedu.jc.domain.dto.TextbookUserDTO; +import xyz.playedu.jc.service.IBookChapterService; +import xyz.playedu.jc.service.ITextbookService; import xyz.playedu.resource.domain.Resource; import xyz.playedu.resource.service.ResourceService; import xyz.playedu.resource.service.UploadService; @@ -49,47 +58,68 @@ import xyz.playedu.resource.service.UploadService; @Slf4j public class UserController { - @Autowired private UserService userService; + @Autowired + private UserService userService; - @Autowired private DepartmentService departmentService; + @Autowired + private DepartmentService departmentService; - @Autowired private CourseService courseService; + @Autowired + private CourseService courseService; - @Autowired private CourseHourService hourService; + @Autowired + private CourseHourService hourService; - @Autowired private UserCourseRecordService userCourseRecordService; + @Autowired + private UserCourseRecordService userCourseRecordService; - @Autowired private UserCourseHourRecordService userCourseHourRecordService; + @Autowired + private UserCourseHourRecordService userCourseHourRecordService; - @Autowired private UserLearnDurationStatsService userLearnDurationStatsService; + @Autowired + private UserLearnDurationStatsService userLearnDurationStatsService; - @Autowired private UploadService uploadService; + @Autowired + private UploadService uploadService; - @Autowired private CertUserService certUserService; + @Autowired + private CertUserService certUserService; - @Autowired private CertService certService; + @Autowired + private CertService certService; - @Autowired private ExamTaskUserService examTaskUserService; + @Autowired + private ExamTaskUserService examTaskUserService; - @Autowired private StudyTaskUserService studyTaskUserService; + @Autowired + private StudyTaskUserService studyTaskUserService; - @Autowired private ResourceService resourceService; + @Autowired + private ResourceService resourceService; - @Autowired private UserUploadImageLogService userUploadImageLogService; + @Autowired + private UserUploadImageLogService userUploadImageLogService; - @Autowired private AppConfigService appConfigService; + @Autowired + private AppConfigService appConfigService; - @Autowired private CurrentDepIdCache currentDepIdCache; + @Autowired + private CurrentDepIdCache currentDepIdCache; - @Autowired private ExamTaskDepartmentUserService examTaskDepartmentUserService; + @Autowired + private ExamTaskDepartmentUserService examTaskDepartmentUserService; - @Autowired private StudyTaskDepartmentUserService studyTaskDepartmentUserService; + @Autowired + private StudyTaskDepartmentUserService studyTaskDepartmentUserService; - @Autowired private CreditRuleService creditRuleService; + @Autowired + private CreditRuleService creditRuleService; - @Autowired private UserCreditDetailService userCreditDetailService; + @Autowired + private UserCreditDetailService userCreditDetailService; - @Autowired private LoginCreditCache loginCreditCache; + @Autowired + private LoginCreditCache loginCreditCache; @GetMapping("/detail") public JsonResponse detail() { @@ -613,4 +643,112 @@ public class UserController { } return JsonResponse.data(data); } + + @Autowired + private ITextbookService textbookService; + + @GetMapping("/userByTextBook") + public JsonResponse userByTextBook(@RequestParam HashMap params) throws NotFoundException { + Integer depId = MapUtils.getInteger(params, "dep_id"); + if (depId == null || depId == 0) { + return JsonResponse.error("部门为空"); + } + +// Integer categoryId = MapUtils.getInteger(params, "category_id"); + + String name = MapUtils.getString(params, "name"); + String publishTime = MapUtils.getString(params, "publishTime"); + String major = MapUtils.getString(params, "major"); + + List userJoinDepIds = userService.getDepIdsByUserId(FCtx.getId()); + if (userJoinDepIds == null) { + return JsonResponse.error("当前学员未加入任何部门"); + } + if (!userJoinDepIds.contains(depId)) { + return JsonResponse.error("当前学员未加入所选择部门"); + } + + HashMap data = new HashMap<>(); + data.put("learn_course_records", new HashMap<>()); + + // 获取所有部门ID + List depIds = new ArrayList<>(); + depIds.add(depId); + Department department = departmentService.findOrFail(depId); + String parentChain = department.getParentChain(); + if (StringUtil.isNotEmpty(parentChain)) { + List parentChainList = + Arrays.stream(parentChain.split(",")).map(Integer::parseInt).toList(); + if (StringUtil.isNotEmpty(parentChainList)) { + depIds.addAll(parentChainList); + } + } + List userIds = new ArrayList<>(); + userIds.add(FCtx.getId()); + TextbookUserDTO textbookUserDTO = new TextbookUserDTO(); + textbookUserDTO.setUserIds(userIds); + textbookUserDTO.setDepIds(depIds); + textbookUserDTO.setPublishTime(publishTime); + textbookUserDTO.setMajor(major); + textbookUserDTO.setName(name); + // -------- 读取当前学员可以参加的课程 ---------- + List courses = new ArrayList<>(); + // 读取部门课 + List depCourses = + textbookService.getDepCoursesAndShow( + textbookUserDTO); + + // 汇总到一个list中 + if (depCourses != null && !depCourses.isEmpty()) { + courses.addAll(depCourses); + } + // 对结果进行去重、排序->按照id去重、排序时间倒序 + if (!courses.isEmpty()) { + courses = + courses.stream() + .collect( + Collectors.collectingAndThen( + Collectors.toCollection( + () -> + new TreeSet<>( + Comparator.comparing( + Textbook::getId))), + ArrayList::new)) + .stream() + .sorted( + Comparator.comparing( + (Textbook c) -> + c.getCreateTime() != null + ? c.getPublishTime() + : new Date(0)) + .reversed()) + .toList(); + } + + data.put("textbook", courses); + +// List courseIds = courses.stream().map(Textbook::getId).toList(); + + + return JsonResponse.data(data); + } + @Autowired + private IBookChapterService bookChapterService; + @GetMapping("/index") + @Log(title = "章节后台-列表", businessType = BusinessTypeConstant.GET) + public JsonResponse index(@RequestParam HashMap params) { + Integer bookId = MapUtils.getInteger(params, "bookId"); + if (bookId == null && StringUtil.isNull(bookId)) { + return JsonResponse.error("请传入教材id"); + } + HashMap data = new HashMap<>(); + // 只返回树形章节结构 + data.put("chapters", bookChapterService.groupByParentByBookId(bookId)); + return JsonResponse.data(data); + + } + + + + } diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/interceptor/WebMvcConfig.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/interceptor/WebMvcConfig.java index b84e201..2bf7ca8 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/interceptor/WebMvcConfig.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/interceptor/WebMvcConfig.java @@ -9,7 +9,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import xyz.playedu.common.config.PlatformConfig; +import xyz.playedu.common.util.FileUploadUtils; @Configuration @Slf4j @@ -36,4 +39,17 @@ public class WebMvcConfig implements WebMvcConfigurer { .allowedHeaders("*") .maxAge(1_296_000); } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + // profile = platform.config.profile,比如:/data/platform + String profile = PlatformConfig.getProfile(); + if (!profile.endsWith("/")) { + profile = profile + "/"; + } + + // /profile/** 访问的是磁盘上的 profile 目录 + registry.addResourceHandler(FileUploadUtils.RESOURCE_PREFIX + "/**") + .addResourceLocations("file:" + profile); + } } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/JCResource.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/JCResource.java index 1db3b6e..ca6e3ac 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/JCResource.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/JCResource.java @@ -75,4 +75,9 @@ public class JCResource { @TableField("tenant_id") private String tenantId; + + @TableField(exist = false) + private String url; + @TableField(exist = false) + private String allUrl; } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Textbook.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Textbook.java index e6c8719..4dfd672 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Textbook.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Textbook.java @@ -26,7 +26,7 @@ public class Textbook extends TenantBaseDO { /** 封面地址 */ @TableField("thumb") - private Integer thumb; + private String thumb; /** 简介 */ @TableField("short_desc") @@ -54,5 +54,9 @@ public class Textbook extends TenantBaseDO { /** 发布时间 */ @TableField("publish_time") private Date publishTime; + @TableField(exist = false) + private String url; + @TableField(exist = false) + private String allUrl; } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/BookChapterDTO.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/BookChapterDTO.java new file mode 100644 index 0000000..316f4c8 --- /dev/null +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/BookChapterDTO.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 杭州白书科技有限公司 + */ +package xyz.playedu.jc.domain.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import java.io.Serial; +import java.io.Serializable; + +/** + * @Author 杭州白书科技有限公司 + * + * @create 2023/2/19 10:42 + */ +@Data +public class BookChapterDTO implements Serializable { + @Serial private static final long serialVersionUID = 1L; + + @NotBlank(message = "name参数为空") + @Length(min = 1, max = 64, message = "名称长度在1-64个字符之间") + private String name; + + @JsonProperty("parentId") + @NotNull(message = "parentId参数为空") + private Integer parentId; + + @JsonProperty("bookId") + @NotNull(message = "bookId参数为空") + private Integer bookId; + + @NotNull(message = "sort参数为空") + private Integer sort; +} diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookRequestDTO.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookRequestDTO.java index 567fa73..8b9f1b9 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookRequestDTO.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookRequestDTO.java @@ -26,7 +26,7 @@ public class TextbookRequestDTO { /** 封面地址 */ @TableField("thumb") - private Integer thumb; + private String thumb; /** 简介 */ @TableField("short_desc") diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookUserDTO.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookUserDTO.java new file mode 100644 index 0000000..e398e03 --- /dev/null +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/dto/TextbookUserDTO.java @@ -0,0 +1,58 @@ +package xyz.playedu.jc.domain.dto; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +/** + * + * 对应表: + */ +@Data +public class TextbookUserDTO { + + +// /** 教材名称 */ +// @TableField("title") +// private String title; +// +// /** 封面地址 */ +// @TableField("thumb") +// private String thumb; + + /** 简介 */ +// @TableField("short_desc") +// private String shortDesc; + + /** 学科专业信息 */ + @TableField("major") + private String major; + +// /** 作者 */ +// @TableField("author") +// private String author; +// +// /** ISBN 或教材编号 */ +// @TableField("isbn") +// private String isbn; + + + /** 出版社 */ +// @TableField("publish_unit") +// private String publishUnit; + + /** 发布时间 */ + private String publishTime; + + private String name; + + private List depIds; + + private List userIds; + +} \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/mapper/BookDepartmentUserMapper.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/mapper/BookDepartmentUserMapper.java index e07140c..8b22a3c 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/mapper/BookDepartmentUserMapper.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/mapper/BookDepartmentUserMapper.java @@ -1,7 +1,12 @@ package xyz.playedu.jc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import xyz.playedu.course.domain.CourseDepartmentUser; import xyz.playedu.jc.domain.BookDepartmentUser; +import java.util.List; + public interface BookDepartmentUserMapper extends BaseMapper { + List chunksByDepIdsOrUserIdsOrGroupIds( + List depIds, List userIds, List groupIds); } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookChapterService.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookChapterService.java index 036539e..45a4562 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookChapterService.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookChapterService.java @@ -1,11 +1,14 @@ package xyz.playedu.jc.service; import com.baomidou.mybatisplus.extension.service.IService; +import xyz.playedu.common.domain.Department; +import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.jc.domain.BookChapter; import xyz.playedu.jc.domain.dto.ChapterSortDTO; import xyz.playedu.jc.domain.vo.ChapterTreeVO; import java.util.List; +import java.util.Map; /** * 教材章节 Service @@ -28,4 +31,34 @@ public interface IBookChapterService extends IService { /** 删除章节时做子节点/内容校验(需要的话) */ void removeChapter(Integer id); + +// List getUserIdsByDepIds(List depIds); + + + Map> groupByParentByBookId(Integer bookId); + + List allByFromScene(Integer fromScene); + List getChildDepartmentsByParentChain(Integer parentId, String parentChain); + void destroy(Integer id) throws NotFoundException; + + List listByParentId(Integer id); + + + void changeParent(Integer id, Integer parentId, List ids) throws NotFoundException; + + void resetSort(List ids); + + BookChapter findOrFail(Integer id) throws NotFoundException; + + String childrenParentChain(BookChapter bookChapter); + + void update(BookChapter bookChapter, String name, Integer parentId, Integer sort) + throws NotFoundException; + + BookChapter chunkByNameAndParentId(String name, Integer parentId); + + BookChapter create(BookChapter bookChapter) + throws NotFoundException; + + String compParentChain(Integer parentId) throws NotFoundException; } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookDepartmentUserService.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookDepartmentUserService.java index 9ca0091..cc7b1bd 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookDepartmentUserService.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IBookDepartmentUserService.java @@ -12,4 +12,6 @@ public interface IBookDepartmentUserService extends IService List chunksByBookIds(List bookIds); List chunksByCourseId(Integer courseId); + + List getCourseIdsByDepIdsOrUserIds(List depIds, List userIds); } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/ITextbookService.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/ITextbookService.java index 54f337e..4314e9c 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/ITextbookService.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/ITextbookService.java @@ -3,10 +3,13 @@ package xyz.playedu.jc.service; import com.baomidou.mybatisplus.extension.service.IService; import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.types.paginate.PaginationResult; +import xyz.playedu.course.domain.Course; import xyz.playedu.jc.domain.JCResource; import xyz.playedu.jc.domain.Textbook; +import xyz.playedu.jc.domain.dto.TextbookUserDTO; import java.util.HashMap; +import java.util.List; /** * 教材 Service @@ -17,4 +20,7 @@ public interface ITextbookService extends IService { Textbook findOrFail(Integer id) throws NotFoundException; + List getDepCoursesAndShow( + TextbookUserDTO textbookUserDTO); + } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/JCIResourceService.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/JCIResourceService.java index ff83fb5..d9f244a 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/JCIResourceService.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/JCIResourceService.java @@ -2,13 +2,17 @@ package xyz.playedu.jc.service; import com.baomidou.mybatisplus.extension.service.IService; import xyz.playedu.common.exception.NotFoundException; +import xyz.playedu.common.types.paginate.PaginationResult; import xyz.playedu.course.domain.Course; import xyz.playedu.jc.domain.JCResource; +import xyz.playedu.jc.domain.Textbook; +import java.util.HashMap; import java.util.List; import java.util.Map; public interface JCIResourceService extends IService { - Map chunksPreSignUrlByIds(List ids); + Map chunksPreSignUrlByIds(List ids); + PaginationResult paginate(HashMap params); } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookChapterServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookChapterServiceImpl.java index 4578119..6f3d4ff 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookChapterServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookChapterServiceImpl.java @@ -3,16 +3,17 @@ package xyz.playedu.jc.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; +import xyz.playedu.common.domain.Department; +import xyz.playedu.common.exception.NotFoundException; +import xyz.playedu.common.util.StringUtil; import xyz.playedu.jc.domain.BookChapter; import xyz.playedu.jc.domain.dto.ChapterSortDTO; import xyz.playedu.jc.domain.vo.ChapterTreeVO; import xyz.playedu.jc.mapper.BookChapterMapper; import xyz.playedu.jc.service.IBookChapterService; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** * 教材章节 Service 实现 @@ -95,4 +96,227 @@ public class BookChapterServiceImpl //todo 校验是否有子节点 / 内容 removeById(id); } + @Override + public Map> groupByParentByBookId(Integer bookId) { + return list(query().getWrapper().eq("book_id", bookId).orderByAsc("sort")) + .stream() + .collect(Collectors.groupingBy(BookChapter::getParentId)); + +// return list( +// query() +// .getWrapper() +// .eq(BookChapter::getBookId, bookId) // ✅ 插入条件 +// .orderByAsc(BookChapter::getParentId) +// ) +// .stream() +// .collect(Collectors.groupingBy(BookChapter::getParentId)); + } + + + +// @Override +// public List getUserIdsByDepIds(List depIds) { +// if (StringUtil.isEmpty(depIds)) { +// return new ArrayList<>(); +// } +// return list(query().getWrapper().in("dep_id", depIds)).stream() +// .map(UserDepartment::getUserId) +// .toList(); +// } + + @Override + public List allByFromScene(Integer fromScene) { + if (StringUtil.isNull(fromScene)) { + return list(query().getWrapper().orderByAsc("sort")); + } else { + return list(query().getWrapper().eq("from_scene", fromScene).orderByAsc("sort")); + } + } + + @Override + public List getChildDepartmentsByParentChain(Integer parentId, String parentChain) { + if (StringUtil.isEmpty(parentChain)) { + return new ArrayList<>(); + } + return list( + query().getWrapper() + .eq("parent_id", parentId) + .or() + .likeRight("parent_chain", parentChain + ",")); + } + + @Override + public void destroy(Integer id) throws NotFoundException { + BookChapter bookChapter = getById(id); + if (bookChapter != null) { + updateParentChain(bookChapter.getChapterCode(), childrenParentChain(bookChapter)); + removeById(bookChapter.getId()); + } + } + + + @Override + public List listByParentId(Integer id) { + return list(query().getWrapper().eq("parent_id", id).orderByAsc("sort")); + } + + @Override + public void changeParent(Integer id, Integer parentId, List ids) + throws NotFoundException { + BookChapter bookChapter = findOrFail(id); + update(bookChapter, bookChapter.getName(), parentId, bookChapter.getSort()); + // 重置排序 + resetSort(ids); + } + + @Override + public void resetSort(List ids) { + if (ids == null || ids.isEmpty()) { + return; + } + List departments = new ArrayList<>(); + int sortVal = 0; + for (Integer idItem : ids) { + Integer finalSortVal = ++sortVal; + departments.add( + new BookChapter() { + { + setId(idItem); + setSort(finalSortVal); + } + }); + } + updateBatchById(departments); + } + + + @Override + public void update(BookChapter bookChapter, String name, Integer parentId, Integer sort) + throws NotFoundException { + // 计算该部门作为其它子部门的parentChain值 + String childrenChainPrefix = childrenParentChain(bookChapter); + + BookChapter data = new BookChapter(); + data.setId(bookChapter.getId()); + data.setName(name); +// data.setFromScene(bookChapter.getFromScene()); + + if (!bookChapter.getParentId().equals(parentId)) { + data.setParentId(parentId); + if (parentId.equals(0)) { // 重置一级部门 + data.setChapterCode(""); + } else { + BookChapter parentBookChapter = findOrFail(parentId); + data.setChapterCode(childrenParentChain(parentBookChapter)); + } + } + if (!bookChapter.getSort().equals(sort)) { // 更换部门排序值 + data.setSort(sort); + } + + // 提交更换 + updateById(data); + + bookChapter = getById(bookChapter.getId()); + updateParentChain(childrenParentChain(bookChapter), childrenChainPrefix); + } + + + private void updateParentChain(String newChildrenPC, String oldChildrenPC) { + List children = + list(query().getWrapper().like("chapter_code", oldChildrenPC + "%")); + if (children.isEmpty()) { + return; + } + + ArrayList updateRows = new ArrayList<>(); + for (BookChapter tmpDepartment : children) { + BookChapter tmpUpdateDepartment = new BookChapter(); + tmpUpdateDepartment.setId(tmpDepartment.getId()); + + // parentChain计算 + String pc = newChildrenPC; + if (!tmpDepartment.getChapterCode().equals(oldChildrenPC)) { + pc = + tmpDepartment + .getChapterCode() + .replaceFirst( + oldChildrenPC + ",", + newChildrenPC.isEmpty() + ? newChildrenPC + : newChildrenPC + ','); + } + tmpUpdateDepartment.setChapterCode(pc); + + // parentId计算 + int parentId = 0; + if (pc != null && !pc.isEmpty()) { + String[] parentIds = pc.split(","); + parentId = Integer.parseInt(parentIds[parentIds.length - 1]); + } + tmpUpdateDepartment.setParentId(parentId); + + updateRows.add(tmpUpdateDepartment); + } + updateBatchById(updateRows); + } + + + /** + * 章节id查询 + * @param id + * @return + * @throws NotFoundException + */ + @Override + public BookChapter findOrFail(Integer id) throws NotFoundException { + BookChapter bookChapter = getById(id); + if (bookChapter == null) { + throw new NotFoundException("部门不存在"); + } + return bookChapter; + } + + + @Override + public String childrenParentChain(BookChapter bookChapter) { + String prefix = bookChapter.getId() + ""; + if (bookChapter.getChapterCode() != null && !bookChapter.getChapterCode().isEmpty()) { + prefix = bookChapter.getChapterCode() + "," + prefix; + } + return prefix; + } + + @Override + public BookChapter chunkByNameAndParentId(String name, Integer parentId) { + return getOne(query().getWrapper().eq("name", name).eq("parent_id", parentId)); + } + + @Override + public BookChapter create(BookChapter bookChapter) + throws NotFoundException { + String parentChain = ""; + if (bookChapter.getParentId() != 0) { + parentChain = compParentChain(bookChapter.getParentId()); + } + bookChapter.setChapterCode(parentChain); + save(bookChapter); + + return bookChapter; + } + + + @Override + public String compParentChain(Integer parentId) throws NotFoundException { + String parentChain = ""; + if (parentId != 0) { + BookChapter bookChapter = getById(parentId); + if (bookChapter == null) { + throw new NotFoundException("父级部门不存在"); + } + String pc = bookChapter.getChapterCode(); + parentChain = pc == null || pc.isEmpty() ? parentId + "" : pc + "," + parentId; + } + return parentChain; + } } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookDepartmentUserServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookDepartmentUserServiceImpl.java index ff7f235..f934535 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookDepartmentUserServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/BookDepartmentUserServiceImpl.java @@ -1,18 +1,25 @@ package xyz.playedu.jc.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import xyz.playedu.common.domain.UserGroup; +import xyz.playedu.common.service.UserGroupService; +import xyz.playedu.common.util.StringUtil; import xyz.playedu.course.domain.CourseDepartmentUser; import xyz.playedu.jc.domain.BookDepartmentUser; import xyz.playedu.jc.mapper.BookDepartmentUserMapper; import xyz.playedu.jc.service.IBookDepartmentUserService; +import java.util.ArrayList; import java.util.List; @Service public class BookDepartmentUserServiceImpl extends ServiceImpl implements IBookDepartmentUserService { + @Autowired + private UserGroupService userGroupService; @Override public void removeByBookId(Integer bookId) { @@ -29,4 +36,22 @@ public class BookDepartmentUserServiceImpl return list(query().getWrapper().eq("book_id", bookId)); } + @Override + public List getCourseIdsByDepIdsOrUserIds( + List depIds, List userIds) { + + List groupIds = new ArrayList<>(); + List userGroupList = userGroupService.chunksByUserIds(userIds); + if (StringUtil.isNotEmpty(userGroupList)) { + groupIds.addAll(userGroupList.stream().map(UserGroup::getGroupId).toList()); + } + List departmentUserList = + getBaseMapper().chunksByDepIdsOrUserIdsOrGroupIds(depIds, userIds, groupIds); + if (StringUtil.isEmpty(departmentUserList)) { + return new ArrayList<>(); + } + + return departmentUserList.stream().map(BookDepartmentUser::getBookId).toList(); + } + } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/JCResourceServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/JCResourceServiceImpl.java index 7f1f4f2..ef7f0d6 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/JCResourceServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/JCResourceServiceImpl.java @@ -1,22 +1,31 @@ package xyz.playedu.jc.service.impl; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.service.AppConfigService; +import xyz.playedu.common.types.paginate.PaginationResult; import xyz.playedu.common.util.S3Util; import xyz.playedu.common.util.StringUtil; import xyz.playedu.course.domain.Course; import xyz.playedu.jc.domain.JCResource; +import xyz.playedu.jc.domain.Textbook; import xyz.playedu.jc.mapper.JCResourceMapper; import xyz.playedu.jc.service.JCIResourceService; import xyz.playedu.resource.domain.Resource; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +@Slf4j @Service public class JCResourceServiceImpl extends ServiceImpl @@ -24,9 +33,85 @@ public class JCResourceServiceImpl @Autowired private AppConfigService appConfigService; - @Override - public Map chunksPreSignUrlByIds(List ids) { + public PaginationResult paginate(HashMap params) { + try { + /** 获取分页参数,默认第1页,每页10条 */ + Integer page = MapUtils.getInteger(params, "page", 1); + Integer size = MapUtils.getInteger(params, "size", 10); + /** 创建分页对象 */ + Page pageParam = new Page<>(page, size); + + /** 创建 Lambda 条件构造器,用于构建类型安全的查询条件 */ + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + /** 固定条件:只查询未删除的记录(km_is_del = false) */ +// queryWrapper.eq(Textbook::getKmIsDel, false); + + /** 动态添加查询条件:资源类型 */ + if (MapUtils.getString(params, "type") != null && !MapUtils.getString(params, "type").isEmpty()) { + queryWrapper.eq(JCResource::getType, MapUtils.getString(params, "type")); + } + + /** 添加排序条件:按创建时间降序排列 默认时间降序*/ + if (MapUtils.getString(params, "sortOrder") != null + && !MapUtils.getString(params, "sortOrder").isEmpty() + && MapUtils.getString(params, "sortFiled") != null + && !MapUtils.getString(params, "sortFiled").isEmpty() + ) { + String sortOrder = MapUtils.getString(params, "sortOrder"); + String sortFiled = MapUtils.getString(params, "sortFiled"); + if ("ascend".equals(sortOrder)){ + if ("name".equals(sortFiled)) { + queryWrapper.orderByAsc(JCResource::getName); + }else if ("size".equals(sortFiled)){ + queryWrapper.orderByAsc(JCResource::getSize); + }else { + queryWrapper.orderByAsc(JCResource::getCreateTime); + } + }else { + if ("name".equals(sortFiled)) { + queryWrapper.orderByDesc(JCResource::getName); + }else if ("size".equals(sortFiled)){ + queryWrapper.orderByDesc(JCResource::getSize); + }else { + queryWrapper.orderByDesc(JCResource::getCreateTime); + } + } + }else { + queryWrapper.orderByDesc(JCResource::getCreateTime); + } + + /** 执行分页查询 */ + IPage pageResult = this.page(pageParam, queryWrapper); + + /** 计算总页数 */ + Long total = pageResult.getTotal(); + Long pages = (total + size - 1) / size; // 向上取整 + + /** 构建返回结果,包含完整的分页信息 */ + PaginationResult result = new PaginationResult<>(); + result.setData(pageResult.getRecords()); + result.setTotal(total); + result.setCurrent(page); // 当前页码 + result.setSize(size); // 每页大小 + result.setPages(pages); // 总页数 + + return result; + } catch (Exception e) { + log.error("分页查询消息失败,参数:{}", params, e); + /** 返回空结果 */ + PaginationResult emptyResult = new PaginationResult<>(); + emptyResult.setData(new ArrayList<>()); + emptyResult.setTotal(0L); + emptyResult.setCurrent(MapUtils.getInteger(params, "page", 1)); + emptyResult.setSize(MapUtils.getInteger(params, "size", 10)); + emptyResult.setPages(0L); + return emptyResult; + } + } + @Override + public Map chunksPreSignUrlByIds(List ids) { S3Util s3Util = new S3Util(appConfigService.getS3Config()); Map preSignUrlMap = new HashMap<>(); diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/TextbookServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/TextbookServiceImpl.java index 2315fa3..ca733ee 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/TextbookServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/TextbookServiceImpl.java @@ -2,21 +2,32 @@ package xyz.playedu.jc.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import xyz.playedu.common.config.ServerConfig; import xyz.playedu.common.exception.NotFoundException; import xyz.playedu.common.types.paginate.PaginationResult; +import xyz.playedu.common.util.StringUtil; +import xyz.playedu.course.domain.Course; import xyz.playedu.jc.domain.JCResource; import xyz.playedu.jc.domain.Textbook; +import xyz.playedu.jc.domain.dto.TextbookUserDTO; import xyz.playedu.jc.mapper.TextbookMapper; +import xyz.playedu.jc.service.IBookDepartmentUserService; import xyz.playedu.jc.service.ITextbookService; import xyz.playedu.knowledge.domain.KnowledgeMessages; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; /** * 教材 Service 实现 @@ -48,11 +59,18 @@ public class TextbookServiceImpl LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); /** 固定条件:只查询未删除的记录(km_is_del = false) */ -// queryWrapper.eq(Textbook::getKmIsDel, false); + // queryWrapper.eq(Textbook::getKmIsDel, false); - /** 动态添加查询条件:会话ID */ - if (MapUtils.getString(params, "title") != null && !MapUtils.getString(params, "title").isEmpty()) { - queryWrapper.eq(Textbook::getTitle, MapUtils.getString(params, "title")); + /** 动态添加查询条件:关键词 */ + String title = MapUtils.getString(params, "title"); + + // 动态添加模糊查询条件:name / author / . + if (title != null && !title.isEmpty()) { + queryWrapper.and(w -> w + .like(Textbook::getTitle, title) // 书名模糊 + .or().like(Textbook::getAuthor, title) // 作者模糊 + .or().like(Textbook::getMajor, title) // 专业模糊 + ); } @@ -96,4 +114,77 @@ public class TextbookServiceImpl } return textbook; } + @Autowired + private ServerConfig serverConfig; + @Autowired + private IBookDepartmentUserService bookDepartmentUserService; + @Override + public List getDepCoursesAndShow( + TextbookUserDTO textbookUserDTO) { + if (StringUtil.isEmpty(textbookUserDTO.getDepIds())) { + return new ArrayList<>(); + } + +// List res = new ArrayList<>(); + + List courseIds = + bookDepartmentUserService.getCourseIdsByDepIdsOrUserIds(textbookUserDTO.getDepIds(), textbookUserDTO.getUserIds()); + if (StringUtil.isEmpty(courseIds)) { + return new ArrayList<>(); + } +// if (categoryId != null && categoryId > 0) { +// // 获取所有子类 +// List allCategoryIdsList = +// categoryService.getChildCategoryIdsByParentId(categoryId + ""); +// List tmpCourseIds = +// courseCategoryService.getCourseIdsByCategoryIds(allCategoryIdsList); +// if (StringUtil.isEmpty(tmpCourseIds)) { +// return new ArrayList<>(); +// } +// courseIds = courseIds.stream().filter(tmpCourseIds::contains).toList(); +// if (StringUtil.isEmpty(courseIds)) { +// return new ArrayList<>(); +// } +// } + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery(); + +// 课程范围 + queryWrapper.in(Textbook::getId, courseIds); + +// 关键字查询:书名 / 作者 / 专业 + String keyword = textbookUserDTO.getName(); + if (keyword != null && !keyword.isEmpty()) { + queryWrapper.and(w -> w + .like(Textbook::getTitle, keyword) + .or().like(Textbook::getAuthor, keyword) + .or().like(Textbook::getMajor, keyword) + ); + } + +// 出版年份查询(只传年份,比如 "2024") + String publishYearStr = textbookUserDTO.getPublishTime(); + if (publishYearStr != null && !publishYearStr.isEmpty()) { + int year = Integer.parseInt(publishYearStr); + LocalDateTime start = LocalDateTime.of(year, 1, 1, 0, 0, 0); + LocalDateTime end = start.plusYears(1); + + queryWrapper.ge(Textbook::getPublishTime, start) + .lt(Textbook::getPublishTime, end); + } + +// 专业单独查询(如果是专门挑专业) + String major = textbookUserDTO.getMajor(); + if (major != null && !major.isEmpty()) { + queryWrapper.like(Textbook::getMajor, major); + } + + List res = list(queryWrapper); + + for (Textbook re : res) { + re.setAllUrl(serverConfig.getUrl() + re.getThumb()); + re.setUrl(serverConfig.getUrl()); + } + return res; + + } } \ No newline at end of file diff --git a/app/api/playedu-course/src/main/resources/mapper/jc/BookDepartmentUserMapper.xml b/app/api/playedu-course/src/main/resources/mapper/jc/BookDepartmentUserMapper.xml index f54e7ab..fa7122d 100644 --- a/app/api/playedu-course/src/main/resources/mapper/jc/BookDepartmentUserMapper.xml +++ b/app/api/playedu-course/src/main/resources/mapper/jc/BookDepartmentUserMapper.xml @@ -24,5 +24,22 @@ update_time, tenant_id - + diff --git a/app/api/playedu-resource/src/main/java/xyz/playedu/resource/service/impl/ResourceTranscodeInfoServiceImpl.java b/app/api/playedu-resource/src/main/java/xyz/playedu/resource/service/impl/ResourceTranscodeInfoServiceImpl.java index e1c4889..be7a392 100644 --- a/app/api/playedu-resource/src/main/java/xyz/playedu/resource/service/impl/ResourceTranscodeInfoServiceImpl.java +++ b/app/api/playedu-resource/src/main/java/xyz/playedu/resource/service/impl/ResourceTranscodeInfoServiceImpl.java @@ -155,7 +155,7 @@ public class ResourceTranscodeInfoServiceImpl || timestamp == null || StringUtil.isEmpty(definition) || StringUtil.isEmpty(sign)) { - throw new ServiceException("参数为空"); + throw new ServiceException("参数为空2"); } String str = From f7e04de5e5ad25344fa19eddd5a69cae522f3388 Mon Sep 17 00:00:00 2001 From: menft <17554333016@163.com> Date: Sat, 29 Nov 2025 22:31:42 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat(exam):=20=E8=AF=95=E9=A2=98=E5=85=B3?= =?UTF-8?q?=E8=81=94=E7=9F=A5=E8=AF=86=E7=82=B9=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端:ExamQuestion 实体新增 knowledge_code 字段存储关联知识点 - 后端:TextbookController 新增教材下拉列表、知识点列表接口 - 后端:KnowledgeController 新增按编码列表查询知识点接口 - 前端:试题创建/编辑页面增加教材-知识点两级级联选择器 - 支持多选知识点,编辑时自动回显已关联知识点 --- .../backend/exam/QuestionController.java | 6 + .../backend/jc/KnowledgeController.java | 16 +++ .../backend/jc/TextbookController.java | 27 +++- .../backend/exam/ExamQuestionRequest.java | 3 + .../paginate/ExamQuestionPaginateFilter.java | 2 + .../java/xyz/playedu/jc/domain/Knowledge.java | 3 +- .../jc/service/impl/KnowledgeServiceImpl.java | 3 +- .../xyz/playedu/exam/domain/ExamQuestion.java | 9 ++ .../exam/service/ExamQuestionService.java | 8 +- .../service/impl/ExamQuestionServiceImpl.java | 10 +- .../resources/mapper/ExamQuestionMapper.xml | 9 +- app/backend/src/api/question.ts | 17 ++- app/backend/src/api/textbook.ts | 19 +++ .../src/compenents/add-question/index.tsx | 86 +++++++++++- .../questions/compenents/detail-create.tsx | 89 +++++++++++- .../questions/compenents/detail-update.tsx | 128 +++++++++++++++++- 16 files changed, 410 insertions(+), 25 deletions(-) diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java index 031fbe5..c463bc9 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java @@ -74,6 +74,7 @@ public class QuestionController { String content = MapUtils.getString(params, "content"); Integer level = MapUtils.getInteger(params, "level"); Integer type = MapUtils.getInteger(params, "type"); + String knowledgeCode = MapUtils.getString(params, "knowledge_code"); ExamQuestionPaginateFilter filter = new ExamQuestionPaginateFilter(); filter.setSortAlgo(sortAlgo); @@ -82,6 +83,7 @@ public class QuestionController { filter.setContent(content); filter.setLevel(level); filter.setType(type); + filter.setKnowledgeCode(knowledgeCode); if (!backendBus.isSuperAdmin()) { // 非超管只能读取它自己的题库 filter.setAdminIds(backendBus.getSameDataPermissionAdminIds()); @@ -125,6 +127,7 @@ public class QuestionController { String content = MapUtils.getString(params, "content"); Integer level = MapUtils.getInteger(params, "level"); Integer type = MapUtils.getInteger(params, "type"); + String knowledgeCode = MapUtils.getString(params, "knowledge_code"); List questionCategories = new ArrayList<>(); if (StringUtil.isNotNull(categoryId)) { @@ -147,6 +150,7 @@ public class QuestionController { filter.setContent(content); filter.setLevel(level); filter.setType(type); + filter.setKnowledgeCode(knowledgeCode); if (!backendBus.isSuperAdmin()) { // 非超管只能读取它自己的题库 filter.setAdminIds(backendBus.getSameDataPermissionAdminIds()); @@ -169,6 +173,7 @@ public class QuestionController { req.getCategoryId(), req.getContent().replaceAll(" ", ""), req.getLevel(), + req.getKnowledgeCode(), req.getType(), BCtx.getId()); @@ -198,6 +203,7 @@ public class QuestionController { examQuestion.getId(), req.getContent().replaceAll(" ", ""), req.getLevel(), + req.getKnowledgeCode(), req.getType(), req.getCategoryId(), BCtx.getId()); diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java index 3b5c2c3..dcef4f1 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/KnowledgeController.java @@ -72,4 +72,20 @@ public class KnowledgeController { knowledgeService.remove(queryWrapper); return JsonResponse.success(); } + + /** + * 根据知识点编码列表获取知识点详情(用于编辑回显) + * @param codes 逗号分隔的知识点编码 + */ + @GetMapping("/byCodes") + public JsonResponse getByCodes(@RequestParam("codes") String codes) { + if (codes == null || codes.trim().isEmpty()) { + return JsonResponse.data(List.of()); + } + String[] codeArray = codes.split(","); + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(Knowledge::getKnowledgeCode, (Object[]) codeArray); + List list = knowledgeService.list(queryWrapper); + return JsonResponse.data(list); + } } diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java index 9314fc3..5525fae 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/jc/TextbookController.java @@ -1,7 +1,6 @@ package xyz.playedu.api.controller.backend.jc; import cn.hutool.core.util.ObjectUtil; -import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -37,7 +36,10 @@ import xyz.playedu.jc.service.ITextbookService; import xyz.playedu.jc.service.JCIResourceService; import xyz.playedu.knowledge.domain.KnowledgeMessages; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -273,6 +275,22 @@ public class TextbookController { return JsonResponse.data(list); } + /** + * 获取教材下拉选择列表(轻量级,仅返回id和title) + */ + @GetMapping("/selectList") + public JsonResponse selectList() { + List list = textbookService.list(); + List> result = new ArrayList<>(); + for (Textbook textbook : list) { + Map item = new HashMap<>(); + item.put("id", textbook.getId()); + item.put("title", textbook.getTitle()); + result.add(item); + } + return JsonResponse.data(result); + } + // @GetMapping("/{id}") // public JsonResponse detail(@PathVariable("id") Integer id) { // Textbook one = textbookService.getById(id); @@ -347,7 +365,4 @@ public class TextbookController { textbookService.updateById(textbook); return JsonResponse.success(); } - - - -} \ No newline at end of file +} diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java index bcc132d..4619857 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/request/backend/exam/ExamQuestionRequest.java @@ -24,6 +24,9 @@ public class ExamQuestionRequest implements Serializable { @NotNull(message = "level参数为空") private Integer level; + @JsonProperty("knowledge_code") + private String knowledgeCode; + @NotNull(message = "type参数为空") private Integer type; } diff --git a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java index bdec8f8..bfd27cb 100644 --- a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java +++ b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionPaginateFilter.java @@ -17,6 +17,8 @@ public class ExamQuestionPaginateFilter { private String categoryId; + private String knowledgeCode; + private List adminIds; private String sortField; diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java index eb5214a..ffb35c4 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/domain/Knowledge.java @@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import xyz.playedu.framework.tenant.core.db.TenantBaseDO; - import java.util.Date; import java.util.List; @@ -38,7 +37,7 @@ public class Knowledge extends TenantBaseDO { private String knowledgeCode; /** 知识点介绍 */ - @TableField("desc") + @TableField("`desc`") private String desc; /** 层级 */ diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java index 3c39430..dbfd480 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java @@ -47,7 +47,8 @@ public class KnowledgeServiceImpl extends ServiceImpl listVo(KnowledgeParam param) { //获取知识点卡片 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(Knowledge::getBookId, param.getBookId()) + // 当bookId不为空时才加条件,否则查询所有知识点 + queryWrapper.eq(param.getBookId() != null, Knowledge::getBookId, param.getBookId()) .eq(Knowledge::getIsReal,"1").orderByAsc(Knowledge::getOrderNum); return list(queryWrapper); } diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java index 687f936..3af51b2 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/domain/ExamQuestion.java @@ -36,6 +36,10 @@ public class ExamQuestion extends TenantBaseDO { /** 难度等级:1-4 */ private Integer level; + /** 知识点CODE(多个用逗号分隔) */ + @JsonProperty("knowledge_code") + private String knowledgeCode; + /** 内容 */ private String content; @@ -78,6 +82,9 @@ public class ExamQuestion extends TenantBaseDO { && (this.getLevel() == null ? other.getLevel() == null : this.getLevel().equals(other.getLevel())) + && (this.getKnowledgeCode() == null + ? other.getKnowledgeCode() == null + : this.getKnowledgeCode().equals(other.getKnowledgeCode())) && (this.getContent() == null ? other.getContent() == null : this.getContent().equals(other.getContent())) @@ -101,6 +108,7 @@ public class ExamQuestion extends TenantBaseDO { result = prime * result + ((getAdminId() == null) ? 0 : getAdminId().hashCode()); result = prime * result + ((getType() == null) ? 0 : getType().hashCode()); result = prime * result + ((getLevel() == null) ? 0 : getLevel().hashCode()); + result = prime * result + ((getKnowledgeCode() == null) ? 0 : getKnowledgeCode().hashCode()); result = prime * result + ((getContent() == null) ? 0 : getContent().hashCode()); result = prime * result + ((getCreatedAt() == null) ? 0 : getCreatedAt().hashCode()); result = prime * result + ((getUpdatedAt() == null) ? 0 : getUpdatedAt().hashCode()); @@ -119,6 +127,7 @@ public class ExamQuestion extends TenantBaseDO { sb.append(", adminId=").append(adminId); sb.append(", type=").append(type); sb.append(", level=").append(level); + sb.append(", knowledgeCode=").append(knowledgeCode); sb.append(", content=").append(content); sb.append(", createdAt=").append(createdAt); sb.append(", updatedAt=").append(updatedAt); diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java index 4dd6dda..018daa7 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java @@ -20,12 +20,18 @@ public interface ExamQuestionService extends IService { PaginationResult paginate(int page, int size, ExamQuestionPaginateFilter filter); Integer create( - Integer categoryId, String content, Integer level, Integer type, Integer adminId); + Integer categoryId, + String content, + Integer level, + String knowledgeCode, + Integer type, + Integer adminId); void update( Integer id, String content, Integer level, + String knowledgeCode, Integer type, Integer categoryId, Integer adminId); diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java index 9ef5681..4affbb9 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java @@ -50,13 +50,19 @@ public class ExamQuestionServiceImpl extends ServiceImpl + @@ -18,7 +19,7 @@ id,category_id,admin_id,type, - level,content,created_at, + level,knowledge_code,content,created_at, updated_at,deleted @@ -42,6 +43,9 @@ AND `exam_question`.`level` = #{level} + + AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`) + @@ -87,6 +91,9 @@ AND `exam_question`.`level` = #{level} + + AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`) + diff --git a/app/backend/src/api/question.ts b/app/backend/src/api/question.ts index db028cc..3f8c254 100644 --- a/app/backend/src/api/question.ts +++ b/app/backend/src/api/question.ts @@ -54,7 +54,8 @@ export function questionList( sortAlgo: string, content: string, level: any, - type: any + type: any, + knowledgeCode?: string ) { return client.get('/backend/v1/exam/question/index', { category_id, @@ -65,15 +66,23 @@ export function questionList( content, level, type, + knowledge_code: knowledgeCode, }); } -export function questionStore(category_id: number, content: string, level: any, type: any) { +export function questionStore( + category_id: number, + content: string, + level: any, + type: any, + knowledge_code?: string +) { return client.post('/backend/v1/exam/question/create', { category_id, content, level, type, + knowledge_code, }); } @@ -86,13 +95,15 @@ export function questionUpdate( category_id: number, content: string, level: any, - type: any + type: any, + knowledge_code?: string ) { return client.put(`/backend/v1/exam/question/${id}`, { category_id, content, level, type, + knowledge_code, }); } diff --git a/app/backend/src/api/textbook.ts b/app/backend/src/api/textbook.ts index d2cd50e..1f5ff48 100644 --- a/app/backend/src/api/textbook.ts +++ b/app/backend/src/api/textbook.ts @@ -229,6 +229,25 @@ export function EditChapterApi( }); } +/* + * Knowledge 知识点 + * */ + +// 获取知识点列表 +export function getKnowledgeListApi(bookId?: number) { + return client.get('/backend/v1/jc/knowledge/list', { bookId: bookId }); +} + +// 获取教材下拉列表(不分页,用于选择器) +export function getTextbookSelectListApi() { + return client.get('/backend/v1/jc/textbook/selectList', {}); +} + +// 根据知识点编码列表获取知识点详情(用于编辑回显) +export function getKnowledgeByCodesApi(codes: string) { + return client.get('/backend/v1/jc/knowledge/byCodes', { codes }); +} + /* * resource List * */ diff --git a/app/backend/src/compenents/add-question/index.tsx b/app/backend/src/compenents/add-question/index.tsx index 28583af..81e60a0 100644 --- a/app/backend/src/compenents/add-question/index.tsx +++ b/app/backend/src/compenents/add-question/index.tsx @@ -13,7 +13,7 @@ import { Spin, message, } from 'antd'; -import { question, resourceCategory } from '../../api'; +import { question, resourceCategory, textbook } from '../../api'; import type { ColumnsType } from 'antd/es/table'; import styles from './index.module.less'; import { TreeQuestion } from '../../compenents'; @@ -63,6 +63,11 @@ export const AddQuestion = (props: PropsInterface) => { const [selectedIds, setSelectedIds] = useState([]); const [selectVideos, setSelectVideos] = useState([]); const [categories, setCategories] = useState([]); + const [textbookList, setTextbookList] = useState([]); // 教材列表 + const [selectedTextbookId, setSelectedTextbookId] = useState(undefined); // 选中的教材ID + const [knowledgeCode, setKnowledgeCode] = useState(''); + const [knowledgeList, setKnowledgeList] = useState([]); + const [knowledgeLoading, setKnowledgeLoading] = useState(false); // 知识点加载状态 const [resourceUrl, setResourceUrl] = useState({}); const types = [ { label: t('exam.question.choice.label2'), value: 1 }, @@ -92,10 +97,43 @@ export const AddQuestion = (props: PropsInterface) => { const initData = async () => { await getCategory(); + await getTextbookList(); await getList(); setInit(false); }; + // 加载教材列表 + const getTextbookList = async () => { + try { + const res: any = await textbook.getTextbookSelectListApi(); + if (res.data && Array.isArray(res.data)) { + setTextbookList(res.data); + } + } catch (err) { + console.error('加载教材列表失败:', err); + } + }; + + // 当选择教材时,加载对应的知识点列表 + const handleTextbookChange = async (textbookId: number | undefined) => { + setSelectedTextbookId(textbookId); + setKnowledgeCode(''); // 清空已选知识点 + setKnowledgeList([]); + + if (textbookId) { + setKnowledgeLoading(true); + try { + const res: any = await textbook.getKnowledgeListApi(textbookId); + if (res.data && Array.isArray(res.data)) { + setKnowledgeList(res.data); + } + } catch (err) { + console.error('加载知识点列表失败:', err); + } + setKnowledgeLoading(false); + } + }; + const getCategory = async () => { const res: any = await resourceCategory.resourceCategoryList(); const categories = res.data.categories; @@ -122,7 +160,8 @@ export const AddQuestion = (props: PropsInterface) => { '', name, level.length === 0 ? '' : level, - type.length === 0 ? '' : type + type.length === 0 ? '' : type, + knowledgeCode || undefined ); setResourceUrl(res.data.resource_url); const data = res.data.result.data; @@ -152,6 +191,9 @@ export const AddQuestion = (props: PropsInterface) => { setName(''); setType([]); setLevel([]); + setSelectedTextbookId(undefined); + setKnowledgeCode(''); + setKnowledgeList([]); setSelectedRowKeys([]); setSelectedIds([]); setRefresh(!refresh); @@ -380,6 +422,46 @@ export const AddQuestion = (props: PropsInterface) => { placeholder={t('exam.question.detail.namePlaceholder2')} /> + +
+
+ 教材 + + (option?.label ?? '').toLowerCase().includes(input.toLowerCase()) + } + onChange={(value: any) => setKnowledgeCode(value || '')} + options={knowledgeList.map((item: any) => ({ + label: item.name, + value: item.knowledgeCode || item.knowledge_code, + }))} + /> +
diff --git a/app/backend/src/pages/exam/questions/compenents/detail-create.tsx b/app/backend/src/pages/exam/questions/compenents/detail-create.tsx index 3602e97..e45cab2 100644 --- a/app/backend/src/pages/exam/questions/compenents/detail-create.tsx +++ b/app/backend/src/pages/exam/questions/compenents/detail-create.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect } from 'react'; -import { Modal, Form, Tabs, Radio, Spin, message } from 'antd'; +import { Modal, Form, Tabs, Radio, Spin, message, Cascader } from 'antd'; import type { TabsProps } from 'antd'; -import { question } from '../../../../api/index'; +import type { CascaderProps } from 'antd'; +import { question, textbook } from '../../../../api/index'; import { QuestionInput } from '../../../../compenents'; import { QChoice } from './choice'; import { QSelect } from './select'; @@ -11,6 +12,15 @@ import { QQa } from './qa'; import { QCap } from './cap'; import { useTranslation } from 'react-i18next'; +// 级联选择器选项类型 +interface CascaderOption { + value: string | number; + label: string; + children?: CascaderOption[]; + isLeaf?: boolean; + loading?: boolean; +} + interface PropInterface { id: number; open: boolean; @@ -24,6 +34,7 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan const [loading, setLoading] = useState(false); const [refresh, setRefresh] = useState(false); const [type, setType] = useState('1'); + const [cascaderOptions, setCascaderOptions] = useState([]); // 级联选择器选项 const [formParams, setFormParams] = useState({ v: 'v1', d: { @@ -38,6 +49,7 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan setType('1'); form.setFieldsValue({ level: 1, + knowledge_cascader: [], }); setFormParams({ v: 'v1', @@ -46,10 +58,49 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan remark: null, }, }); + // 加载教材列表作为级联选择器第一级 + textbook.getTextbookSelectListApi().then((res: any) => { + if (res.data && Array.isArray(res.data)) { + const options: CascaderOption[] = res.data.map((item: any) => ({ + value: item.id, + label: item.title, + isLeaf: false, // 表示有子节点 + })); + setCascaderOptions(options); + } + }).catch((err) => { + console.error('加载教材列表失败:', err); + }); setInit(false); } }, [form, open, id]); + // 级联选择器动态加载知识点 + const loadKnowledgeData = (selectedOptions: CascaderOption[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1]; + targetOption.loading = true; + + // 加载该教材下的知识点 + textbook.getKnowledgeListApi(targetOption.value as number).then((res: any) => { + targetOption.loading = false; + if (res.data && Array.isArray(res.data)) { + targetOption.children = res.data.map((item: any) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, // 知识点是叶子节点 + })); + } else { + targetOption.children = []; + } + setCascaderOptions([...cascaderOptions]); + }).catch((err) => { + console.error('加载知识点失败:', err); + targetOption.loading = false; + targetOption.children = []; + setCascaderOptions([...cascaderOptions]); + }); + }; + const items: TabsProps['items'] = [ { key: '1', @@ -289,8 +340,19 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan } } const params = JSON.stringify(formParams); + // 处理知识点:从级联选择器值中提取知识点编码(第二级的值) + let knowledgeCode: string | undefined = undefined; + if (values.knowledge_cascader && values.knowledge_cascader.length > 0) { + // 级联选择器多选值格式: [[textbook_id, knowledge_code], ...] + const codes = values.knowledge_cascader + .filter((item: any[]) => item && item.length === 2) + .map((item: any[]) => item[1]); // 取第二级的值(知识点编码) + if (codes.length > 0) { + knowledgeCode = codes.join(','); + } + } setLoading(true); - question.questionStore(id, params, values.level, Number(type)).then((res: any) => { + question.questionStore(id, params, values.level, Number(type), knowledgeCode).then((res: any) => { setLoading(false); message.success(t('commen.saveSuccess')); onCancel(); @@ -413,6 +475,27 @@ export const QuestionsDetailCreate: React.FC = ({ id, open, onCan + + ['loadData']} + multiple + maxTagCount="responsive" + placeholder="请选择教材和知识点(可多选)" + style={{ width: '100%' }} + showCheckedStrategy={Cascader.SHOW_CHILD} + showSearch={{ + filter: (inputValue: string, path: CascaderOption[]) => + path.some( + (option) => + (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + ), + }} + /> + = ({ id, qid, open, const [refresh, setRefresh] = useState(false); const [type, setType] = useState('1'); const [resourceUrl, setResourceUrl] = useState({}); + const [cascaderOptions, setCascaderOptions] = useState([]); // 级联选择器选项 const [formParams, setFormParams] = useState({ v: 'v1', d: { @@ -40,20 +51,96 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, } }, [form, open, id, qid]); - const getDetail = () => { - question.questionDetail(qid).then((res: any) => { + const getDetail = async () => { + try { + // 1. 加载教材列表作为级联选择器第一级 + const textbookRes = await textbook.getTextbookSelectListApi(); + let options: CascaderOption[] = []; + if (textbookRes.data && Array.isArray(textbookRes.data)) { + options = textbookRes.data.map((item: any) => ({ + value: item.id, + label: item.title, + isLeaf: false, + })); + setCascaderOptions(options); + } + + // 2. 加载试题详情 + const res: any = await question.questionDetail(qid); setResourceUrl(res.data.resource_url); const data = res.data.question; setType(String(data.type)); const params = JSON.parse(res.data.question.content); + + // 3. 处理知识点回显 + let cascaderValue: (string | number)[][] = []; + if (data.knowledge_code) { + try { + // 获取知识点详情(包含bookId) + const knowledgeRes: any = await textbook.getKnowledgeByCodesApi(data.knowledge_code); + if (knowledgeRes.data && Array.isArray(knowledgeRes.data) && knowledgeRes.data.length > 0) { + // 获取所有需要预加载的教材ID(去重) + const bookIds = [...new Set(knowledgeRes.data.map((k: any) => k.bookId))]; + + // 预加载每个教材的知识点列表到级联选项中 + for (const bookId of bookIds) { + const knowledgeListRes: any = await textbook.getKnowledgeListApi(bookId as number); + const targetOption = options.find(opt => opt.value === bookId); + if (targetOption && knowledgeListRes.data && Array.isArray(knowledgeListRes.data)) { + targetOption.children = knowledgeListRes.data.map((item: any) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, + })); + } + } + setCascaderOptions([...options]); + + // 构建级联选择器的值 [[bookId, knowledgeCode], ...] + cascaderValue = knowledgeRes.data.map((k: any) => [k.bookId, k.knowledgeCode || k.knowledge_code]); + } + } catch (err) { + console.error('加载知识点详情失败:', err); + } + } + form.setFieldsValue({ level: data.level, type: String(data.type), content: params.d.content, remark: params.d.remark, + knowledge_cascader: cascaderValue, }); setFormParams(params); setInit(false); + } catch (err) { + console.error('加载数据失败:', err); + setInit(false); + } + }; + + // 级联选择器动态加载知识点 + const loadKnowledgeData = (selectedOptions: CascaderOption[]) => { + const targetOption = selectedOptions[selectedOptions.length - 1]; + targetOption.loading = true; + + textbook.getKnowledgeListApi(targetOption.value as number).then((res: any) => { + targetOption.loading = false; + if (res.data && Array.isArray(res.data)) { + targetOption.children = res.data.map((item: any) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, + })); + } else { + targetOption.children = []; + } + setCascaderOptions([...cascaderOptions]); + }).catch((err) => { + console.error('加载知识点失败:', err); + targetOption.loading = false; + targetOption.children = []; + setCascaderOptions([...cascaderOptions]); }); }; @@ -268,8 +355,18 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, } } const params = JSON.stringify(formParams); + // 处理知识点:从级联选择器值中提取知识点编码(第二级的值) + let knowledgeCode: string | undefined = undefined; + if (values.knowledge_cascader && values.knowledge_cascader.length > 0) { + const codes = values.knowledge_cascader + .filter((item: any[]) => item && item.length === 2) + .map((item: any[]) => item[1]); + if (codes.length > 0) { + knowledgeCode = codes.join(','); + } + } setLoading(true); - question.questionUpdate(qid, id, params, values.level, Number(type)).then((res: any) => { + question.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode).then((res: any) => { setLoading(false); message.success(t('commen.saveSuccess')); onCancel(); @@ -438,6 +535,27 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, + + ['loadData']} + multiple + maxTagCount="responsive" + placeholder="请选择教材和知识点(可多选)" + style={{ width: '100%' }} + showCheckedStrategy={Cascader.SHOW_CHILD} + showSearch={{ + filter: (inputValue: string, path: CascaderOption[]) => + path.some( + (option) => + (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + ), + }} + /> + Date: Sun, 30 Nov 2025 10:37:31 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat(exam):=20=E9=9A=8F=E6=9C=BA=E8=AF=95?= =?UTF-8?q?=E5=8D=B7=E6=94=AF=E6=8C=81=E9=9A=BE=E5=BA=A6=E4=B8=8E=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E7=82=B9=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 /knowledge-codes 接口,根据题库/题型/难度获取知识点列表 - ExamQuestionMapper.xml 添加 level 和 knowledge_code 过滤条件 - IKnowledgeService 新增 getByKnowledgeCodes 批量查询方法 - 前端 random-paper.tsx 各题型增加难度和知识点下拉选择 - 知识点动态加载,支持编辑时回显名称 - 更新前端 CLAUDE.md 文档,补充 httpClient 响应结构说明 --- .../backend/exam/QuestionController.java | 54 +++ .../controller/frontend/PaperController.java | 14 +- .../types/paginate/ExamQuestionFilter.java | 12 + .../playedu/jc/service/IKnowledgeService.java | 9 +- .../jc/service/impl/KnowledgeServiceImpl.java | 10 + .../exam/mapper/ExamQuestionMapper.java | 2 + .../exam/service/ExamQuestionService.java | 7 + .../service/impl/ExamQuestionServiceImpl.java | 18 + .../resources/mapper/ExamQuestionMapper.xml | 78 +++++ app/backend/CLAUDE.md | 207 ++++++++++++ app/backend/src/api/question.ts | 14 + .../exam/papers/compenents/random-paper.tsx | 310 +++++++++++++++++- .../questions/compenents/detail-update.tsx | 71 ++-- 13 files changed, 767 insertions(+), 39 deletions(-) create mode 100644 app/backend/CLAUDE.md diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java index c463bc9..86fcb9d 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/backend/exam/QuestionController.java @@ -39,6 +39,8 @@ import xyz.playedu.common.util.StringUtil; import xyz.playedu.exam.constants.ExamConstant; import xyz.playedu.exam.domain.*; import xyz.playedu.exam.service.*; +import xyz.playedu.jc.domain.Knowledge; +import xyz.playedu.jc.service.IKnowledgeService; import xyz.playedu.resource.service.ResourceService; @RestController @@ -62,6 +64,8 @@ public class QuestionController { @Autowired private ResourceService resourceService; + @Autowired private IKnowledgeService knowledgeService; + @GetMapping("/index") @BackendPermission(slug = BPermissionConstant.EXAM_QUESTION) @Log(title = "试题-列表", businessType = BusinessTypeConstant.GET) @@ -1178,4 +1182,54 @@ public class QuestionController { .size()); examQuestionCategoryService.updateById(examQuestionCategory); } + + /** + * 根据题库ID、题型、难度获取关联的知识点列表(去重) + * 用于随机试卷配置时的知识点筛选 + * 返回格式: [{code: "MATH001", name: "导数与微分"}, ...] + * + * @author menft + */ + @GetMapping("/knowledge-codes") + @BackendPermission(slug = BPermissionConstant.EXAM_QUESTION) + public JsonResponse getKnowledgeCodes(@RequestParam HashMap params) { + String categoryIds = MapUtils.getString(params, "category_ids"); + Integer type = MapUtils.getInteger(params, "type"); + Integer level = MapUtils.getInteger(params, "level"); + + if (StringUtil.isEmpty(categoryIds)) { + return JsonResponse.data(List.of()); + } + + // 获取去重的知识点编码 + List knowledgeCodes = + examQuestionService.getDistinctKnowledgeCodes(categoryIds, type, level); + if (knowledgeCodes.isEmpty()) { + return JsonResponse.data(List.of()); + } + + // 查询知识点名称 + List knowledgeList = knowledgeService.getByKnowledgeCodes(knowledgeCodes); + Map codeToName = + knowledgeList.stream() + .collect( + Collectors.toMap( + Knowledge::getKnowledgeCode, + Knowledge::getName, + (v1, v2) -> v1)); + + // 构建返回结果 + List> result = + knowledgeCodes.stream() + .map( + code -> { + Map item = new HashMap<>(); + item.put("code", code); + item.put("name", codeToName.getOrDefault(code, code)); + return item; + }) + .collect(Collectors.toList()); + + return JsonResponse.data(result); + } } diff --git a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java index 11a113c..15a77a3 100644 --- a/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java +++ b/app/api/playedu-api/src/main/java/xyz/playedu/api/controller/frontend/PaperController.java @@ -961,41 +961,53 @@ public class PaperController { JSONObject source = randomRules.getJSONObject("source"); List categoryIds = source.getBeanList("category_ids", Integer.class); filter.setCategoryIds(categoryIds); - // 题型、数量 + // 题型、数量、难度、知识点 JSONObject score = randomRules.getJSONObject("score"); JSONObject type1 = score.getJSONObject(ExamConstant.TYPE_1 + ""); if (StringUtil.isNotNull(type1)) { filter.setType1Number(type1.getInt("number")); + filter.setType1Level(type1.getInt("level")); + filter.setType1KnowledgeCodes(type1.getStr("knowledge_codes")); } else { filter.setType1Number(0); } JSONObject type2 = score.getJSONObject(ExamConstant.TYPE_2 + ""); if (StringUtil.isNotNull(type2)) { filter.setType2Number(type2.getInt("number")); + filter.setType2Level(type2.getInt("level")); + filter.setType2KnowledgeCodes(type2.getStr("knowledge_codes")); } else { filter.setType2Number(0); } JSONObject type3 = score.getJSONObject(ExamConstant.TYPE_3 + ""); if (StringUtil.isNotNull(type3)) { filter.setType3Number(type3.getInt("number")); + filter.setType3Level(type3.getInt("level")); + filter.setType3KnowledgeCodes(type3.getStr("knowledge_codes")); } else { filter.setType3Number(0); } JSONObject type4 = score.getJSONObject(ExamConstant.TYPE_4 + ""); if (StringUtil.isNotNull(type4)) { filter.setType4Number(type4.getInt("number")); + filter.setType4Level(type4.getInt("level")); + filter.setType4KnowledgeCodes(type4.getStr("knowledge_codes")); } else { filter.setType4Number(0); } JSONObject type5 = score.getJSONObject(ExamConstant.TYPE_5 + ""); if (StringUtil.isNotNull(type5)) { filter.setType5Number(type5.getInt("number")); + filter.setType5Level(type5.getInt("level")); + filter.setType5KnowledgeCodes(type5.getStr("knowledge_codes")); } else { filter.setType5Number(0); } JSONObject type6 = score.getJSONObject(ExamConstant.TYPE_6 + ""); if (StringUtil.isNotNull(type6)) { filter.setType6Number(type6.getInt("number")); + filter.setType6Level(type6.getInt("level")); + filter.setType6KnowledgeCodes(type6.getStr("knowledge_codes")); } else { filter.setType6Number(0); } diff --git a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java index 47a18ef..c4e472a 100644 --- a/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java +++ b/app/api/playedu-common/src/main/java/xyz/playedu/common/types/paginate/ExamQuestionFilter.java @@ -12,14 +12,26 @@ public class ExamQuestionFilter { private List categoryIds; private Integer type1Number; + private Integer type1Level; + private String type1KnowledgeCodes; private Integer type2Number; + private Integer type2Level; + private String type2KnowledgeCodes; private Integer type3Number; + private Integer type3Level; + private String type3KnowledgeCodes; private Integer type4Number; + private Integer type4Level; + private String type4KnowledgeCodes; private Integer type5Number; + private Integer type5Level; + private String type5KnowledgeCodes; private Integer type6Number; + private Integer type6Level; + private String type6KnowledgeCodes; } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java index dbf4149..8322572 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/IKnowledgeService.java @@ -20,5 +20,12 @@ public interface IKnowledgeService extends IService { JSONObject getByIdVo(Integer id); JSONObject stuGetByIdVo(Integer id); - + /** + * 根据知识点编码列表批量查询知识点 + * + * @param codes 知识点编码列表 + * @return 知识点列表 + * @author menft + */ + List getByKnowledgeCodes(List codes); } diff --git a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java index dbfd480..aff443a 100644 --- a/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java +++ b/app/api/playedu-course/src/main/java/xyz/playedu/jc/service/impl/KnowledgeServiceImpl.java @@ -217,6 +217,16 @@ public class KnowledgeServiceImpl extends ServiceImpl getByKnowledgeCodes(List codes) { + if (codes == null || codes.isEmpty()) { + return new ArrayList<>(); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.in(Knowledge::getKnowledgeCode, codes); + return list(queryWrapper); + } + /** * 递增编码 * @param code 当前编码 diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java index 98b62cd..377b5dd 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/mapper/ExamQuestionMapper.java @@ -22,4 +22,6 @@ public interface ExamQuestionMapper extends BaseMapper { Long paginateCount(ExamQuestionPaginateFilter filter); List chunksByCategoryIdAndLimit(ExamQuestionFilter filter); + + List getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level); } diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java index 018daa7..a823ac4 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/ExamQuestionService.java @@ -43,4 +43,11 @@ public interface ExamQuestionService extends IService { List chunksByCategoryId(Integer categoryId); List chunksByCategoryIdAndLimit(ExamQuestionFilter filter); + + /** + * 根据题库ID、题型、难度获取去重的知识点编码列表 + * + * @author menft + */ + List getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level); } diff --git a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java index 4affbb9..bfbec1a 100644 --- a/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java +++ b/app/api/playedu-exam/src/main/java/xyz/playedu/exam/service/impl/ExamQuestionServiceImpl.java @@ -116,4 +116,22 @@ public class ExamQuestionServiceImpl extends ServiceImpl chunksByCategoryIdAndLimit(ExamQuestionFilter filter) { return getBaseMapper().chunksByCategoryIdAndLimit(filter); } + + @Override + public List getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level) { + // 获取所有 knowledge_code 字段(可能是逗号分隔的多个值) + List rawCodes = getBaseMapper().getDistinctKnowledgeCodes(categoryIds, type, level); + if (rawCodes == null || rawCodes.isEmpty()) { + return new ArrayList<>(); + } + // 拆分并去重 + return rawCodes.stream() + .filter(code -> code != null && !code.isEmpty()) + .flatMap(code -> java.util.Arrays.stream(code.split(","))) + .map(String::trim) + .filter(code -> !code.isEmpty()) + .distinct() + .sorted() + .collect(java.util.stream.Collectors.toList()); + } } diff --git a/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml b/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml index b949770..57522bb 100644 --- a/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml +++ b/app/api/playedu-exam/src/main/resources/mapper/ExamQuestionMapper.xml @@ -107,6 +107,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type1Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type1Number}) @@ -122,6 +132,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type2Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type2Number}) @@ -137,6 +157,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type3Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type3Number}) @@ -152,6 +182,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type4Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type4Number}) @@ -167,6 +207,16 @@ #{categoryId} + + and `exam_question`.`level` = #{type5Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type5Number}) @@ -182,8 +232,36 @@ #{categoryId} + + and `exam_question`.`level` = #{type6Level} + + + and ( + + FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`) + + ) + ORDER BY RAND() LIMIT #{type6Number}) + + diff --git a/app/backend/CLAUDE.md b/app/backend/CLAUDE.md new file mode 100644 index 0000000..76981f3 --- /dev/null +++ b/app/backend/CLAUDE.md @@ -0,0 +1,207 @@ +[根目录](../../CLAUDE.md) > **app/backend** + +--- + +# 管理后台前端(PlayEdu Backend) + +## 变更记录 (Changelog) + +### 2025-11-24 22:45:26 +- 初始化模块文档 + +--- + +## 模块职责 + +PlayEdu 管理后台是供平台管理员使用的 Web 应用,提供: +- 课程管理(线上课程、线下课程、实验课程、软件模块) +- 教材与知识点管理 +- 考试与题库管理 +- 学员与部门管理 +- 资源库管理(视频、文档、音频、图片、附件) +- AI 知识库管理 +- 系统配置与权限管理 +- 数据统计与日志查看 + +--- + +## 入口与启动 + +**主入口文件**:`src/main.tsx` + +**启动命令**: +```bash +cd app/backend +npm install +npm run dev +``` + +**构建命令**: +```bash +npm run build +``` + +**开发服务器**:`http://localhost:5173`(Vite 默认端口) + +**生产构建输出**:`dist/` + +--- + +## 对外接口 + +### API 通信 +- **API Base URL**:配置在 `src/api/internal/httpClient.ts` +- **认证方式**:Bearer Token(存储在 localStorage) + +### ⚠️ httpClient 响应数据结构(重要) + +**httpClient 已经解包了一层**,调用 `client.get/post/put` 返回的是 `response.data`,即: + +```typescript +// 后端原始响应 +{ "code": 0, "msg": "", "data": [...] } + +// httpClient.get() 返回的 res 就是上面这个对象 +const res = await client.get('/api/xxx'); +// res = { code: 0, msg: "", data: [...] } +// res.data = [...] ✅ 正确 +// res.data.data ❌ 错误!多解了一层 +``` + +**正确用法**: +```typescript +const res: any = await someApi.getData(); +const items = res.data || []; // ✅ 直接用 res.data +``` + +**错误用法**: +```typescript +const res: any = await someApi.getData(); +const items = res.data.data || []; // ❌ 多解了一层,永远是 undefined +``` + +### 主要 API 模块(位于 `src/api/`): + - `login.ts` - 管理员登录与认证 + - `course.ts` - 课程管理 + - `paper.ts` / `question.ts` - 考试与题库 + - `user.ts` / `department.ts` - 学员与部门 + - `resource.ts` / `resource-category.ts` - 资源管理 + - `knowledge-*.ts` - AI 知识库 + - `system.ts` / `app-config.ts` - 系统配置 + - `upload.ts` - 文件上传(支持 S3/OSS) + +### 路由结构 +- **公开路由**:`/login` - 登录页 +- **管理员路由**(需认证): + - `/dashboard` - 数据看板 + - `/course/*` - 课程管理 + - `/offline-course/*` - 线下课程 + - `/lab-course/*` - 实验课程 + - `/textbook/*` - 教材管理 + - `/exam/*` - 考试管理 + - `/question/*` - 题库管理 + - `/resource/*` - 资源库 + - `/repository/*` - 仓库管理 + - `/knowledge/*` - AI 知识库 + - `/user/*` - 学员管理 + - `/department/*` - 部门管理 + - `/system/*` - 系统配置 + +--- + +## 关键依赖与配置 + +### 技术栈 +- **框架**:React 18.2 + TypeScript 4.9 +- **构建工具**:Vite 7.1.3 +- **UI 库**:Ant Design 5.12.2 +- **状态管理**:Redux Toolkit + React-Redux +- **路由**:React Router DOM 6.9 +- **HTTP 客户端**:Axios 1.3.4 +- **富文本编辑器**:Quill 2.0.3 + Braft Editor +- **图表**:ECharts 5.4.2 + ECharts for React +- **视频播放器**:XGPlayer 3.0.13 + HLS 支持 +- **文件上传**:Uppy 4.x(支持 AWS S3) +- **国际化**:i18next + react-i18next +- **工具库**:Day.js、Moment.js、Lodash、XLSX、FileSaver + +### 配置文件 +- `package.json` - 依赖与脚本定义 +- `vite.config.ts` - Vite 构建配置(未找到,可能使用默认配置) +- `tsconfig.json` - TypeScript 配置 +- `eslint.config.js` - ESLint 配置(使用 Prettier 集成) + +### 代码规范工具 +- ESLint 9.x(@typescript-eslint/eslint-plugin) +- Prettier 3.6.2 +- 格式化命令:`npm run format` + +--- + +## 数据模型 + +### Redux Store 结构(`src/store/`) +- `user.ts` / `userSlice` - 当前管理员信息 +- `system.ts` / `systemSlice` - 系统配置(API URL、logo、名称等) +- `resource.ts` / `resourceSlice` - 资源 URL 映射 + +### 本地存储(localStorage) +- `token` - 认证 Token +- `api_url` - API 服务器地址 +- `system_name` - 系统名称 +- `language` - 界面语言(zh-CN / zh-TC) + +--- + +## 测试与质量 + +- **代码检查**:ESLint(`npm run lint` / `npm run lint:quiet`) +- **代码格式化**:Prettier(`npm run format`) +- **类型检查**:TypeScript 严格模式 +- **浏览器兼容性**:通过 Vite 自动处理 polyfills + +--- + +## 常见问题 (FAQ) + +### Q1:如何添加新的管理页面? +1. 在 `src/pages/` 下创建新页面组件 +2. 在路由配置中添加路由(通常在 `App.tsx` 或独立的路由文件中) +3. 添加对应的 API 调用(在 `src/api/` 中) +4. 更新导航菜单(如果需要) + +### Q2:如何配置 API 服务器地址? +- 开发环境:在 `src/api/internal/httpClient.ts` 中配置 `baseURL` +- 生产环境:通过环境变量或运行时配置 + +### Q3:文件上传如何工作? +- 使用 Uppy 组件(`@uppy/react`) +- 支持直传到 S3/OSS(通过 `@uppy/aws-s3`) +- 上传接口:`POST /backend/v1/upload/*` + +### Q4:如何支持国际化? +- 配置文件:`src/i18n/config.ts` +- 语言包:`src/i18n/locales/` 目录 +- 使用 `useTranslation()` Hook 进行翻译 + +--- + +## 相关文件清单 + +### 配置文件 +- `app/backend/package.json` - NPM 依赖配置 +- `app/backend/README.md` - 模块说明 + +### 核心代码 +- `src/main.tsx` - 应用入口 +- `src/App.tsx` - 根组件与路由配置 +- `src/api/` - API 接口定义 +- `src/store/` - Redux 状态管理 +- `src/pages/` - 页面组件 +- `src/compenents/` - 可复用组件 +- `src/assets/` - 静态资源(图片、字体、样式) + +### 样式文件 +- `src/index.less` - 全局样式 +- `src/App.module.less` - 根组件样式 +- 组件样式:`*.module.less` / `*.module.scss` diff --git a/app/backend/src/api/question.ts b/app/backend/src/api/question.ts index 3f8c254..d747253 100644 --- a/app/backend/src/api/question.ts +++ b/app/backend/src/api/question.ts @@ -134,3 +134,17 @@ export function storeBatch(categoryId: number, startLine: number, questions: str export function uploadTxt(params: any) { return client.post('/backend/v1/exam/question/import', params); } + +/** + * 根据题库ID、题型、难度获取关联的知识点编码列表 + * @param categoryIds 题库ID列表(逗号分隔) + * @param type 题型 + * @param level 难度 + */ +export function getKnowledgeCodes(categoryIds: string, type?: number, level?: number | null) { + return client.get('/backend/v1/exam/question/knowledge-codes', { + category_ids: categoryIds, + type: type, + level: level, + }); +} diff --git a/app/backend/src/pages/exam/papers/compenents/random-paper.tsx b/app/backend/src/pages/exam/papers/compenents/random-paper.tsx index f18be2f..00b072f 100644 --- a/app/backend/src/pages/exam/papers/compenents/random-paper.tsx +++ b/app/backend/src/pages/exam/papers/compenents/random-paper.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import styles from './paper.module.less'; -import { message, Button, Table, InputNumber, Spin, Modal } from 'antd'; -import { paper } from '../../../../api'; +import { message, Button, Table, InputNumber, Spin, Modal, Select } from 'antd'; +import { paper, question } from '../../../../api'; import type { ColumnsType } from 'antd/es/table'; import { PlusOutlined } from '@ant-design/icons'; import { useLocation, useNavigate } from 'react-router-dom'; @@ -10,6 +10,14 @@ import { ExclamationCircleFilled } from '@ant-design/icons'; import { useTranslation } from 'react-i18next'; const { confirm } = Modal; +// 难度选项 +const levelOptions = [ + { value: null, label: '不限' }, + { value: 1, label: '简单' }, + { value: 2, label: '中等' }, + { value: 3, label: '困难' }, +]; + interface DataType { id: React.Key; admin_id: number; @@ -61,28 +69,49 @@ export const RendomPaper: React.FC = ({ type }) => { const [choice, setChoice] = useState({ number: 0, score: 0, + level: null as number | null, + knowledge_codes: '' as string, }); const [select, setSelect] = useState({ number: 0, score: 0, missed_score: 0, + level: null as number | null, + knowledge_codes: '' as string, }); const [input, setInput] = useState({ number: 0, score: 0, + level: null as number | null, + knowledge_codes: '' as string, }); const [judge, setJudge] = useState({ number: 0, score: 0, + level: null as number | null, + knowledge_codes: '' as string, }); const [qa, setQa] = useState({ number: 0, score: 0, + level: null as number | null, + knowledge_codes: '' as string, }); const [cap, setCap] = useState({ number: 0, score: 0, missed_score: 0, + level: null as number | null, + knowledge_codes: '' as string, + }); + // 各题型的知识点选项 + const [knowledgeOptions, setKnowledgeOptions] = useState>({ + 1: [], + 2: [], + 3: [], + 4: [], + 5: [], + 6: [], }); useEffect(() => { @@ -91,6 +120,24 @@ export const RendomPaper: React.FC = ({ type }) => { setId(Number(result.get('id'))); }, [result.get('cid'), result.get('title'), result.get('id')]); + // 根据题库、题型、难度加载知识点列表 + const loadKnowledgeCodes = async (type: number, level: number | null) => { + if (questions.length === 0) return; + try { + const res: any = await question.getKnowledgeCodes(questions.join(','), type, level); + // httpClient.get 已经 resolve(res.data),所以 res = {code, msg, data} + const items = res.data || []; + // 后端返回 [{code: "xxx", name: "yyy"}, ...] + const options = items.map((item: { code: string; name: string }) => ({ + value: item.code, + label: item.name, + })); + setKnowledgeOptions((prev) => ({ ...prev, [type]: options })); + } catch (e) { + console.error('加载知识点失败', e); + } + }; + useEffect(() => { if (id === 0) { return; @@ -187,6 +234,8 @@ export const RendomPaper: React.FC = ({ type }) => { setChoice({ number: score[1].number, score: score[1].score, + level: score[1].level || null, + knowledge_codes: score[1].knowledge_codes || '', }); } if (score[2]) { @@ -194,24 +243,32 @@ export const RendomPaper: React.FC = ({ type }) => { number: score[2].number, score: score[2].score, missed_score: score[2].missed_score, + level: score[2].level || null, + knowledge_codes: score[2].knowledge_codes || '', }); } if (score[3]) { setInput({ number: score[3].number, score: score[3].score, + level: score[3].level || null, + knowledge_codes: score[3].knowledge_codes || '', }); } if (score[4]) { setJudge({ number: score[4].number, score: score[4].score, + level: score[4].level || null, + knowledge_codes: score[4].knowledge_codes || '', }); } if (score[5]) { setQa({ number: score[5].number, score: score[5].score, + level: score[5].level || null, + knowledge_codes: score[5].knowledge_codes || '', }); } if (score[6]) { @@ -219,10 +276,33 @@ export const RendomPaper: React.FC = ({ type }) => { number: score[6].number, score: score[6].score, missed_score: 0, + level: score[6].level || null, + knowledge_codes: score[6].knowledge_codes || '', }); } setSpinInit(false); setInit(false); + + // 预加载各题型的知识点选项(用于回显) + if (arr.length > 0) { + const categoryIds = arr.join(','); + [1, 2, 3, 4, 5, 6].forEach((type) => { + const typeScore = score[type]; + if (typeScore && typeScore.knowledge_codes) { + question + .getKnowledgeCodes(categoryIds, type, typeScore.level || null) + .then((res: any) => { + const items = res.data || []; + const options = items.map((item: { code: string; name: string }) => ({ + value: item.code, + label: item.name, + })); + setKnowledgeOptions((prev) => ({ ...prev, [type]: options })); + }) + .catch(() => {}); + } + }); + } }); }; @@ -609,6 +689,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...choice }; + obj.knowledge_codes = values.join(','); + setChoice(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(1, choice.level)} + /> +
{t('exam.paper.compose.text1')} @@ -619,7 +735,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q2 > 0 && (
-
+
* {t('exam.question.select.label')}({t('exam.paper.compose.text1')} @@ -679,6 +795,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text5')}
+
+ 难度 + { + const obj = { ...select }; + obj.knowledge_codes = values.join(','); + setSelect(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(2, select.level)} + /> +
{t('exam.paper.compose.text1')} @@ -689,7 +841,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q3 > 0 && (
-
+
* {t('exam.question.input.label')}({t('exam.paper.compose.text1')} @@ -730,6 +882,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...input }; + obj.knowledge_codes = values.join(','); + setInput(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(3, input.level)} + /> +
{t('exam.paper.compose.text1')} @@ -740,7 +928,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q4 > 0 && (
-
+
* {t('exam.question.judge.label')}({t('exam.paper.compose.text1')} @@ -781,6 +969,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...judge }; + obj.knowledge_codes = values.join(','); + setJudge(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(4, judge.level)} + /> +
{t('exam.paper.compose.text1')} @@ -791,7 +1015,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q5 > 0 && (
-
+
* {t('exam.question.qa.label')}({t('exam.paper.compose.text1')} @@ -832,6 +1056,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...qa }; + obj.knowledge_codes = values.join(','); + setQa(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(5, qa.level)} + /> +
{t('exam.paper.compose.text1')} @@ -842,7 +1102,7 @@ export const RendomPaper: React.FC = ({ type }) => { )} {q6 > 0 && (
-
+
* {t('exam.question.cap.label')}({t('exam.paper.compose.text1')} @@ -883,6 +1143,42 @@ export const RendomPaper: React.FC = ({ type }) => { > {t('exam.paper.compose.text4')}
+
+ 难度 + { + const obj = { ...cap }; + obj.knowledge_codes = values.join(','); + setCap(obj); + }} + maxTagCount="responsive" + placeholder="不限" + onFocus={() => loadKnowledgeCodes(6, cap.level)} + /> +
{t('exam.paper.compose.text1')} diff --git a/app/backend/src/pages/exam/questions/compenents/detail-update.tsx b/app/backend/src/pages/exam/questions/compenents/detail-update.tsx index 755b5e3..14b2390 100644 --- a/app/backend/src/pages/exam/questions/compenents/detail-update.tsx +++ b/app/backend/src/pages/exam/questions/compenents/detail-update.tsx @@ -78,14 +78,18 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, try { // 获取知识点详情(包含bookId) const knowledgeRes: any = await textbook.getKnowledgeByCodesApi(data.knowledge_code); - if (knowledgeRes.data && Array.isArray(knowledgeRes.data) && knowledgeRes.data.length > 0) { + if ( + knowledgeRes.data && + Array.isArray(knowledgeRes.data) && + knowledgeRes.data.length > 0 + ) { // 获取所有需要预加载的教材ID(去重) const bookIds = [...new Set(knowledgeRes.data.map((k: any) => k.bookId))]; // 预加载每个教材的知识点列表到级联选项中 for (const bookId of bookIds) { const knowledgeListRes: any = await textbook.getKnowledgeListApi(bookId as number); - const targetOption = options.find(opt => opt.value === bookId); + const targetOption = options.find((opt) => opt.value === bookId); if (targetOption && knowledgeListRes.data && Array.isArray(knowledgeListRes.data)) { targetOption.children = knowledgeListRes.data.map((item: any) => ({ value: item.knowledgeCode || item.knowledge_code, @@ -97,7 +101,10 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, setCascaderOptions([...options]); // 构建级联选择器的值 [[bookId, knowledgeCode], ...] - cascaderValue = knowledgeRes.data.map((k: any) => [k.bookId, k.knowledgeCode || k.knowledge_code]); + cascaderValue = knowledgeRes.data.map((k: any) => [ + k.bookId, + k.knowledgeCode || k.knowledge_code, + ]); } } catch (err) { console.error('加载知识点详情失败:', err); @@ -124,24 +131,27 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, const targetOption = selectedOptions[selectedOptions.length - 1]; targetOption.loading = true; - textbook.getKnowledgeListApi(targetOption.value as number).then((res: any) => { - targetOption.loading = false; - if (res.data && Array.isArray(res.data)) { - targetOption.children = res.data.map((item: any) => ({ - value: item.knowledgeCode || item.knowledge_code, - label: item.name, - isLeaf: true, - })); - } else { + textbook + .getKnowledgeListApi(targetOption.value as number) + .then((res: any) => { + targetOption.loading = false; + if (res.data && Array.isArray(res.data)) { + targetOption.children = res.data.map((item: any) => ({ + value: item.knowledgeCode || item.knowledge_code, + label: item.name, + isLeaf: true, + })); + } else { + targetOption.children = []; + } + setCascaderOptions([...cascaderOptions]); + }) + .catch((err) => { + console.error('加载知识点失败:', err); + targetOption.loading = false; targetOption.children = []; - } - setCascaderOptions([...cascaderOptions]); - }).catch((err) => { - console.error('加载知识点失败:', err); - targetOption.loading = false; - targetOption.children = []; - setCascaderOptions([...cascaderOptions]); - }); + setCascaderOptions([...cascaderOptions]); + }); }; const onFinish = (values: any) => { @@ -366,11 +376,13 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, } } setLoading(true); - question.questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode).then((res: any) => { - setLoading(false); - message.success(t('commen.saveSuccess')); - onCancel(); - }); + question + .questionUpdate(qid, id, params, values.level, Number(type), knowledgeCode) + .then((res: any) => { + setLoading(false); + message.success(t('commen.saveSuccess')); + onCancel(); + }); }; const onFinishFailed = (errorInfo: any) => { @@ -535,10 +547,7 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, - + ['loadData']} @@ -551,7 +560,9 @@ export const QuestionsDetailUpdate: React.FC = ({ id, qid, open, filter: (inputValue: string, path: CascaderOption[]) => path.some( (option) => - (option.label as string).toLowerCase().indexOf(inputValue.toLowerCase()) > -1 + (option.label as string) + .toLowerCase() + .indexOf(inputValue.toLowerCase()) > -1 ), }} /> From d5bfe994be6da57cfcc34e306567f6441f87cdfe Mon Sep 17 00:00:00 2001 From: menft <17554333016@163.com> Date: Sun, 30 Nov 2025 10:42:28 +0800 Subject: [PATCH 4/5] =?UTF-8?q?:fire:=20=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/backend/CLAUDE.md | 207 ------------------------------------------ 1 file changed, 207 deletions(-) delete mode 100644 app/backend/CLAUDE.md diff --git a/app/backend/CLAUDE.md b/app/backend/CLAUDE.md deleted file mode 100644 index 76981f3..0000000 --- a/app/backend/CLAUDE.md +++ /dev/null @@ -1,207 +0,0 @@ -[根目录](../../CLAUDE.md) > **app/backend** - ---- - -# 管理后台前端(PlayEdu Backend) - -## 变更记录 (Changelog) - -### 2025-11-24 22:45:26 -- 初始化模块文档 - ---- - -## 模块职责 - -PlayEdu 管理后台是供平台管理员使用的 Web 应用,提供: -- 课程管理(线上课程、线下课程、实验课程、软件模块) -- 教材与知识点管理 -- 考试与题库管理 -- 学员与部门管理 -- 资源库管理(视频、文档、音频、图片、附件) -- AI 知识库管理 -- 系统配置与权限管理 -- 数据统计与日志查看 - ---- - -## 入口与启动 - -**主入口文件**:`src/main.tsx` - -**启动命令**: -```bash -cd app/backend -npm install -npm run dev -``` - -**构建命令**: -```bash -npm run build -``` - -**开发服务器**:`http://localhost:5173`(Vite 默认端口) - -**生产构建输出**:`dist/` - ---- - -## 对外接口 - -### API 通信 -- **API Base URL**:配置在 `src/api/internal/httpClient.ts` -- **认证方式**:Bearer Token(存储在 localStorage) - -### ⚠️ httpClient 响应数据结构(重要) - -**httpClient 已经解包了一层**,调用 `client.get/post/put` 返回的是 `response.data`,即: - -```typescript -// 后端原始响应 -{ "code": 0, "msg": "", "data": [...] } - -// httpClient.get() 返回的 res 就是上面这个对象 -const res = await client.get('/api/xxx'); -// res = { code: 0, msg: "", data: [...] } -// res.data = [...] ✅ 正确 -// res.data.data ❌ 错误!多解了一层 -``` - -**正确用法**: -```typescript -const res: any = await someApi.getData(); -const items = res.data || []; // ✅ 直接用 res.data -``` - -**错误用法**: -```typescript -const res: any = await someApi.getData(); -const items = res.data.data || []; // ❌ 多解了一层,永远是 undefined -``` - -### 主要 API 模块(位于 `src/api/`): - - `login.ts` - 管理员登录与认证 - - `course.ts` - 课程管理 - - `paper.ts` / `question.ts` - 考试与题库 - - `user.ts` / `department.ts` - 学员与部门 - - `resource.ts` / `resource-category.ts` - 资源管理 - - `knowledge-*.ts` - AI 知识库 - - `system.ts` / `app-config.ts` - 系统配置 - - `upload.ts` - 文件上传(支持 S3/OSS) - -### 路由结构 -- **公开路由**:`/login` - 登录页 -- **管理员路由**(需认证): - - `/dashboard` - 数据看板 - - `/course/*` - 课程管理 - - `/offline-course/*` - 线下课程 - - `/lab-course/*` - 实验课程 - - `/textbook/*` - 教材管理 - - `/exam/*` - 考试管理 - - `/question/*` - 题库管理 - - `/resource/*` - 资源库 - - `/repository/*` - 仓库管理 - - `/knowledge/*` - AI 知识库 - - `/user/*` - 学员管理 - - `/department/*` - 部门管理 - - `/system/*` - 系统配置 - ---- - -## 关键依赖与配置 - -### 技术栈 -- **框架**:React 18.2 + TypeScript 4.9 -- **构建工具**:Vite 7.1.3 -- **UI 库**:Ant Design 5.12.2 -- **状态管理**:Redux Toolkit + React-Redux -- **路由**:React Router DOM 6.9 -- **HTTP 客户端**:Axios 1.3.4 -- **富文本编辑器**:Quill 2.0.3 + Braft Editor -- **图表**:ECharts 5.4.2 + ECharts for React -- **视频播放器**:XGPlayer 3.0.13 + HLS 支持 -- **文件上传**:Uppy 4.x(支持 AWS S3) -- **国际化**:i18next + react-i18next -- **工具库**:Day.js、Moment.js、Lodash、XLSX、FileSaver - -### 配置文件 -- `package.json` - 依赖与脚本定义 -- `vite.config.ts` - Vite 构建配置(未找到,可能使用默认配置) -- `tsconfig.json` - TypeScript 配置 -- `eslint.config.js` - ESLint 配置(使用 Prettier 集成) - -### 代码规范工具 -- ESLint 9.x(@typescript-eslint/eslint-plugin) -- Prettier 3.6.2 -- 格式化命令:`npm run format` - ---- - -## 数据模型 - -### Redux Store 结构(`src/store/`) -- `user.ts` / `userSlice` - 当前管理员信息 -- `system.ts` / `systemSlice` - 系统配置(API URL、logo、名称等) -- `resource.ts` / `resourceSlice` - 资源 URL 映射 - -### 本地存储(localStorage) -- `token` - 认证 Token -- `api_url` - API 服务器地址 -- `system_name` - 系统名称 -- `language` - 界面语言(zh-CN / zh-TC) - ---- - -## 测试与质量 - -- **代码检查**:ESLint(`npm run lint` / `npm run lint:quiet`) -- **代码格式化**:Prettier(`npm run format`) -- **类型检查**:TypeScript 严格模式 -- **浏览器兼容性**:通过 Vite 自动处理 polyfills - ---- - -## 常见问题 (FAQ) - -### Q1:如何添加新的管理页面? -1. 在 `src/pages/` 下创建新页面组件 -2. 在路由配置中添加路由(通常在 `App.tsx` 或独立的路由文件中) -3. 添加对应的 API 调用(在 `src/api/` 中) -4. 更新导航菜单(如果需要) - -### Q2:如何配置 API 服务器地址? -- 开发环境:在 `src/api/internal/httpClient.ts` 中配置 `baseURL` -- 生产环境:通过环境变量或运行时配置 - -### Q3:文件上传如何工作? -- 使用 Uppy 组件(`@uppy/react`) -- 支持直传到 S3/OSS(通过 `@uppy/aws-s3`) -- 上传接口:`POST /backend/v1/upload/*` - -### Q4:如何支持国际化? -- 配置文件:`src/i18n/config.ts` -- 语言包:`src/i18n/locales/` 目录 -- 使用 `useTranslation()` Hook 进行翻译 - ---- - -## 相关文件清单 - -### 配置文件 -- `app/backend/package.json` - NPM 依赖配置 -- `app/backend/README.md` - 模块说明 - -### 核心代码 -- `src/main.tsx` - 应用入口 -- `src/App.tsx` - 根组件与路由配置 -- `src/api/` - API 接口定义 -- `src/store/` - Redux 状态管理 -- `src/pages/` - 页面组件 -- `src/compenents/` - 可复用组件 -- `src/assets/` - 静态资源(图片、字体、样式) - -### 样式文件 -- `src/index.less` - 全局样式 -- `src/App.module.less` - 根组件样式 -- 组件样式:`*.module.less` / `*.module.scss` From ab787dc3ae9b25ad8de017a72919c22531f99ea5 Mon Sep 17 00:00:00 2001 From: menft <17554333016@163.com> Date: Sun, 30 Nov 2025 10:51:52 +0800 Subject: [PATCH 5/5] =?UTF-8?q?:art:=20=E5=89=8D=E7=AB=AF=E6=8F=90?= =?UTF-8?q?=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/backend/src/pages/exam/papers/compenents/random-paper.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/backend/src/pages/exam/papers/compenents/random-paper.tsx b/app/backend/src/pages/exam/papers/compenents/random-paper.tsx index 00b072f..d7af0a7 100644 --- a/app/backend/src/pages/exam/papers/compenents/random-paper.tsx +++ b/app/backend/src/pages/exam/papers/compenents/random-paper.tsx @@ -125,7 +125,6 @@ export const RendomPaper: React.FC = ({ type }) => { if (questions.length === 0) return; try { const res: any = await question.getKnowledgeCodes(questions.join(','), type, level); - // httpClient.get 已经 resolve(res.data),所以 res = {code, msg, data} const items = res.data || []; // 后端返回 [{code: "xxx", name: "yyy"}, ...] const options = items.map((item: { code: string; name: string }) => ({