Merge remote-tracking branch 'origin/master'

This commit is contained in:
penpenwang 2025-11-30 15:49:15 +08:00
commit 018c447227
45 changed files with 1988 additions and 113 deletions

View File

@ -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);

View File

@ -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)

View File

@ -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();

View File

@ -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)
@ -74,6 +78,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 +87,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 +131,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<ExamQuestionCategory> questionCategories = new ArrayList<>();
if (StringUtil.isNotNull(categoryId)) {
@ -147,6 +154,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 +177,7 @@ public class QuestionController {
req.getCategoryId(),
req.getContent().replaceAll(" ", ""),
req.getLevel(),
req.getKnowledgeCode(),
req.getType(),
BCtx.getId());
@ -198,6 +207,7 @@ public class QuestionController {
examQuestion.getId(),
req.getContent().replaceAll(" ", ""),
req.getLevel(),
req.getKnowledgeCode(),
req.getType(),
req.getCategoryId(),
BCtx.getId());
@ -1172,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<String, Object> 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<String> knowledgeCodes =
examQuestionService.getDistinctKnowledgeCodes(categoryIds, type, level);
if (knowledgeCodes.isEmpty()) {
return JsonResponse.data(List.of());
}
// 查询知识点名称
List<Knowledge> knowledgeList = knowledgeService.getByKnowledgeCodes(knowledgeCodes);
Map<String, String> codeToName =
knowledgeList.stream()
.collect(
Collectors.toMap(
Knowledge::getKnowledgeCode,
Knowledge::getName,
(v1, v2) -> v1));
// 构建返回结果
List<Map<String, String>> result =
knowledgeCodes.stream()
.map(
code -> {
Map<String, String> item = new HashMap<>();
item.put("code", code);
item.put("name", codeToName.getOrDefault(code, code));
return item;
})
.collect(Collectors.toList());
return JsonResponse.data(result);
}
}

View File

@ -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<String, Object> 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<String, Object> params) {
Integer bookId = MapUtils.getInteger(params, "bookId");
if (bookId == null && StringUtil.isNull(bookId)) {
return JsonResponse.error("请传入教材id");
}
HashMap<String, Object> data = new HashMap<>();
// 只返回树形章节结构
data.put("chapters", bookChapterService.groupByParentByBookId(bookId));
return JsonResponse.data(data);
//
// HashMap<String, Object> data = new HashMap<>();
// data.put("departments", bookChapterService.groupByParentByFromScene(fromScene));
//
// HashMap<Integer, Integer> depUserCount = new HashMap<>();
// List<BookChapter> allDepartmentList = bookChapterService.allByFromScene(fromScene);
// if (StringUtil.isNotEmpty(allDepartmentList)) {
// for (BookChapter dep : allDepartmentList) {
// List<Integer> 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<BookChapter> childDepartmentList =
// bookChapterService.getChildDepartmentsByParentChain(
// dep.getId(), parentChain);
// if (StringUtil.isNotEmpty(childDepartmentList)) {
// depIds.addAll(childDepartmentList.stream().map(BookChapter::getId).toList());
// }
//// List<Integer> 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<String, Object> data = new HashMap<>();
data.put("children", bookChapterService.listByParentId(id));
return JsonResponse.data(data);
}
}

View File

@ -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<String, Object> params) {
/** 调用服务层分页查询方法Service 层已包含完整的分页信息 */
PaginationResult<JCResource> result = resourceService.paginate(params);
HashMap<String, Object> 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<Integer> idList = Arrays.stream(idListStr.split(","))
.map(String::trim)
.filter(str -> !str.isEmpty())
.map(Integer::parseInt)
.collect(Collectors.toList());
resourceService.removeByIds(idList);
}
return JsonResponse.success();
}

View File

@ -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<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Knowledge::getKnowledgeCode, (Object[]) codeArray);
List<Knowledge> list = knowledgeService.list(queryWrapper);
return JsonResponse.data(list);
}
}

View File

