You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
IESExtension/src/main/java/cn/teammodel/service/impl/EvaluationServiceImpl.java

426 lines
18 KiB

package cn.teammodel.service.impl;
import cn.hutool.core.lang.UUID;
import cn.teammodel.common.ErrorCode;
import cn.teammodel.common.PK;
import cn.teammodel.config.exception.ServiceException;
import cn.teammodel.dao.*;
import cn.teammodel.model.dto.Appraise.*;
import cn.teammodel.model.entity.User;
import cn.teammodel.model.entity.appraise.Appraise;
import cn.teammodel.model.entity.appraise.AppraiseRecord;
import cn.teammodel.model.entity.appraise.AppraiseRecordItem;
import cn.teammodel.model.entity.appraise.AppraiseTreeNode;
import cn.teammodel.model.entity.school.ClassInfo;
import cn.teammodel.model.entity.school.School;
import cn.teammodel.model.entity.school.Student;
import cn.teammodel.security.utils.SecurityUtil;
import cn.teammodel.service.EvaluationService;
import cn.teammodel.utils.RepositoryUtil;
import cn.teammodel.utils.SchoolDateUtil;
import com.azure.cosmos.models.CosmosPatchOperations;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* @author winter
* @create 2023-11-20 17:47
*/
@Service
public class EvaluationServiceImpl implements EvaluationService {
/**
* 评价记录对象的类型
*/
private final static String TARGET_STUDENT = "student";
private final static String TARGET_CLASS = "class";
@Resource
private ClassRepository classRepository;
@Resource
private StudentRepository studentRepository;
@Resource
private SchoolRepository schoolRepository;
@Resource
private AppraiseRecordRepository appraiseRecordRepository;
@Resource
private AppraiseRepository appraiseRepository;
/**
* 通用的获取用户学校和学段对应的 appraise 的方法: 判断参数,判断数据是否为空 <br/>
* 从 token 中获取 schoolId
*/
private Appraise findAppraise(String periodId) {
// 默认学区为 default
periodId = StringUtils.isEmpty(periodId) ? "default" : periodId;
User loginUser = SecurityUtil.getLoginUser();
String schoolId = loginUser.getSchoolId();
// 拿到要新增节点的原始数据
Appraise appraise = appraiseRepository.findAppraiseBySchoolIdAndPeriodIdAndCode(schoolId, periodId, PK.PK_APPRAISE);
if (appraise == null) {
throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "学校评价数据不存在");
}
return appraise;
}
@Override
public Appraise getTree(GetEvaluateTreeDto getEvaluateTreeDto) {
String periodId = getEvaluateTreeDto.getPeriodId();
// 默认学区为 default
periodId = StringUtils.isEmpty(periodId) ? "default" : periodId;
User loginUser = SecurityUtil.getLoginUser();
String schoolId = loginUser.getSchoolId();
Appraise appraise = appraiseRepository.findAppraiseBySchoolIdAndPeriodIdAndCode(schoolId, periodId, PK.PK_APPRAISE);
// todo: 是否要对学段进行鉴权
if (appraise != null) {
return this.buildTree(appraise);
}
// 如果传参 period 所在没有 tree 的话,则复制一份模板 tree 给他 (处理节点 id)
appraise = RepositoryUtil.findOne(appraiseRepository.findTemplateTree(), "获取模板评价树失败");
if (appraise == null) {
throw new ServiceException();
}
refreshAppraiseTree(appraise.getNodes());
appraise.setPeriodId(periodId);
appraise.setSchoolId(schoolId);
appraise.setId(null);
appraise = appraiseRepository.save(appraise);
return this.buildTree(appraise);
}
/**
* 将树的节点的 id 进行替换(天才)
*/
private void refreshAppraiseTree(List<AppraiseTreeNode> nodes) {
List<AppraiseTreeNode> children = nodes.stream().filter(item -> item.getPid() != null).collect(Collectors.toList());
// 将非 root 的 nodes 通过 pid 收集成 list, 再遍历 nodes, 将其每一个 id 对应 map 中的 list 的 pid修改成新的 id 即可
Map<String, List<AppraiseTreeNode>> pidNodeMap = children.stream().collect(Collectors.groupingBy(AppraiseTreeNode::getPid));
nodes.forEach(item -> {
String newId = UUID.randomUUID().toString();
String oldId = item.getId();
List<AppraiseTreeNode> appraiseTreeNodes = pidNodeMap.get(oldId);
if (appraiseTreeNodes != null) {
appraiseTreeNodes.forEach(node -> node.setPid(newId));
}
// 处理每个节点属性
item.setId(newId);
item.setCreator("template");
});
}
@Override
public Appraise buildTree(Appraise appraise) {
if (appraise == null) {
return null;
}
List<AppraiseTreeNode> nodes = appraise.getNodes();
List<AppraiseTreeNode> parents = new ArrayList<>();
// pid 为 null 或者 "" 则为 parents
for (AppraiseTreeNode node : nodes) {
if (StringUtils.isBlank(node.getPid())) {
parents.add(node);
}
}
// 迭代构建孩子节点
for (AppraiseTreeNode parent : parents) {
buildChildren(parent, nodes);
}
appraise.setNodes(parents);
return appraise;
}
@Override
public void flattenTree(List<AppraiseTreeNode> trees, List<AppraiseTreeNode> nodes) {
// 递归结束条件
if (ObjectUtils.isEmpty(trees)) return;
// 这里的每个 tree 都是组装好的树,区别于单个节点
for (AppraiseTreeNode tree : trees) {
// 递归
flattenTree(tree.getChildren(), nodes);
// 回溯时删除 children,添加进列表
tree.setChildren(null);
nodes.add(tree);
}
}
@Override
public Appraise insertNode(InsertNodeDto insertNodeDto) {
Appraise appraise = findAppraise(insertNodeDto.getPeriodId());
User loginUser = SecurityUtil.getLoginUser();
List<AppraiseTreeNode> originNodes = appraise.getNodes();
// 拷贝数据到新节点
AppraiseTreeNode newNode = new AppraiseTreeNode();
BeanUtils.copyProperties(insertNodeDto, newNode);
String newNodePid = newNode.getPid();
// 根节点直接添加
if (StringUtils.isNotEmpty(newNodePid)) {
boolean invalid = originNodes.stream().noneMatch(item -> newNodePid.equals(item.getId()));
if (invalid) {
throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "父节点不存在");
}
}
// todo: 可不可以添加默认值 order ?
newNode.setId(UUID.randomUUID().toString());
newNode.setCreatorId(loginUser.getId());
newNode.setCreator(loginUser.getName());
newNode.setCreateTime(LocalDateTime.now());
originNodes.add(newNode);
return buildTree(appraiseRepository.save(appraise));
}
@Override
public Appraise updateNode(UpdateNodeDto updateNodeDto) {
String updateNodeId = updateNodeDto.getId();
Appraise appraise = findAppraise(updateNodeDto.getPeriodId());
List<AppraiseTreeNode> originNodes = appraise.getNodes();
// 每个节点都有 id, 直接校验是否合法
AppraiseTreeNode updateNode = originNodes.stream()
.filter(item -> updateNodeId.equals(item.getId()))
.findFirst()
.orElseThrow(() -> new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "更新节点不存在"));
// 更新字段,考虑直接 copy properties
updateNode.setName(updateNodeDto.getName());
updateNode.setLogo(updateNodeDto.getLogo());
updateNode.setOrder(updateNodeDto.getOrder());
updateNode.setPraise(updateNodeDto.isPraise());
// todo: 为新节点赋值必须参数 (id, creator), 可不可以添加默认值 order ?
return buildTree(appraiseRepository.save(appraise));
}
@Override
public Appraise deleteNode(DeleteNodeDto deleteNodeDto) {
// 删除指定节点,可能是(一级,二级,三级),设计一个通用的
Appraise appraise = findAppraise(deleteNodeDto.getPeriodId());
List<AppraiseTreeNode> nodes = appraise.getNodes();
List<AppraiseTreeNode> nodesToDelete = new ArrayList<>();
// 迭代器安全删除
for (AppraiseTreeNode node : nodes) {
if (node.getId().equals(deleteNodeDto.getId())) {
// 删除当前节点
nodesToDelete.add(node);
// 递归删除其孩子节点
this.collectNodesToDelete(node.getId(), nodes, nodesToDelete);
}
}
if (ObjectUtils.isEmpty(nodesToDelete)) {
throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "删除节点不存在");
}
nodes.removeAll(nodesToDelete);
return buildTree(appraiseRepository.save(appraise));
}
@Override
public void vote(AppraiseVoteDto appraiseVoteDto) {
String targetId = appraiseVoteDto.getTargetId();
boolean spread = appraiseVoteDto.isSpread();
String targetType = appraiseVoteDto.getTargetType();
String appraiseId = appraiseVoteDto.getAppraiseId();
User loginUser = SecurityUtil.getLoginUser();
String schoolId = loginUser.getSchoolId();
String classId;
String periodId;
String name;
String avatar = null;
// 分别对班级和学生的关键信息取值
if (targetType.equals(TARGET_STUDENT)) {
Student student = studentRepository.findStudentByIdAndCode(targetId, String.format(PK.STUDENT, schoolId));
if (student == null) {
throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "该学生不存在");
}
classId = student.getClassId();
periodId = student.getPeriodId();
name = student.getName();
avatar = student.getPicture();
} else if (targetType.equals(TARGET_CLASS)){
ClassInfo classInfo = classRepository.findClassByIdAndCode(targetId, String.format(PK.CLASS, schoolId));
if (classInfo == null) {
throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "班级不存在");
}
classId = targetId;
periodId = classInfo.getPeriodId();
name = classInfo.getName();
} else {
throw new ServiceException(ErrorCode.PARAMS_ERROR.getCode(), "不受支持的评价对象");
}
// 获取评价项节点
List<AppraiseTreeNode> nodes = appraiseRepository.findNodeById(PK.PK_APPRAISE, appraiseId);
AppraiseTreeNode appraiseTreeNode = RepositoryUtil.findOne(nodes, "获取评价项失败");
// 通过 periodId 获取 semesters
List<School.Period> periodById = schoolRepository.findPeriodById(schoolId, periodId);
School.Period period = RepositoryUtil.findOne(periodById, "获取学段失败");
List<School.Semester> semesters = period.getSemesters();
// 获取当前学年学期组合 ID
String academicYearId = SchoolDateUtil.generateAcademicId(semesters, LocalDate.now());
// 查询是否存在记录
AppraiseRecord record = appraiseRecordRepository.findAppraiseRecordByTargetIdAndClassIdAndAcademicYearIdAndCode(
targetId,
classId,
academicYearId,
String.format(PK.PK_APPRAISE_RECORD,schoolId)
);
// 初始化新的评价节点
AppraiseRecordItem item = new AppraiseRecordItem();
item.setId(UUID.randomUUID().toString());
item.setAppraiseNode(appraiseTreeNode);
item.setCreator(loginUser.getName());
item.setCreatorId(loginUser.getId());
item.setCreateTime(LocalDateTime.now());
// 不存在或者学生的班级不一样则创建一条新的,存在则处理一下分值后再向其 nodes 中插入一条 item
if (record == null || !classId.equals(record.getClassId())) {
List<AppraiseRecordItem> items = Collections.singletonList(item);
record = new AppraiseRecord();
record.setTargetId(targetId);
record.setTargetType(targetType);
record.setClassId(classId);
record.setName(name);
record.setAvatar(avatar);
record.setAcademicYearId(academicYearId);
record.setPraiseCount(appraiseTreeNode.isPraise() ? 1 : -1);
record.setScore(ObjectUtils.isEmpty(appraiseTreeNode.getScore()) ? 0 : appraiseTreeNode.getScore());
record.setNodes(items);
record.setCode(String.format(PK.PK_APPRAISE_RECORD, schoolId));
appraiseRecordRepository.save(record);
} else {
// 处理学校与学生的差异
if (targetType.equals(TARGET_CLASS)){
item.setSpread(spread);
}
CosmosPatchOperations operations = CosmosPatchOperations.create();
operations.add("/nodes/0", item);
// 表扬
long praise = appraiseTreeNode.isPraise() ? 1 : -1;
operations.increment("/praiseCount", praise);
// 加分
int scoreToPlus = ObjectUtils.isEmpty(appraiseTreeNode.getScore()) ? 0 : appraiseTreeNode.getScore();
operations.increment("/score", scoreToPlus);
// patch doc
appraiseRecordRepository.save(record.getId(), PK.buildOf(PK.PK_APPRAISE_RECORD, schoolId), AppraiseRecord.class, operations);
}
}
@Override
public List<AppraiseRecordItem> findVoteRecord(FindVoteRecordDto findVoteRecordDto) {
String periodId = findVoteRecordDto.getPeriodId();
String targetId = StringUtils.isBlank(findVoteRecordDto.getTargetId()) ? null : findVoteRecordDto.getTargetId();
String targetType = StringUtils.isBlank(findVoteRecordDto.getTargetType()) ? null : findVoteRecordDto.getTargetType();
String classId = StringUtils.isBlank(findVoteRecordDto.getClassId()) ? null : findVoteRecordDto.getClassId();
User loginUser = SecurityUtil.getLoginUser();
String teacherId = loginUser.getId();
String schoolId = loginUser.getSchoolId();
List<School.Semester> semesters = schoolRepository.findSemestersById(schoolId, periodId);
String academicYearId = SchoolDateUtil.generateAcademicId(semesters, LocalDate.now());
List<AppraiseRecordItem> appraiseRecordItems = appraiseRecordRepository.searchNodesByCondition(
targetId,
targetType,
classId,
teacherId,
academicYearId,
String.format(PK.PK_APPRAISE_RECORD, schoolId)
);
return appraiseRecordItems;
}
@Override
public void recallVote(RecallVoteDto recallVoteDto) {
String recordId = recallVoteDto.getRecordId();
String nodeId = recallVoteDto.getNodeId();
User loginUser = SecurityUtil.getLoginUser();
String schoolId = loginUser.getSchoolId();
String userId = loginUser.getId();
Optional<AppraiseRecord> optional = appraiseRecordRepository.findById(recordId, PK.buildOf(PK.PK_APPRAISE_RECORD, schoolId));
AppraiseRecord appraiseRecord = optional.orElseThrow(() -> new ServiceException("该记录不存在"));
AppraiseRecordItem record = appraiseRecord.getNodes()
.stream()
.filter(item -> nodeId.equals(item.getId()))
.findFirst().orElseThrow(() -> new ServiceException("该记录节点不存在"));
// 鉴权(不是创建老师不能撤回)
if (!userId.equals(record.getCreatorId())) {
throw new ServiceException(ErrorCode.NO_AUTH_ERROR.getCode(), "您不是创建老师,不能撤回");
}
// 删除评价项并且恢复评分
appraiseRecord.getNodes().removeIf(item -> nodeId.equals(item.getId()));
boolean praise = record.getAppraiseNode().isPraise();
Integer newPraiseCount = appraiseRecord.getPraiseCount() + (praise ? -1 : 1);
appraiseRecord.setPraiseCount(newPraiseCount);
int score = record.getAppraiseNode().getScore() == null ? 0 : record.getAppraiseNode().getScore();
Integer newScore = appraiseRecord.getScore() - score;
appraiseRecord.setScore(newScore);
// 保存
appraiseRecordRepository.save(appraiseRecord);
}
/**
* 递归收集 id 的节点及 id 节点的孩子节点 (迭代器删除居然也报错)
*/
private void collectNodesToDelete(String id, List<AppraiseTreeNode> nodes, List<AppraiseTreeNode> nodesToDelete) {
for (AppraiseTreeNode node : nodes) {
if (id.equals(node.getPid())) {
// 收集
nodesToDelete.add(node);
// 递归收集
collectNodesToDelete(node.getId(), nodes, nodesToDelete);
}
}
}
/**
* 递归的构建父亲节点的孩子,以及孩子的孩子 (理论支持无极树,但应该考虑是否增加递归深度)
*/
private void buildChildren(AppraiseTreeNode parent, List<AppraiseTreeNode> nodes) {
List<AppraiseTreeNode> children = getChildren(parent, nodes);
if (children.size() == 0) return;
parent.setChildren(children);
for (AppraiseTreeNode child : children) {
// 如果子节点还有孩子的话,就继续递归构建
if (getChildren(child, nodes).size() > 0) {
buildChildren(child, nodes);
}
}
}
/**
* 获取节点的孩子节点列表
*/
private List<AppraiseTreeNode> getChildren(AppraiseTreeNode parent, List<AppraiseTreeNode> nodes) {
List<AppraiseTreeNode> children = new ArrayList<>();
for (AppraiseTreeNode node : nodes) {
if (StringUtils.isNotBlank(node.getPid()) && node.getPid().equals(parent.getId())) {
children.add(node);
}
}
return children;
}
}