package com.yunhai.bschclassmanage.service.impl; import com.alibaba.fastjson.JSON; import com.yunhai.bschclassmanage.Repository.*; import com.yunhai.bschclassmanage.annotation.OperationLog; import com.yunhai.bschclassmanage.entity.*; import com.yunhai.bschclassmanage.enums.CourseType; import com.yunhai.bschclassmanage.model.constant.LogModule; import com.yunhai.bschclassmanage.model.dto.SchDicCourseDTO; import com.yunhai.bschclassmanage.model.entityField.EntityFieldNames; import com.yunhai.bschclassmanage.model.vo.PageVO; import com.yunhai.bschclassmanage.model.vo.SchDicCourseVO; import com.yunhai.bschclassmanage.service.ISchDicCourseService; import com.yunhai.bschclassmanage.service.ISchDicDivisionService; import com.yunhai.bschclassmanage.service.ISchDicMajorsService; import com.yunhai.bschclassmanage.utils.*; import com.yunhai.platform.utils.exception.DException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.persistence.criteria.Predicate; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @Slf4j @Service public class SchDicCourseService implements ISchDicCourseService { @Autowired private SchDicCourseRepository schDicCourseRepository; @Autowired private SchDicCourseMajorsRepository schDicCourseMajorsRepository; @Autowired private SchDicCourseDivisionRepository schDicCourseDivisionRepository; @Autowired private ISchDicMajorsService schDicMajorsService; @Autowired private ISchDicDivisionService schDicDivisionService; @Autowired private RedisUtil redisUtil; public List getSchDicCourseList(Integer schId) { Specification spec = (root, query, cb) -> { List predicates = new ArrayList<>(); predicates.add(cb.equal(root.get(EntityFieldNames.schId), schId)); return cb.and(predicates.toArray(new Predicate[0])); }; return schDicCourseRepository.findAll(spec); } public List getSchDicCourseCacheList(Integer schId) { String cacheKey = RedisConstants.SCHOOL_COURSE + "_" + schId; if (redisUtil.hasKey(cacheKey)) { // 注意:返回副本或不可变列表,避免外部修改影响缓存 return redisUtil.getList(cacheKey,SchDicCourse.class); } // 同步查询数据库,防止缓存击穿 synchronized (this) { if (redisUtil.hasKey(cacheKey)) { return redisUtil.getList(cacheKey,SchDicCourse.class); } List list = getSchDicCourseList(schId); redisUtil.set(cacheKey, new ArrayList<>(list), RedisConstants.CACHE_EXPIRE_30D); return list; } } public PageVO search(Integer schId, Integer type, String keyword, Integer divisionId, Integer majorsId, Pageable pageable) { // 1. 查询课程分页(动态条件已在 Repository 中处理) Page coursePage = schDicCourseRepository.findCoursesByConditions( schId, type, keyword, divisionId, majorsId, pageable ); if (coursePage.isEmpty()) { return PageVO.empty(pageable.getPageSize()); } // 2. 提取课程 ID 列表 List courseIds = coursePage.getContent().stream() .map(SchDicCourse::getId) .collect(Collectors.toList()); // 3. 批量加载关联关系(按课程ID分组) Map> majorsGroup = loadMajorsGroup(courseIds, schId); Map> divisionGroup = loadDivisionGroup(courseIds, schId); // 4. 获取当前学校的所有专业和学部(用于填充实体详情) Map majorsMap = schDicMajorsService.getSchDicMajorsCacheList(schId) .stream() .collect(Collectors.toMap(SchDicMajors::getId, Function.identity())); Map divisionMap = schDicDivisionService.getSchDicDivisionCacheList(schId) .stream() .collect(Collectors.toMap(SchDicDivision::getId, Function.identity())); // 5. 转换每个课程为 VO List voList = coursePage.getContent().stream() .map(course -> convertToVO(course, majorsGroup, divisionGroup, majorsMap, divisionMap)) .collect(Collectors.toList()); return new PageVO<>(pageable.getPageNumber() + 1, pageable.getPageSize(), coursePage.getTotalElements(), voList); } @OperationLog(module = LogModule.COURSE) @Transactional(rollbackFor = Exception.class) public void saveSchCourse(UserContext.UserInfo user, SchDicCourseDTO dto) { Integer schId = user.getSchId(); // 1. 基础校验 if (dto == null) { throw new DException("课程数据不能为空"); } if (StringUtils.isBlank(dto.getName())) { throw new DException("课程名称不能为空"); } // 2. VO → Entity 映射 SchDicCourse entity = ModelMapperUtil.map(dto, SchDicCourse.class); // 3. 名称唯一性校验 boolean isNew = (entity.getId() == null || entity.getId() == 0); boolean nameExists = isNew ? schDicCourseRepository.existsBySchIdAndName(schId, entity.getName()) : schDicCourseRepository.existsBySchIdAndNameAndIdNot(schId, entity.getName(), entity.getId()); if (nameExists) { throw new DException("课程名称已存在:" + entity.getName()); } SchDicCourse oldEntity = null; Set oldMajors = null; Set oldDivisions = null; // 4. 执行保存(新增或更新) if (isNew) { OtherUtil.setBaseEntityValue(user, entity); // 设置创建/更新审计字段 entity = schDicCourseRepository.save(entity); } else { SchDicCourse existing = schDicCourseRepository.findById(dto.getId()) .orElseThrow(() -> new DException("课程不存在,ID=" + dto.getId())); oldEntity=OtherUtil.deepCopy(existing,SchDicCourse.class); oldMajors = loadMajorsIdSet(dto.getId(),user.getSchId()); oldDivisions = loadDivisionsIdSet(dto.getId(),user.getSchId()); // 复制允许修改的字段(忽略 id, schId, 创建审计字段) OtherUtil.setBaseEntityValue(user, entity); // 设置创建/更新审计字段 BeanUtils.copyProperties(entity, existing, EntityFieldNames.id, EntityFieldNames.schId, EntityFieldNames.createDate, EntityFieldNames.createUserId); // 更新时需单独设置 updateDate / updateUserId(假设 setBaseEntityValue 已处理) entity = schDicCourseRepository.save(existing); } // 5. 维护关联关系(删除旧关联 → 添加新关联) Integer courseId = entity.getId(); // 5.1 删除旧的关联 deleteRelations(courseId, schId); // 5.2 保存新的关联 saveRelations(courseId, schId, dto); // 6. 清除缓存 String cacheKey = RedisConstants.SCHOOL_COURSE + "_" + schId; redisUtil.del(cacheKey); { String description = buildCourseLogDescription(entity, isNew, oldEntity, dto, oldMajors, oldDivisions, schDicMajorsService.toIdAndNameMap(user.getSchId()), schDicDivisionService.toIdAndNameMap(user.getSchId())); LogContextHolder.setDescription(description); } } /** * 构建课程操作的详细日志描述 */ private String buildCourseLogDescription(SchDicCourse newEntity, boolean isNew, SchDicCourse oldEntity, SchDicCourseDTO dto, Set oldMajors, Set oldDivisions, Map majorsNameMap, Map divisionNameMap) { try { List parts = new ArrayList<>(); // ========== 1. 课程本身的变化 ========== if (isNew) { parts.add(String.format("新增课程:%s(编码:%s,类型:%s,别称:%s)", newEntity.getName(), newEntity.getCode() != null ? newEntity.getCode() : "无", CourseType.nameById(newEntity.getType()), newEntity.getNickname() != null ? newEntity.getNickname() : "无")); } else { List changes = new ArrayList<>(); if (!Objects.equals(oldEntity.getName(), newEntity.getName())) { changes.add(String.format("名称从 '%s' 修改为 '%s'", oldEntity.getName(), newEntity.getName())); } if (!Objects.equals(oldEntity.getCode(), newEntity.getCode())) { changes.add(String.format("编码从 '%s' 修改为 '%s'", oldEntity.getCode(), newEntity.getCode())); } if (!Objects.equals(oldEntity.getNickname(), newEntity.getNickname())) { changes.add(String.format("别称从 '%s' 修改为 '%s'", oldEntity.getNickname(), newEntity.getNickname())); } if (!Objects.equals(oldEntity.getType(), newEntity.getType())) { changes.add(String.format("类型从 '%s' 修改为 '%s'", CourseType.nameById(oldEntity.getType()), CourseType.nameById(newEntity.getType()))); } if (changes.isEmpty()) { parts.add("课程信息无变更"); } else { parts.add("课程变更:" + String.join(";", changes)); } } // ========== 2. 关联关系变化(专业) ========== Set newMajors = dto.getMajorsIds(); if (!isNew && oldMajors != null) { Set addedMajors = new HashSet<>(newMajors); addedMajors.removeAll(oldMajors); Set removedMajors = new HashSet<>(oldMajors); removedMajors.removeAll(newMajors); if (!addedMajors.isEmpty()) { parts.add("绑定专业:" + getNames(addedMajors, majorsNameMap)); } if (!removedMajors.isEmpty()) { parts.add("解绑专业:" + getNames(removedMajors, majorsNameMap)); } } else if (isNew && newMajors != null && !newMajors.isEmpty()) { parts.add("绑定专业:" + getNames(newMajors, majorsNameMap)); } // ========== 3. 关联关系变化(学部) ========== Set newDivisions = dto.getDivisionIds(); if (!isNew && oldDivisions != null) { Set addedDivs = new HashSet<>(newDivisions); addedDivs.removeAll(oldDivisions); Set removedDivs = new HashSet<>(oldDivisions); removedDivs.removeAll(newDivisions); if (!addedDivs.isEmpty()) { parts.add("绑定学部:" + getNames(addedDivs, divisionNameMap)); } if (!removedDivs.isEmpty()) { parts.add("解绑学部:" + getNames(removedDivs, divisionNameMap)); } } else if (isNew && newDivisions != null && !newDivisions.isEmpty()) { parts.add("绑定学部:" + getNames(newDivisions, divisionNameMap)); } return String.join(";", parts); } catch (Exception e) { // 日志构建失败时返回简短描述,不影响主流程 log.error("构建课程日志描述失败:{}", e.getMessage()); return isNew ? "新增课程(日志生成失败)" : "更新课程(日志生成失败)"; } } /** * 将 Set 转换为名称字符串(用"、"分隔) */ private String getNames(Set ids, Map nameMap) { if (ids == null || ids.isEmpty()) return "无"; return ids.stream() .map(id -> nameMap.getOrDefault(id, String.valueOf(id))) .collect(Collectors.joining("、")); } @OperationLog(module = LogModule.COURSE) @Transactional(rollbackFor = Exception.class) public void delete(UserContext.UserInfo user,Integer id){ SchDicCourse entity= schDicCourseRepository.findByIdAndSchId(id,user.getSchId()) .orElseThrow(() -> new DException( "课程不存在")); String cacheKey = RedisConstants.SCHOOL_COURSE + "_" + user.getSchId(); redisUtil.del(cacheKey); deleteRelations(id, user.getSchId()); schDicCourseRepository.deleteById(id); String description = String.format("删除课程:%s(编码:%s,类型:%s)", entity.getName(), entity.getCode() != null ? entity.getCode() : "无", CourseType.nameById(entity.getType())); LogContextHolder.setDescription(description); log.debug("操作人:{},删除课程日志:{}", user.getUserName(), JSON.toJSONString(entity)); } // ==================== 私有辅助方法 ==================== /** * 批量删除课程-专业、课程-学部关联 */ private void deleteRelations(Integer courseId, Integer schId) { schDicCourseMajorsRepository.deleteByCourseIdAndSchId(courseId, schId); schDicCourseDivisionRepository.deleteByCourseIdAndSchId(courseId, schId); } /** * 批量保存新的课程-专业、课程-学部关联 */ private void saveRelations(Integer courseId, Integer schId, SchDicCourseDTO dto) { // 保存专业关联 if (CollectionUtils.isNotEmpty(dto.getMajorsIds())) { List majorsList = dto.getMajorsIds().stream() .filter(Objects::nonNull) .map(id -> { SchDicCourseMajors rel = new SchDicCourseMajors(); rel.setCourseId(courseId); rel.setMajorsId(id); rel.setSchId(schId); return rel; }) .collect(Collectors.toList()); if (!majorsList.isEmpty()) { schDicCourseMajorsRepository.saveAll(majorsList); } } // 保存学部关联 if (CollectionUtils.isNotEmpty(dto.getDivisionIds())) { List divisionList = dto.getDivisionIds().stream() .filter(Objects::nonNull) .map(id -> { SchDicCourseDivision rel = new SchDicCourseDivision(); rel.setCourseId(courseId); rel.setDivisionId(id); rel.setSchId(schId); return rel; }) .collect(Collectors.toList()); if (!divisionList.isEmpty()) { schDicCourseDivisionRepository.saveAll(divisionList); } } } /** * 批量加载课程-专业关联关系,并按 courseId 分组 */ private Map> loadMajorsGroup(List courseIds, Integer schId) { if (courseIds.isEmpty()) { return Collections.emptyMap(); } return schDicCourseMajorsRepository.findByCourseIdInAndSchId(courseIds, schId) .stream() .collect(Collectors.groupingBy(SchDicCourseMajors::getCourseId)); } /** * 加载指定课程关联的专业列表(单个) */ private List loadMajorsList(Integer courseId, Integer schId) { if (courseId == null) { return Collections.emptyList(); } return schDicCourseMajorsRepository.findByCourseIdAndSchId(courseId, schId); } /** * 加载指定课程关联的专业ID集合(用于日志) */ private Set loadMajorsIdSet(Integer courseId, Integer schId) { return loadMajorsList(courseId, schId).stream() .map(SchDicCourseMajors::getMajorsId) .collect(Collectors.toSet()); } /** * 批量加载课程-学部关联关系,并按 courseId 分组 */ private Map> loadDivisionGroup(List courseIds, Integer schId) { if (courseIds.isEmpty()) { return Collections.emptyMap(); } return schDicCourseDivisionRepository.findByCourseIdInAndSchId(courseIds, schId) .stream() .collect(Collectors.groupingBy(SchDicCourseDivision::getCourseId)); } private List loadDivisionsList(Integer courseId, Integer schId) { if (courseId == null) { return Collections.emptyList(); } return schDicCourseDivisionRepository.findByCourseIdAndSchId(courseId, schId); } private Set loadDivisionsIdSet(Integer courseId, Integer schId) { return loadDivisionsList(courseId, schId).stream() .map(SchDicCourseDivision::getDivisionId) .collect(Collectors.toSet()); } /** * 将单个课程实体转换为 VO,并填充关联列表 */ private SchDicCourseVO convertToVO(SchDicCourse course, Map> majorsGroup, Map> divisionGroup, Map majorsEntityMap, Map divisionEntityMap) { // 使用 ModelMapperUtil 复制基础字段 SchDicCourseVO vo = ModelMapperUtil.map(course, SchDicCourseVO.class); if (vo == null&& course != null) throw new DException("数据转换失败"); // 填充 typeName vo.setTypeName(CourseType.nameById(course.getType())); // 填充 majorsList List majorsRel = majorsGroup.getOrDefault(course.getId(), Collections.emptyList()); List majors = majorsRel.stream() .map(rel -> majorsEntityMap.get(rel.getMajorsId())) .filter(Objects::nonNull) .collect(Collectors.toList()); // 按 sort 升序,null 值排在最后 majors.sort(Comparator.comparing(SchDicMajors::getSort, Comparator.nullsLast(Comparator.naturalOrder()))); vo.setMajorsList(majors); // 填充 divisionList List divisionRel = divisionGroup.getOrDefault(course.getId(), Collections.emptyList()); List divisions = divisionRel.stream() .map(rel -> divisionEntityMap.get(rel.getDivisionId())) .filter(Objects::nonNull) .collect(Collectors.toList()); // 按 sort 升序,null 值排在最后 divisions.sort(Comparator.comparing(SchDicDivision::getSort, Comparator.nullsLast(Comparator.naturalOrder()))); vo.setDivisionList(divisions); return vo; } }