467 lines
20 KiB
Java
467 lines
20 KiB
Java
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<SchDicCourse> getSchDicCourseList(Integer schId) {
|
||
Specification<SchDicCourse> spec = (root, query, cb) -> {
|
||
List<Predicate> 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<SchDicCourse> 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<SchDicCourse> list = getSchDicCourseList(schId);
|
||
|
||
redisUtil.set(cacheKey, new ArrayList<>(list), RedisConstants.CACHE_EXPIRE_30D);
|
||
return list;
|
||
}
|
||
}
|
||
|
||
|
||
public PageVO<SchDicCourseVO> search(Integer schId, Integer type, String keyword,
|
||
Integer divisionId, Integer majorsId,
|
||
Pageable pageable) {
|
||
|
||
// 1. 查询课程分页(动态条件已在 Repository 中处理)
|
||
Page<SchDicCourse> coursePage = schDicCourseRepository.findCoursesByConditions(
|
||
schId, type, keyword, divisionId, majorsId, pageable
|
||
);
|
||
|
||
if (coursePage.isEmpty()) {
|
||
return PageVO.empty(pageable.getPageSize());
|
||
}
|
||
|
||
// 2. 提取课程 ID 列表
|
||
List<Integer> courseIds = coursePage.getContent().stream()
|
||
.map(SchDicCourse::getId)
|
||
.collect(Collectors.toList());
|
||
|
||
// 3. 批量加载关联关系(按课程ID分组)
|
||
Map<Integer, List<SchDicCourseMajors>> majorsGroup = loadMajorsGroup(courseIds, schId);
|
||
Map<Integer, List<SchDicCourseDivision>> divisionGroup = loadDivisionGroup(courseIds, schId);
|
||
|
||
// 4. 获取当前学校的所有专业和学部(用于填充实体详情)
|
||
Map<Integer, SchDicMajors> majorsMap = schDicMajorsService.getSchDicMajorsCacheList(schId)
|
||
.stream()
|
||
.collect(Collectors.toMap(SchDicMajors::getId, Function.identity()));
|
||
|
||
Map<Integer, SchDicDivision> divisionMap = schDicDivisionService.getSchDicDivisionCacheList(schId)
|
||
.stream()
|
||
.collect(Collectors.toMap(SchDicDivision::getId, Function.identity()));
|
||
|
||
// 5. 转换每个课程为 VO
|
||
List<SchDicCourseVO> 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<Integer> oldMajors = null;
|
||
Set<Integer> 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<Integer> oldMajors, Set<Integer> oldDivisions,
|
||
Map<Integer, String> majorsNameMap,
|
||
Map<Integer, String> divisionNameMap) {
|
||
|
||
try {
|
||
List<String> 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<String> 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<Integer> newMajors = dto.getMajorsIds();
|
||
if (!isNew && oldMajors != null) {
|
||
Set<Integer> addedMajors = new HashSet<>(newMajors);
|
||
addedMajors.removeAll(oldMajors);
|
||
Set<Integer> 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<Integer> newDivisions = dto.getDivisionIds();
|
||
if (!isNew && oldDivisions != null) {
|
||
Set<Integer> addedDivs = new HashSet<>(newDivisions);
|
||
addedDivs.removeAll(oldDivisions);
|
||
Set<Integer> 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<Integer> 转换为名称字符串(用"、"分隔)
|
||
*/
|
||
private String getNames(Set<Integer> ids, Map<Integer, String> 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<SchDicCourseMajors> 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<SchDicCourseDivision> 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<Integer, List<SchDicCourseMajors>> loadMajorsGroup(List<Integer> courseIds, Integer schId) {
|
||
if (courseIds.isEmpty()) {
|
||
return Collections.emptyMap();
|
||
}
|
||
return schDicCourseMajorsRepository.findByCourseIdInAndSchId(courseIds, schId)
|
||
.stream()
|
||
.collect(Collectors.groupingBy(SchDicCourseMajors::getCourseId));
|
||
}
|
||
|
||
/**
|
||
* 加载指定课程关联的专业列表(单个)
|
||
*/
|
||
private List<SchDicCourseMajors> loadMajorsList(Integer courseId, Integer schId) {
|
||
if (courseId == null) {
|
||
return Collections.emptyList();
|
||
}
|
||
return schDicCourseMajorsRepository.findByCourseIdAndSchId(courseId, schId);
|
||
}
|
||
|
||
/**
|
||
* 加载指定课程关联的专业ID集合(用于日志)
|
||
*/
|
||
private Set<Integer> loadMajorsIdSet(Integer courseId, Integer schId) {
|
||
return loadMajorsList(courseId, schId).stream()
|
||
.map(SchDicCourseMajors::getMajorsId)
|
||
.collect(Collectors.toSet());
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* 批量加载课程-学部关联关系,并按 courseId 分组
|
||
*/
|
||
private Map<Integer, List<SchDicCourseDivision>> loadDivisionGroup(List<Integer> courseIds, Integer schId) {
|
||
if (courseIds.isEmpty()) {
|
||
return Collections.emptyMap();
|
||
}
|
||
return schDicCourseDivisionRepository.findByCourseIdInAndSchId(courseIds, schId)
|
||
.stream()
|
||
.collect(Collectors.groupingBy(SchDicCourseDivision::getCourseId));
|
||
}
|
||
|
||
private List<SchDicCourseDivision> loadDivisionsList(Integer courseId, Integer schId) {
|
||
if (courseId == null) {
|
||
return Collections.emptyList();
|
||
}
|
||
return schDicCourseDivisionRepository.findByCourseIdAndSchId(courseId, schId);
|
||
}
|
||
|
||
private Set<Integer> loadDivisionsIdSet(Integer courseId, Integer schId) {
|
||
return loadDivisionsList(courseId, schId).stream()
|
||
.map(SchDicCourseDivision::getDivisionId)
|
||
.collect(Collectors.toSet());
|
||
}
|
||
|
||
/**
|
||
* 将单个课程实体转换为 VO,并填充关联列表
|
||
*/
|
||
private SchDicCourseVO convertToVO(SchDicCourse course,
|
||
Map<Integer, List<SchDicCourseMajors>> majorsGroup,
|
||
Map<Integer, List<SchDicCourseDivision>> divisionGroup,
|
||
Map<Integer, SchDicMajors> majorsEntityMap,
|
||
Map<Integer, SchDicDivision> 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<SchDicCourseMajors> majorsRel = majorsGroup.getOrDefault(course.getId(), Collections.emptyList());
|
||
List<SchDicMajors> 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<SchDicCourseDivision> divisionRel = divisionGroup.getOrDefault(course.getId(), Collections.emptyList());
|
||
List<SchDicDivision> 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;
|
||
}
|
||
|
||
}
|