@ -8,8 +8,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 +19,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;
@ -58,7 +63,8 @@ public class TextbookController {
@Autowired private UserService userService;
@Autowired private GroupService groupService;
@Autowired
private ServerConfig serverConfig;
@GetMapping("/index")
public JsonResponse index(@RequestParam HashMap<String, Object> params) {
@ -68,24 +74,16 @@ public class TextbookController {
data.put("data", result.getData());
data.put("total", result.getTotal());
// 课程封面资源ID
List<Integer> 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<Integer> 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<Integer, Integer> 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<Integer, Integer> book_user_count = new HashMap<>();
@ -203,13 +201,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<String, Object> data = new HashMap<>();
data.put("textbook", textbook);
List<Integer> rids = new ArrayList<>();
rids.add(textbook.getThumb());
// 获取签名url
data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids));
// List<Integer> rids = new ArrayList<>();
// rids.add(textbook.getThumb());
// 获取签名url
// data.put("resource_url", jciResourceService.chunksPreSignUrlByIds(rids));
// 指派范围
List<BookDepartmentUser> courseDepartmentUserList =
@ -273,6 +275,22 @@ public class TextbookController {
return JsonResponse.data(list);
}
/**
* 获取教材下拉选择列表轻量级仅返回id和title
*/
@GetMapping("/selectList")
public JsonResponse selectList() {
List<Textbook> list = textbookService.list();
List<Map<String, Object>> result = new ArrayList<>();
for (Textbook textbook : list) {
Map<String, Object> 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,4 +365,4 @@ public class TextbookController {
textbookService.updateById(textbook);
return JsonResponse.success();
}
}
}

View File

@ -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());

View File

@ -961,41 +961,53 @@ public class PaperController {
JSONObject source = randomRules.getJSONObject("source");
List<Integer> 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);
}

View File

@ -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<String, Object> 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<Integer> userJoinDepIds = userService.getDepIdsByUserId(FCtx.getId());
if (userJoinDepIds == null) {
return JsonResponse.error("当前学员未加入任何部门");
}
if (!userJoinDepIds.contains(depId)) {
return JsonResponse.error("当前学员未加入所选择部门");
}
HashMap<String, Object> data = new HashMap<>();
data.put("learn_course_records", new HashMap<>());
// 获取所有部门ID
List<Integer> depIds = new ArrayList<>();
depIds.add(depId);
Department department = departmentService.findOrFail(depId);
String parentChain = department.getParentChain();
if (StringUtil.isNotEmpty(parentChain)) {
List<Integer> parentChainList =
Arrays.stream(parentChain.split(",")).map(Integer::parseInt).toList();
if (StringUtil.isNotEmpty(parentChainList)) {
depIds.addAll(parentChainList);
}
}
List<Integer> 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<Textbook> courses = new ArrayList<>();
// 读取部门课
List<Textbook> 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<Integer> 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<String, Object> params) {
Integer bookId = MapUtils.getInteger(params, "bookId");
if (bookId == null && StringUtil.isNull(bookId)) {
return JsonResponse.error("请传入教材id");
}
HashMap<String, Object> data = new HashMap<>();
// 只返回树形章节结构
data.put("chapters", bookChapterService.groupByParentByBookId(bookId));
return JsonResponse.data(data);
}
}

View File

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

View File

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

View File

@ -12,14 +12,26 @@ public class ExamQuestionFilter {
private List<Integer> 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;
}

View File

@ -17,6 +17,8 @@ public class ExamQuestionPaginateFilter {
private String categoryId;
private String knowledgeCode;
private List<Integer> adminIds;
private String sortField;

View File

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

View File

@ -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;
/** 层级 */

View File

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

View File

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

View File

@ -26,7 +26,7 @@ public class TextbookRequestDTO {
/** 封面地址 */
@TableField("thumb")
private Integer thumb;
private String thumb;
/** 简介 */
@TableField("short_desc")

View File

@ -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<Integer> depIds;
private List<Integer> userIds;
}

View File

@ -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<BookDepartmentUser> {
List<BookDepartmentUser> chunksByDepIdsOrUserIdsOrGroupIds(
List<Integer> depIds, List<Integer> userIds, List<Integer> groupIds);
}

View File

@ -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<BookChapter> {
/** 删除章节时做子节点/内容校验(需要的话) */
void removeChapter(Integer id);
// List<Integer> getUserIdsByDepIds(List<Integer> depIds);
Map<Integer, List<BookChapter>> groupByParentByBookId(Integer bookId);
List<BookChapter> allByFromScene(Integer fromScene);
List<BookChapter> getChildDepartmentsByParentChain(Integer parentId, String parentChain);
void destroy(Integer id) throws NotFoundException;
List<BookChapter> listByParentId(Integer id);
void changeParent(Integer id, Integer parentId, List<Integer> ids) throws NotFoundException;
void resetSort(List<Integer> 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;
}

View File

@ -12,4 +12,6 @@ public interface IBookDepartmentUserService extends IService<BookDepartmentUser>
List<BookDepartmentUser> chunksByBookIds(List<Integer> bookIds);
List<BookDepartmentUser> chunksByCourseId(Integer courseId);
List<Integer> getCourseIdsByDepIdsOrUserIds(List<Integer> depIds, List<Integer> userIds);
}

View File

@ -20,5 +20,12 @@ public interface IKnowledgeService extends IService<Knowledge> {
JSONObject getByIdVo(Integer id);
JSONObject stuGetByIdVo(Integer id);
/**
* 根据知识点编码列表批量查询知识点
*
* @param codes 知识点编码列表
* @return 知识点列表
* @author menft
*/
List<Knowledge> getByKnowledgeCodes(List<String> codes);
}

View File

@ -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> {
Textbook findOrFail(Integer id) throws NotFoundException;
List<Textbook> getDepCoursesAndShow(
TextbookUserDTO textbookUserDTO);
}

View File

@ -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<JCResource> {
Map<Integer, String> chunksPreSignUrlByIds(List<Integer> ids);
Map<Integer, String> chunksPreSignUrlByIds(List<String> ids);
PaginationResult<JCResource> paginate(HashMap<String, Object> params);
}

View File

@ -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<Integer, List<BookChapter>> 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<Integer> getUserIdsByDepIds(List<Integer> depIds) {
// if (StringUtil.isEmpty(depIds)) {
// return new ArrayList<>();
// }
// return list(query().getWrapper().in("dep_id", depIds)).stream()
// .map(UserDepartment::getUserId)
// .toList();
// }
@Override
public List<BookChapter> 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<BookChapter> 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<BookChapter> listByParentId(Integer id) {
return list(query().getWrapper().eq("parent_id", id).orderByAsc("sort"));
}
@Override
public void changeParent(Integer id, Integer parentId, List<Integer> ids)
throws NotFoundException {
BookChapter bookChapter = findOrFail(id);
update(bookChapter, bookChapter.getName(), parentId, bookChapter.getSort());
// 重置排序
resetSort(ids);
}
@Override
public void resetSort(List<Integer> ids) {
if (ids == null || ids.isEmpty()) {
return;
}
List<BookChapter> 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<BookChapter> children =
list(query().getWrapper().like("chapter_code", oldChildrenPC + "%"));
if (children.isEmpty()) {
return;
}
ArrayList<BookChapter> 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;
}
}

View File

@ -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<BookDepartmentUserMapper, BookDepartmentUser>
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<Integer> getCourseIdsByDepIdsOrUserIds(
List<Integer> depIds, List<Integer> userIds) {
List<Integer> groupIds = new ArrayList<>();
List<UserGroup> userGroupList = userGroupService.chunksByUserIds(userIds);
if (StringUtil.isNotEmpty(userGroupList)) {
groupIds.addAll(userGroupList.stream().map(UserGroup::getGroupId).toList());
}
List<BookDepartmentUser> departmentUserList =
getBaseMapper().chunksByDepIdsOrUserIdsOrGroupIds(depIds, userIds, groupIds);
if (StringUtil.isEmpty(departmentUserList)) {
return new ArrayList<>();
}
return departmentUserList.stream().map(BookDepartmentUser::getBookId).toList();
}
}

View File

@ -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<JCResourceMapper, JCResource>
@ -24,9 +33,85 @@ public class JCResourceServiceImpl
@Autowired
private AppConfigService appConfigService;
@Override
public Map<Integer, String> chunksPreSignUrlByIds(List<Integer> ids) {
public PaginationResult<JCResource> paginate(HashMap<String, Object> params) {
try {
/** 获取分页参数默认第1页每页10条 */
Integer page = MapUtils.getInteger(params, "page", 1);
Integer size = MapUtils.getInteger(params, "size", 10);
/** 创建分页对象 */
Page<JCResource> pageParam = new Page<>(page, size);
/** 创建 Lambda 条件构造器,用于构建类型安全的查询条件 */
LambdaQueryWrapper<JCResource> 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<JCResource> pageResult = this.page(pageParam, queryWrapper);
/** 计算总页数 */
Long total = pageResult.getTotal();
Long pages = (total + size - 1) / size; // 向上取整
/** 构建返回结果,包含完整的分页信息 */
PaginationResult<JCResource> 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<JCResource> 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<Integer, String> chunksPreSignUrlByIds(List<String> ids) {
S3Util s3Util = new S3Util(appConfigService.getS3Config());
Map<Integer, String> preSignUrlMap = new HashMap<>();

View File

@ -47,7 +47,8 @@ public class KnowledgeServiceImpl extends ServiceImpl<KnowledgeMapper, Knowledge
public List<Knowledge> listVo(KnowledgeParam param) {
//获取知识点卡片
LambdaQueryWrapper<Knowledge> 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);
}
@ -216,6 +217,16 @@ public class KnowledgeServiceImpl extends ServiceImpl<KnowledgeMapper, Knowledge
}
}
@Override
public List<Knowledge> getByKnowledgeCodes(List<String> codes) {
if (codes == null || codes.isEmpty()) {
return new ArrayList<>();
}
LambdaQueryWrapper<Knowledge> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(Knowledge::getKnowledgeCode, codes);
return list(queryWrapper);
}
/**
* 递增编码
* @param code 当前编码

View File

@ -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<Textbook> 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<Textbook> getDepCoursesAndShow(
TextbookUserDTO textbookUserDTO) {
if (StringUtil.isEmpty(textbookUserDTO.getDepIds())) {
return new ArrayList<>();
}
// List<Textbook> res = new ArrayList<>();
List<Integer> courseIds =
bookDepartmentUserService.getCourseIdsByDepIdsOrUserIds(textbookUserDTO.getDepIds(), textbookUserDTO.getUserIds());
if (StringUtil.isEmpty(courseIds)) {
return new ArrayList<>();
}
// if (categoryId != null && categoryId > 0) {
// // 获取所有子类
// List<Integer> allCategoryIdsList =
// categoryService.getChildCategoryIdsByParentId(categoryId + "");
// List<Integer> 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<Textbook> 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<Textbook> res = list(queryWrapper);
for (Textbook re : res) {
re.setAllUrl(serverConfig.getUrl() + re.getThumb());
re.setUrl(serverConfig.getUrl());
}
return res;
}
}

View File

@ -24,5 +24,22 @@
update_time,
tenant_id
</sql>
<select id="chunksByDepIdsOrUserIdsOrGroupIds" resultType="xyz.playedu.jc.domain.BookDepartmentUser">
SELECT DISTINCT `jc_book_department_user`.*
FROM `jc_book_department_user`
<where>
<if test="depIds != null and !depIds.isEmpty()">
OR (`jc_book_department_user`.`range_id` IN (<foreach collection="depIds" item="tmpId" separator=",">
#{tmpId}</foreach>) AND `jc_book_department_user`.`type` = 0)
</if>
<if test="userIds != null and !userIds.isEmpty()">
OR (`jc_book_department_user`.`range_id` IN (<foreach collection="userIds" item="tmpId" separator=",">
#{tmpId}</foreach>) AND `jc_book_department_user`.`type` = 1)
</if>
<if test="groupIds != null and !groupIds.isEmpty()">
OR (`jc_book_department_user`.`range_id` IN (<foreach collection="groupIds" item="tmpId" separator=",">
#{tmpId}</foreach>) AND `jc_book_department_user`.`type` = 2)
</if>
</where>
</select>
</mapper>

View File

@ -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);

View File

@ -22,4 +22,6 @@ public interface ExamQuestionMapper extends BaseMapper<ExamQuestion> {
Long paginateCount(ExamQuestionPaginateFilter filter);
List<ExamQuestion> chunksByCategoryIdAndLimit(ExamQuestionFilter filter);
List<String> getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level);
}

View File

@ -20,12 +20,18 @@ public interface ExamQuestionService extends IService<ExamQuestion> {
PaginationResult<ExamQuestion> 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);
@ -37,4 +43,11 @@ public interface ExamQuestionService extends IService<ExamQuestion> {
List<ExamQuestion> chunksByCategoryId(Integer categoryId);
List<ExamQuestion> chunksByCategoryIdAndLimit(ExamQuestionFilter filter);
/**
* 根据题库ID题型难度获取去重的知识点编码列表
*
* @author menft
*/
List<String> getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level);
}

View File

@ -50,13 +50,19 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
@Override
public Integer create(
Integer categoryId, String content, Integer level, Integer type, Integer adminId) {
Integer categoryId,
String content,
Integer level,
String knowledgeCode,
Integer type,
Integer adminId) {
ExamQuestion examQuestion =
new ExamQuestion() {
{
setCategoryId(categoryId);
setAdminId(adminId);
setLevel(level);
setKnowledgeCode(knowledgeCode);
setType(type);
setContent(content);
setCreatedAt(new Date());
@ -72,6 +78,7 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
Integer id,
String content,
Integer level,
String knowledgeCode,
Integer type,
Integer categoryId,
Integer adminId) {
@ -82,6 +89,7 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
question.setCategoryId(categoryId);
question.setContent(content);
question.setLevel(level);
question.setKnowledgeCode(knowledgeCode);
question.setType(type);
question.setUpdatedAt(new Date());
@ -108,4 +116,22 @@ public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, Exa
public List<ExamQuestion> chunksByCategoryIdAndLimit(ExamQuestionFilter filter) {
return getBaseMapper().chunksByCategoryIdAndLimit(filter);
}
@Override
public List<String> getDistinctKnowledgeCodes(String categoryIds, Integer type, Integer level) {
// 获取所有 knowledge_code 字段可能是逗号分隔的多个值
List<String> 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());
}
}

View File

@ -10,6 +10,7 @@
<result property="adminId" column="admin_id" jdbcType="INTEGER"/>
<result property="type" column="type" jdbcType="INTEGER"/>
<result property="level" column="level" jdbcType="TINYINT"/>
<result property="knowledgeCode" column="knowledge_code" jdbcType="VARCHAR"/>
<result property="content" column="content" jdbcType="VARCHAR"/>
<result property="createdAt" column="created_at" jdbcType="TIMESTAMP"/>
<result property="updatedAt" column="updated_at" jdbcType="TIMESTAMP"/>
@ -18,7 +19,7 @@
<sql id="Base_Column_List">
id,category_id,admin_id,type,
level,content,created_at,
level,knowledge_code,content,created_at,
updated_at,deleted
</sql>
@ -42,6 +43,9 @@
<if test="level != null">
AND `exam_question`.`level` = #{level}
</if>
<if test="knowledgeCode != null and knowledgeCode != ''">
AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`)
</if>
</where>
<if test="sortAlgo == 'asc'">
@ -87,6 +91,9 @@
<if test="level != null">
AND `exam_question`.`level` = #{level}
</if>
<if test="knowledgeCode != null and knowledgeCode != ''">
AND FIND_IN_SET(#{knowledgeCode}, `exam_question`.`knowledge_code`)
</if>
</where>
</select>
@ -100,6 +107,16 @@
#{categoryId}
</foreach>
</if>
<if test="type1Level != null">
and `exam_question`.`level` = #{type1Level}
</if>
<if test="type1KnowledgeCodes != null and type1KnowledgeCodes != ''">
and (
<foreach collection="type1KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type1Number})
@ -115,6 +132,16 @@
#{categoryId}
</foreach>
</if>
<if test="type2Level != null">
and `exam_question`.`level` = #{type2Level}
</if>
<if test="type2KnowledgeCodes != null and type2KnowledgeCodes != ''">
and (
<foreach collection="type2KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type2Number})
@ -130,6 +157,16 @@
#{categoryId}
</foreach>
</if>
<if test="type3Level != null">
and `exam_question`.`level` = #{type3Level}
</if>
<if test="type3KnowledgeCodes != null and type3KnowledgeCodes != ''">
and (
<foreach collection="type3KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type3Number})
@ -145,6 +182,16 @@
#{categoryId}
</foreach>
</if>
<if test="type4Level != null">
and `exam_question`.`level` = #{type4Level}
</if>
<if test="type4KnowledgeCodes != null and type4KnowledgeCodes != ''">
and (
<foreach collection="type4KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type4Number})
@ -160,6 +207,16 @@
#{categoryId}
</foreach>
</if>
<if test="type5Level != null">
and `exam_question`.`level` = #{type5Level}
</if>
<if test="type5KnowledgeCodes != null and type5KnowledgeCodes != ''">
and (
<foreach collection="type5KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type5Number})
@ -175,8 +232,36 @@
#{categoryId}
</foreach>
</if>
<if test="type6Level != null">
and `exam_question`.`level` = #{type6Level}
</if>
<if test="type6KnowledgeCodes != null and type6KnowledgeCodes != ''">
and (
<foreach collection="type6KnowledgeCodes.split(',')" item="code" separator=" OR ">
FIND_IN_SET(#{code}, `exam_question`.`knowledge_code`)
</foreach>
)
</if>
</where>
ORDER BY RAND()
LIMIT #{type6Number})
</select>
<select id="getDistinctKnowledgeCodes" resultType="java.lang.String">
SELECT DISTINCT `knowledge_code`
FROM `exam_question`
<where>
AND `knowledge_code` IS NOT NULL
AND `knowledge_code` != ''
<if test="categoryIds != null and categoryIds != ''">
AND `category_id` IN (${categoryIds})
</if>
<if test="type != null">
AND `type` = #{type}
</if>
<if test="level != null">
AND `level` = #{level}
</if>
</where>
</select>
</mapper>

View File

@ -155,7 +155,7 @@ public class ResourceTranscodeInfoServiceImpl
|| timestamp == null
|| StringUtil.isEmpty(definition)
|| StringUtil.isEmpty(sign)) {
throw new ServiceException("参数为空");
throw new ServiceException("参数为空2");
}
String str =

View File

@ -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,
});
}
@ -123,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,
});
}

View File

@ -228,6 +228,25 @@ export function GetChapterContentApi(chapterId: number, bookId: number) {
return client.get(`/backend/v1/jc/chapter-content/${chapterId}`, { bookId });
}
/*
* 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
* */

View File

@ -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<any>([]);
const [selectVideos, setSelectVideos] = useState<any[]>([]);
const [categories, setCategories] = useState<Option[]>([]);
const [textbookList, setTextbookList] = useState<any[]>([]); // 教材列表
const [selectedTextbookId, setSelectedTextbookId] = useState<number | undefined>(undefined); // 选中的教材ID
const [knowledgeCode, setKnowledgeCode] = useState('');
const [knowledgeList, setKnowledgeList] = useState<any[]>([]);
const [knowledgeLoading, setKnowledgeLoading] = useState(false); // 知识点加载状态
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
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')}
/>
</div>
</div>
<div className="d-flex mb-24">
<div className="d-flex mr-16">
<Typography.Text></Typography.Text>
<Select
style={{ width: 150 }}
placeholder="请选择教材"
value={selectedTextbookId}
allowClear
showSearch
filterOption={(input: string, option: any) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
onChange={handleTextbookChange}
options={textbookList.map((item: any) => ({
label: item.title,
value: item.id,
}))}
/>
</div>
<div className="d-flex mr-16">
<Typography.Text></Typography.Text>
<Select
style={{ width: 150 }}
placeholder={selectedTextbookId ? "请选择知识点" : "请先选择教材"}
value={knowledgeCode || undefined}
disabled={!selectedTextbookId}
loading={knowledgeLoading}
allowClear
showSearch
filterOption={(input: string, option: any) =>
(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,
}))}
/>
</div>
<Button className="mr-16" onClick={resetList}>
{t('commen.reset')}
</Button>

View File

@ -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<PropInterface> = ({ 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<Record<number, any[]>>({
1: [],
2: [],
3: [],
4: [],
5: [],
6: [],
});
useEffect(() => {
@ -91,6 +120,23 @@ export const RendomPaper: React.FC<PropInterface> = ({ 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);
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 +233,8 @@ export const RendomPaper: React.FC<PropInterface> = ({ 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 +242,32 @@ export const RendomPaper: React.FC<PropInterface> = ({ 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 +275,33 @@ export const RendomPaper: React.FC<PropInterface> = ({ 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 +688,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={choice.level}
onChange={(value) => {
const obj = { ...choice };
obj.level = value;
obj.knowledge_codes = '';
setChoice(obj);
loadKnowledgeCodes(1, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[1]}
value={choice.knowledge_codes ? choice.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...choice };
obj.knowledge_codes = values.join(',');
setChoice(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(1, choice.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -619,7 +734,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q2 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.select.label')}({t('exam.paper.compose.text1')}
@ -679,6 +794,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text5')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={select.level}
onChange={(value) => {
const obj = { ...select };
obj.level = value;
obj.knowledge_codes = '';
setSelect(obj);
loadKnowledgeCodes(2, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[2]}
value={select.knowledge_codes ? select.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...select };
obj.knowledge_codes = values.join(',');
setSelect(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(2, select.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -689,7 +840,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q3 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.input.label')}({t('exam.paper.compose.text1')}
@ -730,6 +881,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={input.level}
onChange={(value) => {
const obj = { ...input };
obj.level = value;
obj.knowledge_codes = '';
setInput(obj);
loadKnowledgeCodes(3, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[3]}
value={input.knowledge_codes ? input.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...input };
obj.knowledge_codes = values.join(',');
setInput(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(3, input.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -740,7 +927,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q4 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.judge.label')}({t('exam.paper.compose.text1')}
@ -781,6 +968,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={judge.level}
onChange={(value) => {
const obj = { ...judge };
obj.level = value;
obj.knowledge_codes = '';
setJudge(obj);
loadKnowledgeCodes(4, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[4]}
value={judge.knowledge_codes ? judge.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...judge };
obj.knowledge_codes = values.join(',');
setJudge(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(4, judge.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -791,7 +1014,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q5 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.qa.label')}({t('exam.paper.compose.text1')}
@ -832,6 +1055,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={qa.level}
onChange={(value) => {
const obj = { ...qa };
obj.level = value;
obj.knowledge_codes = '';
setQa(obj);
loadKnowledgeCodes(5, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[5]}
value={qa.knowledge_codes ? qa.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...qa };
obj.knowledge_codes = values.join(',');
setQa(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(5, qa.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}
@ -842,7 +1101,7 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
)}
{q6 > 0 && (
<div className={styles['config-item']}>
<div className="d-flex">
<div className="d-flex" style={{ flexWrap: 'wrap', gap: '8px 0' }}>
<div className={styles['label']}>
<span className="c-red">*</span>
{t('exam.question.cap.label')}({t('exam.paper.compose.text1')}
@ -883,6 +1142,42 @@ export const RendomPaper: React.FC<PropInterface> = ({ type }) => {
></InputNumber>
{t('exam.paper.compose.text4')}
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 100, marginLeft: 8 }}
size="large"
options={levelOptions}
value={cap.level}
onChange={(value) => {
const obj = { ...cap };
obj.level = value;
obj.knowledge_codes = '';
setCap(obj);
loadKnowledgeCodes(6, value);
}}
allowClear
placeholder="不限"
/>
</div>
<div className="d-flex ml-30">
<Select
style={{ width: 200, marginLeft: 8 }}
size="large"
mode="multiple"
options={knowledgeOptions[6]}
value={cap.knowledge_codes ? cap.knowledge_codes.split(',') : []}
onChange={(values: string[]) => {
const obj = { ...cap };
obj.knowledge_codes = values.join(',');
setCap(obj);
}}
maxTagCount="responsive"
placeholder="不限"
onFocus={() => loadKnowledgeCodes(6, cap.level)}
/>
</div>
</div>
<div className="d-flex">
{t('exam.paper.compose.text1')}

View File

@ -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<PropInterface> = ({ id, open, onCan
const [loading, setLoading] = useState(false);
const [refresh, setRefresh] = useState(false);
const [type, setType] = useState('1');
const [cascaderOptions, setCascaderOptions] = useState<CascaderOption[]>([]); // 级联选择器选项
const [formParams, setFormParams] = useState({
v: 'v1',
d: {
@ -38,6 +49,7 @@ export const QuestionsDetailCreate: React.FC<PropInterface> = ({ id, open, onCan
setType('1');
form.setFieldsValue({
level: 1,
knowledge_cascader: [],
});
setFormParams({
v: 'v1',
@ -46,10 +58,49 @@ export const QuestionsDetailCreate: React.FC<PropInterface> = ({ 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<PropInterface> = ({ 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<PropInterface> = ({ id, open, onCan
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label="关联知识点"
name="knowledge_cascader"
>
<Cascader
options={cascaderOptions}
loadData={loadKnowledgeData as CascaderProps<CascaderOption>['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
),
}}
/>
</Form.Item>
<Form.Item
label={t('exam.question.detail.edit.name')}
name="content"

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Modal, Form, Radio, Spin, message } from 'antd';
import { question } from '../../../../api/index';
import { Modal, Form, Radio, Spin, message, Cascader } from 'antd';
import type { CascaderProps } from 'antd';
import { question, textbook } from '../../../../api/index';
import { QuestionInput } from '../../../../compenents';
import { QChoice } from './choice';
import { QSelect } from './select';
@ -10,6 +11,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;
qid: number;
@ -25,6 +35,7 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
const [refresh, setRefresh] = useState(false);
const [type, setType] = useState('1');
const [resourceUrl, setResourceUrl] = useState<ResourceUrlModel>({});
const [cascaderOptions, setCascaderOptions] = useState<CascaderOption[]>([]); // 级联选择器选项
const [formParams, setFormParams] = useState({
v: 'v1',
d: {
@ -40,21 +51,107 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ 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]);
});
};
const onFinish = (values: any) => {
@ -268,12 +365,24 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ 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) => {
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) => {
@ -438,6 +547,26 @@ export const QuestionsDetailUpdate: React.FC<PropInterface> = ({ id, qid, open,
</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="关联知识点" name="knowledge_cascader">
<Cascader
options={cascaderOptions}
loadData={loadKnowledgeData as CascaderProps<CascaderOption>['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
),
}}
/>
</Form.Item>
<Form.Item
label={t('exam.question.detail.edit.name')}
name="content"