获取任教班级
build / build-push (push) Failing after 15s

This commit is contained in:
wanghu
2026-09-20 20:55:59 +08:00
parent c31ff87479
commit 2d549eb065
23 changed files with 189 additions and 460 deletions
@@ -25,7 +25,7 @@ import java.util.Map;
/**
* 课堂作业 V2 学生端接口(PRD §18 路径一字对齐:/student/homeworks…)。
* <p>
* 身份:当前登录用户ID ↔ class_student 名册;仅可见本人班级作业,禁 IDOR。
* 身份:当前登录用户ID ↔ bschclass 学生名册(实时匹配);仅可见本人班级作业,禁 IDOR。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/student/homeworks")
@@ -17,7 +17,6 @@ import com.yunhai.homework.model.v2.SubmissionVersionVo;
import com.yunhai.homework.model.v2.SubjectDto;
import com.yunhai.homework.model.v2.TeacherHomeworkQuery;
import com.yunhai.homework.model.v2.TeachingClassVo;
import com.yunhai.homework.service.impl.ClassStudentService;
import com.yunhai.homework.service.impl.TeacherHomeworkV2Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
@@ -51,9 +50,6 @@ public class TeacherHomeworkV2Controller {
@Autowired
private TeacherHomeworkV2Service teacherHomeworkV2Service;
@Autowired
private ClassStudentService classStudentService;
// ============================ 页 1:发布作业 ============================
/** 创建作业(落草稿) */
@@ -198,22 +194,15 @@ public class TeacherHomeworkV2Controller {
/** 按学科筛选我的任教班级(subId=学科编码;班级名称取学校班级列表) */
@GetMapping("/subjects/classes")
public List<TeachingClassVo> teachingClasses(@RequestParam("subId") String subId) {
return teacherHomeworkV2Service.teachingClasses(subId);
public List<TeachingClassVo> teachingClasses(@RequestParam("subject") String subject) {
return teacherHomeworkV2Service.teachingClasses(subject);
}
// ============================ 辅助:班级树 / 名册同步 ============================
/** 年级→班级树(发布选班;名册快照聚合) */
/** 年级→班级树(发布选班;subId 传学科编码时仅返回该学科任教班级) */
@GetMapping("/classes")
public List<ClassTreeNode> classTree() {
return teacherHomeworkV2Service.classTree();
}
/** 手动触发名册同步(bschclass 接口就绪前为空操作) */
@PostMapping("/classes/sync")
public Map<String, Integer> syncClasses() {
int synced = classStudentService.syncAll();
return java.util.Collections.singletonMap("synced", synced);
public List<ClassTreeNode> classTree(@RequestParam(value = "subject", required = false) String subject) {
return teacherHomeworkV2Service.classTree(subject);
}
}
@@ -1,59 +0,0 @@
package com.yunhai.homework.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import java.time.LocalDateTime;
/**
* 班级学生名册快照(PRD class_student)。
* <p>
* 由同步任务从 bschclass 拉取(接口未就绪前用种子数据),每日全量刷 + 手动触发;
* 同步不物理删,转出置 status=0。学生身份口径:用户中心数字ID(UserContext.getUserId())
* ↔ student_id(SchStudent.UserStuSerID),名册仅存学生ID + 姓名快照。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "class_student",
uniqueConstraints = @UniqueConstraint(name = "uk_class_student", columnNames = {"class_id", "student_id"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class ClassStudent extends BaseEntity {
/** 学校ID */
@Column(name = "school_id", nullable = false)
private Integer schoolId;
/** 班级ID */
@Column(name = "class_id", nullable = false, length = 64)
private String classId;
/** 班级名快照 */
@Column(name = "class_name", length = 50)
private String className;
/** 年级名快照 */
@Column(name = "grade_name", length = 50)
private String gradeName;
/** 学生ID(用户中心数字ID,即 SchStudent.UserStuSerID) */
@Column(name = "student_id", nullable = false)
private Integer studentId;
/** 学生姓名 */
@Column(name = "student_name", nullable = false, length = 50)
private String studentName;
/** 在班状态:1 在班 / 0 已转出退课 */
@Column(name = "status", nullable = false)
private Integer status = 1;
/** 离班时间 */
@Column(name = "leave_at")
private LocalDateTime leaveAt;
}
@@ -26,8 +26,8 @@ public class HomeworkClass extends BaseEntity {
private Integer homeworkId;
/** 班级ID */
@Column(name = "class_id", nullable = false, length = 64)
private String classId;
@Column(name = "class_id", nullable = false)
private Integer classId;
/** 班级名快照 */
@Column(name = "class_name", length = 50)
@@ -37,8 +37,8 @@ public class HomeworkReadRecord extends BaseEntity {
private String studentName;
/** 班级ID */
@Column(name = "class_id", length = 64)
private String classId;
@Column(name = "class_id")
private Integer classId;
/** 阅读状态:0 UNREAD / 1 READ */
@Column(name = "is_read", nullable = false)
@@ -41,8 +41,8 @@ public class HomeworkSubmission extends BaseEntity {
private String studentName;
/** 班级ID */
@Column(name = "class_id", nullable = false, length = 64)
private String classId;
@Column(name = "class_id", nullable = false)
private Integer classId;
/** 班级名快照 */
@Column(name = "class_name", nullable = false, length = 50)
@@ -1,46 +0,0 @@
package com.yunhai.homework.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
/**
* 班级任教关系快照(bschclass sch_class_teach 本地镜像,PRD 口径同 class_student)。
* <p>
* 由同步任务从 bschclass 拉取(接口未就绪前用种子数据);bschclass 任教接口
* 不可用时,任教学科链路降级读本表。教师口径:用户中心数字ID(UserContext.getUserId());
* class_id 与 class_student.class_id 同一取值域。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "sch_class_teach",
uniqueConstraints = @UniqueConstraint(name = "uk_sch_class_teach", columnNames = {"school_id", "class_id", "tea_id", "sub_id"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class SchClassTeach extends BaseEntity {
/** 学校ID */
@Column(name = "school_id", nullable = false)
private Integer schoolId;
/** 班级ID(与 class_student.class_id 同一取值域) */
@Column(name = "class_id", nullable = false, length = 64)
private String classId;
/** 教师ID(用户中心数字ID) */
@Column(name = "tea_id", nullable = false)
private Integer teaId;
/** 学科编码(bschclass SubID;本地无字典时编码即展示名) */
@Column(name = "sub_id", nullable = false, length = 64)
private String subId;
/** 有效状态:1 有效 / 0 失效 */
@Column(name = "status", nullable = false)
private Integer status = 1;
}
@@ -3,9 +3,7 @@ package com.yunhai.homework.job;
import com.yunhai.homework.entity.Homework;
import com.yunhai.homework.enums.HwStatusEnum;
import com.yunhai.homework.enums.NotificationTypeEnum;
import com.yunhai.homework.repository.HomeworkClassRepository;
import com.yunhai.homework.repository.HomeworkRepository;
import com.yunhai.homework.service.impl.ClassStudentService;
import com.yunhai.homework.service.impl.HomeworkStateService;
import com.yunhai.homework.service.impl.NotificationService;
import com.yunhai.homework.service.impl.TeacherHomeworkV2Service;
@@ -33,9 +31,6 @@ public class HomeworkScheduledJobs {
@Autowired
private HomeworkRepository homeworkRepository;
@Autowired
private HomeworkClassRepository homeworkClassRepository;
@Autowired
private HomeworkStateService stateService;
@@ -45,9 +40,6 @@ public class HomeworkScheduledJobs {
@Autowired
private TeacherHomeworkV2Service teacherHomeworkV2Service;
@Autowired
private ClassStudentService classStudentService;
/**
* 定时发布:每分钟扫描到点的 DRAFT_SCHEDULED 作业 → PUBLISHED + 作业发布通知;
* 失败回退 DRAFT_UNPUBLISHED 并站内通知教师。
@@ -146,16 +138,4 @@ public class HomeworkScheduledJobs {
NotificationService.deadlineReminderContent(homework) + "(剩余不足 " + slotDesc + ")");
log.info("[deadlineReminder] homeworkId={}, slot={}, sent={}", homework.getId(), slot, sent);
}
/**
* 名册同步:每日 02:00 全量刷新 class_student(bschclass 接口就绪前仅日志)。
*/
@Scheduled(cron = "${homework.sync.class-data-cron:0 0 2 * * ?}")
public void classDataSync() {
try {
classStudentService.syncAll();
} catch (Exception e) {
log.error("[classDataSync] 名册同步失败", e);
}
}
}
@@ -12,7 +12,7 @@ import lombok.NoArgsConstructor;
@AllArgsConstructor
public class ClassItem {
private String classId;
private Integer classId;
private String className;
}
@@ -7,12 +7,12 @@ import java.util.List;
/**
* 年级→班级树(发布作业选班 + 应交名单基数来源)。
* 叶子节点 id = 班级 id(class_student 快照聚合)。
* 叶子节点 id = 班级 id(bschclass 班级/年级实时组装)。
*/
@Data
public class ClassTreeNode {
/** 年级节点为 gradeName,班级叶子为 classId */
/** 年级节点为年级ID(无年级归属时为「未分组」文案),班级叶子为 classId */
private String id;
/** 展示名 */
@@ -27,7 +27,7 @@ public class HomeworkStatsVo {
@Data
public static class ByClass {
private String classId;
private Integer classId;
private String className;
@@ -26,7 +26,7 @@ public class HomeworkV2Request {
private String description;
/** 参与班级 id 列表 */
private List<String> classIds;
private List<Integer> classIds;
/** 附件列表 */
private List<AttachmentItem> attachments;
@@ -12,7 +12,7 @@ import lombok.EqualsAndHashCode;
public class StudentMatrixQuery extends BasePageQuery {
/** 班级筛选(空 = 全部参与班级) */
private String classId;
private Integer classId;
/** 提交状态筛选:NOT_SUBMITTED/SUBMITTED/LATE_SUBMITTED/RETURNED/REVIEWED */
private String submitStatus;
@@ -21,7 +21,7 @@ public class StudentMatrixVo {
/** 姓名 */
private String studentName;
private String classId;
private Integer classId;
private String className;
@@ -9,7 +9,7 @@ import lombok.Data;
public class TeachingClassVo {
/** 班级业务ID(bschclass sch_class.class_id) */
private String classId;
private Integer classId;
/** 班级名称(学校班级列表映射;缺失时回退为班级ID) */
private String className;
@@ -1,31 +0,0 @@
package com.yunhai.homework.repository;
import com.yunhai.homework.entity.ClassStudent;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
/**
* 班级学生名册仓储。
*/
public interface ClassStudentRepository extends JpaRepository<ClassStudent, Integer> {
/** 按学生ID(用户中心数字ID)反查学生身份(学生端接口入口) */
List<ClassStudent> findByStudentIdAndStatus(Integer studentId, Integer status);
/** 班级在班学生 */
List<ClassStudent> findByClassIdAndStatus(String classId, Integer status);
/** 指定班级集合的全部在班学生(应交名单) */
List<ClassStudent> findByClassIdInAndStatus(List<String> classIds, Integer status);
/** 指定学校的全部名册(同步任务全量刷新用) */
List<ClassStudent> findBySchoolId(Integer schoolId);
/** 年级→班级树:按学校去重班级(class_student 快照聚合),列序:classId, className, gradeName */
@Query("select distinct c.classId, c.className, c.gradeName from ClassStudent c " +
"where c.schoolId = :schoolId and c.status = 1")
List<Object[]> findDistinctClasses(@Param("schoolId") Integer schoolId);
}
@@ -18,7 +18,7 @@ public interface HomeworkClassRepository extends JpaRepository<HomeworkClass, In
/** 指定班级的全部作业ID(学生端列表倒查用) */
@Query("select c.homeworkId from HomeworkClass c where c.classId in :classIds")
List<Integer> findHomeworkIdsByClassIds(@Param("classIds") List<String> classIds);
List<Integer> findHomeworkIdsByClassIds(@Param("classIds") List<Integer> classIds);
/** 批量查 N 个作业的班级关联(列表展示避免 N+1) */
@Query("select c from HomeworkClass c where c.homeworkId in :homeworkIds")
@@ -1,22 +0,0 @@
package com.yunhai.homework.repository;
import com.yunhai.homework.entity.SchClassTeach;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
/**
* 班级任教关系快照仓储。
*/
public interface SchClassTeachRepository extends JpaRepository<SchClassTeach, Integer> {
/** 指定学校指定教师的全部有效任教记录 */
List<SchClassTeach> findBySchoolIdAndTeaIdAndStatus(Integer schoolId, Integer teaId, Integer status);
/** 指定学校指定教师去重学科编码(字典序由调用方保证) */
@Query("select distinct t.subId from SchClassTeach t " +
"where t.schoolId = :schoolId and t.teaId = :teaId and t.status = 1")
List<String> findDistinctSubIds(@Param("schoolId") Integer schoolId, @Param("teaId") Integer teaId);
}
@@ -1,81 +0,0 @@
package com.yunhai.homework.service.impl;
import com.yunhai.homework.entity.ClassStudent;
import com.yunhai.homework.repository.ClassStudentRepository;
import com.yunhai.homework.utils.UserContext;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 班级学生名册服务:学生端身份映射 + 名册同步任务入口。
* <p>
* 身份口径:当前登录人用户中心数字ID(UserContext.getUserId(),即 SchStudent.UserStuSerID)
* ↔ class_student.student_id(status=1 在班)。名册不再存登录账号。
* bschclass 名册开放接口未就绪期间(计划 §13-1),联调用种子数据
* (sql/v3_seed.sql)填充本表;syncAll() 预留同步实现位。
*/
@Service
public class ClassStudentService {
private static final Logger log = LoggerFactory.getLogger(ClassStudentService.class);
@Autowired
private ClassStudentRepository classStudentRepository;
/**
* 当前登录用户ID对应的学生名册行(在班)。
*
* @throws BizException 未登录或用户ID未匹配名册
*/
public List<ClassStudent> resolveMe() {
Integer userId = UserContext.getUserId();
if (userId == null) {
throw new BizException(ErrorCode.FORBIDDEN, "未获取到登录身份");
}
List<ClassStudent> me = classStudentRepository.findByStudentIdAndStatus(userId, 1);
if (me.isEmpty()) {
throw new BizException(ErrorCode.FORBIDDEN, "当前用户未匹配到学生名册,请联系管理员确认班级名册");
}
return me;
}
/**
* 学生 id(多班级重复时取第一个;学籍号一人一号,正常唯一)。
*/
public Integer resolveMyStudentId() {
return resolveMe().get(0).getStudentId();
}
/**
* 我所在的班级 id 集合。
*/
public List<String> resolveMyClassIds() {
return resolveMe().stream().map(ClassStudent::getClassId).distinct().collect(Collectors.toList());
}
/**
* 名册全量同步(每日 Job / 手动触发)。
* TODO bschclass「按学校查年级班级树+学生名册」开放接口就绪后接入
* (参照 ClassService.getSchTeaList 的直连模式);未就绪时仅记录日志跳过。
*/
@Transactional
public int syncAll() {
log.warn("[syncAll] bschclass 名册开放接口未就绪,本轮跳过(联调请执行 sql/v3_seed.sql 种子数据)");
return 0;
}
/**
* 班级在班学生(运维排查/种子校验用)。
*/
public List<ClassStudent> findByClassId(String classId) {
return classStudentRepository.findByClassIdAndStatus(classId, 1);
}
}
@@ -1,19 +1,15 @@
package com.yunhai.homework.service.impl;
import com.yunhai.homework.entity.ClassStudent;
import com.yunhai.homework.entity.HomeworkReadRecord;
import com.yunhai.homework.entity.HomeworkSubmission;
import com.yunhai.homework.enums.SubmitStatusEnum;
import com.yunhai.homework.model.v2.ProgressVo;
import com.yunhai.homework.repository.ClassStudentRepository;
import com.yunhai.homework.repository.HomeworkReadRecordRepository;
import com.yunhai.homework.repository.HomeworkSubmissionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -27,40 +23,31 @@ import java.util.stream.Collectors;
* 单作业量级 ≤ 数百人,全部实时聚合不落冗余字段:
* should = 参与班级在班学生去重数;submitted = 当前版本 status ∈ 已交集合;
* graded = REVIEWED;unreadUndone = should −(已读 ∪ 已交)。
* 名册实时取 bschclass({@link RosterService},GetClaArrStulist),无本地快照。
*/
@Service
public class HomeworkProgressCalculator {
/**
* 名册条目(去重前):矩阵与提醒名单需要 班级×学生 维度。
*/
public static class RosterEntry {
public Integer studentId;
public String studentName;
public String classId;
public String className;
}
/**
* 聚合结果:总进度 + 分班进度 + 全量名册(班级×学生)。
*/
public static class Result {
public ProgressVo total;
public List<ClassProgress> byClass = new ArrayList<>();
public List<RosterEntry> roster = new ArrayList<>();
public List<RosterService.RosterEntry> roster = new ArrayList<>();
}
/**
* 分班进度条目。
*/
public static class ClassProgress {
public String classId;
public Integer classId;
public String className;
public ProgressVo progress;
}
@Autowired
private ClassStudentRepository classStudentRepository;
private RosterService rosterService;
@Autowired
private HomeworkReadRecordRepository readRecordRepository;
@@ -73,25 +60,18 @@ public class HomeworkProgressCalculator {
*
* @param classIds 参与班级 id(homework_class 解析结果)
*/
public Result compute(Integer homeworkId, List<String> classIds) {
public Result compute(Integer homeworkId, List<Integer> classIds) {
Result result = new Result();
// 1. 名册:班级×学生(应交基数)
List<ClassStudent> roster = classIds == null || classIds.isEmpty()
? new ArrayList<>()
: classStudentRepository.findByClassIdInAndStatus(classIds, 1);
// 1. 名册:班级×学生(应交基数,实时 bschclass)
List<RosterService.RosterEntry> roster = rosterService.roster(classIds);
Set<Integer> distinctStudents = new HashSet<>();
for (ClassStudent cs : roster) {
RosterEntry entry = new RosterEntry();
entry.studentId = cs.getStudentId();
entry.studentName = cs.getStudentName();
entry.classId = cs.getClassId();
entry.className = cs.getClassName();
result.roster.add(entry);
distinctStudents.add(cs.getStudentId());
for (RosterService.RosterEntry cs : roster) {
result.roster.add(cs);
distinctStudents.add(cs.studentId);
}
result.roster.sort(Comparator
.comparing((RosterEntry e) -> nullSafe(e.className))
result.roster.sort(java.util.Comparator
.comparing((RosterService.RosterEntry e) -> nullSafe(e.className))
.thenComparing(e -> nullSafe(e.studentName)));
// 2. 已读集合
@@ -109,9 +89,9 @@ public class HomeworkProgressCalculator {
result.total = aggregate(distinctStudents, readSet, latest);
// 5. 分班进度(班级维度不去重,矩阵口径)
Map<String, List<RosterEntry>> byClassEntries = result.roster.stream()
.collect(Collectors.groupingBy(e -> nullSafe(e.classId), LinkedHashMap::new, Collectors.toList()));
for (Map.Entry<String, List<RosterEntry>> e : byClassEntries.entrySet()) {
Map<Integer, List<RosterService.RosterEntry>> byClassEntries = result.roster.stream()
.collect(Collectors.groupingBy(e -> e.classId, LinkedHashMap::new, Collectors.toList()));
for (Map.Entry<Integer, List<RosterService.RosterEntry>> e : byClassEntries.entrySet()) {
Set<Integer> classStudentIds = e.getValue().stream().map(r -> r.studentId).collect(Collectors.toSet());
ClassProgress cp = new ClassProgress();
cp.classId = e.getKey();
@@ -354,14 +354,14 @@ public class HomeworkService implements IHomeworkService {
if (collectableIds.isEmpty()) {
return;
}
Map<Integer, List<String>> classMap = homeworkClassRepository.findByHomeworkIds(collectableIds).stream()
Map<Integer, List<Integer>> classMap = homeworkClassRepository.findByHomeworkIds(collectableIds).stream()
.collect(Collectors.groupingBy(c -> c.getHomeworkId(),
Collectors.mapping(c -> c.getClassId(), Collectors.toList())));
for (HomeworkVo vo : content) {
if (!collectableIds.contains(vo.getId())) {
continue;
}
List<String> classIds = classMap.getOrDefault(vo.getId(), Collections.emptyList());
List<Integer> classIds = classMap.getOrDefault(vo.getId(), Collections.emptyList());
ProgressVo progress = homeworkProgressCalculator.compute(vo.getId(), classIds).total;
vo.setProgress(progress);
}
@@ -776,7 +776,7 @@ public class HomeworkService implements IHomeworkService {
}
List<Integer> hwIds = collected.stream().map(Homework::getId).collect(Collectors.toList());
Map<Integer, List<String>> classMap = homeworkClassRepository.findByHomeworkIds(hwIds).stream()
Map<Integer, List<Integer>> classMap = homeworkClassRepository.findByHomeworkIds(hwIds).stream()
.collect(Collectors.groupingBy(HomeworkClass::getHomeworkId,
Collectors.mapping(HomeworkClass::getClassId, Collectors.toList())));
@@ -787,7 +787,7 @@ public class HomeworkService implements IHomeworkService {
long excellent = 0, good = 0, pass = 0, fail = 0;
for (Homework h : collected) {
List<String> classIds = classMap.getOrDefault(h.getId(), Collections.emptyList());
List<Integer> classIds = classMap.getOrDefault(h.getId(), Collections.emptyList());
ProgressVo p = homeworkProgressCalculator.compute(h.getId(), classIds).total;
HomeworkStatisticsVo.HomeworkStat d = new HomeworkStatisticsVo.HomeworkStat();
@@ -898,8 +898,7 @@ public class HomeworkService implements IHomeworkService {
return names;
}
return classes.stream()
.map(HomeworkClass::getClassId)
.filter(StringUtils::hasText)
.map(c -> String.valueOf(c.getClassId()))
.collect(Collectors.joining("、"));
}
@@ -1,6 +1,5 @@
package com.yunhai.homework.service.impl;
import com.yunhai.homework.entity.ClassStudent;
import com.yunhai.homework.entity.Homework;
import com.yunhai.homework.entity.HomeworkAttachment;
import com.yunhai.homework.entity.HomeworkClass;
@@ -19,7 +18,6 @@ import com.yunhai.homework.model.v2.StudentResultVo;
import com.yunhai.homework.model.v2.StudentSubmitRequest;
import com.yunhai.homework.model.v2.SubmissionActionRequest;
import com.yunhai.homework.model.v2.SubmissionVersionVo;
import com.yunhai.homework.repository.ClassStudentRepository;
import com.yunhai.homework.repository.HomeworkAttachmentRepository;
import com.yunhai.homework.repository.HomeworkClassRepository;
import com.yunhai.homework.repository.HomeworkReadRecordRepository;
@@ -46,8 +44,8 @@ import java.util.stream.Collectors;
/**
* 课堂作业 V2 学生端业务(PRD §18 学生页 1~3)。
* <p>
* 身份:登录平台用户ID ↔ class_student.student_id 名册(ClassStudentService);仅可见本人班级作业。
* 提交多版本:提交/打回重交/撤回均新增行(version+1、is_latest 翻转),历史保留。
* 身份:登录平台用户ID ↔ bschclass 学生名册({@link RosterService} 实时匹配,无本地快照);
* 仅可见本人班级作业。提交多版本:提交/打回重交/撤回均新增行(version+1、is_latest 翻转),历史保留。
*/
@Service
public class StudentHomeworkService {
@@ -71,10 +69,7 @@ public class StudentHomeworkService {
private SubmissionAttachmentRepository submissionAttachmentRepository;
@Autowired
private ClassStudentRepository classStudentRepository;
@Autowired
private ClassStudentService classStudentService;
private RosterService rosterService;
// ============================ 页 1:作业列表 ============================
@@ -83,9 +78,9 @@ public class StudentHomeworkService {
*/
@Transactional(readOnly = true)
public BasePageResult<StudentHomeworkVo> list(String tab, com.yunhai.homework.model.BasePageQuery query) {
List<ClassStudent> me = classStudentService.resolveMe();
List<String> myClassIds = me.stream().map(ClassStudent::getClassId).distinct().collect(Collectors.toList());
Integer myStudentId = me.get(0).getStudentId();
List<RosterService.RosterEntry> me = rosterService.mine();
List<Integer> myClassIds = me.stream().map(e -> e.classId).distinct().collect(Collectors.toList());
Integer myStudentId = me.get(0).studentId;
List<Integer> homeworkIds = homeworkClassRepository.findHomeworkIdsByClassIds(myClassIds);
if (homeworkIds.isEmpty()) {
@@ -182,16 +177,16 @@ public class StudentHomeworkService {
if (status != HwStatusEnum.PUBLISHED && status != HwStatusEnum.CLOSED) {
return;
}
ClassStudent mine = classStudentService.resolveMe().get(0);
RosterService.RosterEntry mine = rosterService.mine().get(0);
LocalDateTime now = LocalDateTime.now();
HomeworkReadRecord record = readRecordRepository.findByHomeworkIdAndStudentId(id, mine.getStudentId())
HomeworkReadRecord record = readRecordRepository.findByHomeworkIdAndStudentId(id, mine.studentId)
.orElse(null);
if (record == null) {
record = new HomeworkReadRecord();
record.setHomeworkId(id);
record.setStudentId(mine.getStudentId());
record.setStudentName(mine.getStudentName());
record.setClassId(mine.getClassId());
record.setStudentId(mine.studentId);
record.setStudentName(mine.studentName);
record.setClassId(mine.classId);
record.setIsRead(1);
record.setFirstReadAt(now);
record.setLastReadAt(now);
@@ -210,9 +205,9 @@ public class StudentHomeworkService {
@Transactional(readOnly = true)
public StudentResultVo result(Integer id) {
Homework homework = requireVisible(id);
ClassStudent mine = classStudentService.resolveMe().get(0);
RosterService.RosterEntry mine = rosterService.mine().get(0);
List<HomeworkSubmission> versions =
submissionRepository.findByHomeworkIdAndStudentIdOrderByVersionDesc(id, mine.getStudentId());
submissionRepository.findByHomeworkIdAndStudentIdOrderByVersionDesc(id, mine.studentId);
StudentResultVo vo = new StudentResultVo();
vo.setHomeworkId(id);
vo.setHomeworkTitle(homework.getTitle());
@@ -281,7 +276,7 @@ public class StudentHomeworkService {
// ============================ 提交内核 ============================
private Integer doSubmit(Homework homework, String text, List<AttachmentItem> attachments) {
ClassStudent mine = classStudentService.resolveMe().get(0);
RosterService.RosterEntry mine = rosterService.mine().get(0);
HwStatusEnum hwStatus = HwStatusEnum.of(homework.getHwStatus());
if (hwStatus == HwStatusEnum.ARCHIVED) {
throw new BizException(ErrorCode.FORBIDDEN, "作业已归档,不可提交");
@@ -320,9 +315,9 @@ public class StudentHomeworkService {
}
HomeworkSubmission submission = new HomeworkSubmission();
fillSnapshot(submission, homework);
submission.setStudentId(mine.getStudentId());
submission.setStudentName(mine.getStudentName());
submission.setVersion(nextVersion(homework.getId(), mine.getStudentId()));
submission.setStudentId(mine.studentId);
submission.setStudentName(mine.studentName);
submission.setVersion(nextVersion(homework.getId(), mine.studentId));
submission.setIsLatest(1);
submission.setStatus(late ? SubmitStatusEnum.LATE_SUBMITTED.getCode() : SubmitStatusEnum.SUBMITTED.getCode());
submission.setIsLate(late ? 1 : 0);
@@ -333,10 +328,10 @@ public class StudentHomeworkService {
int sort = 0;
for (AttachmentItem item : items) {
SubmissionAttachment sa = new SubmissionAttachment();
sa.setSubmissionId(submission.getId());
sa.setHomeworkId(homework.getId());
sa.setStudentId(mine.getStudentId());
SubmissionAttachment sa = new SubmissionAttachment();
sa.setSubmissionId(submission.getId());
sa.setHomeworkId(homework.getId());
sa.setStudentId(mine.studentId);
sa.setFileId(item.getFileId());
sa.setFileUrl(item.getFileUrl());
sa.setFileType(item.getFileType());
@@ -355,15 +350,15 @@ public class StudentHomeworkService {
private void fillSnapshot(HomeworkSubmission submission, Homework homework) {
submission.setHomeworkId(homework.getId());
ClassStudent mine = classStudentService.resolveMe().get(0);
submission.setClassId(mine.getClassId());
submission.setClassName(mine.getClassName() == null ? mine.getClassId() : mine.getClassName());
RosterService.RosterEntry mine = rosterService.mine().get(0);
submission.setClassId(mine.classId);
submission.setClassName(mine.className == null ? String.valueOf(mine.classId) : mine.className);
// studentId/studentName 由调用方覆盖(fillSnapshot 先行填充默认)
if (submission.getStudentId() == null) {
submission.setStudentId(mine.getStudentId());
submission.setStudentId(mine.studentId);
}
if (submission.getStudentName() == null) {
submission.setStudentName(mine.getStudentName());
submission.setStudentName(mine.studentName);
}
}
@@ -378,7 +373,8 @@ public class StudentHomeworkService {
if (!visibleStatus(HwStatusEnum.of(homework.getHwStatus()))) {
throw new BizException(ErrorCode.FORBIDDEN, "作业不在可见范围");
}
List<String> myClassIds = classStudentService.resolveMyClassIds();
List<Integer> myClassIds = rosterService.mine().stream()
.map(e -> e.classId).distinct().collect(Collectors.toList());
boolean hit = homeworkClassRepository.findByHomeworkId(homeworkId).stream()
.map(HomeworkClass::getClassId)
.anyMatch(myClassIds::contains);
@@ -397,7 +393,7 @@ public class StudentHomeworkService {
}
private HomeworkSubmission myLatest(Integer homeworkId) {
Integer studentId = classStudentService.resolveMyStudentId();
Integer studentId = rosterService.mine().get(0).studentId;
return submissionRepository.findByHomeworkIdAndStudentIdAndIsLatest(homeworkId, studentId, 1)
.orElse(null);
}
@@ -2,7 +2,6 @@ package com.yunhai.homework.service.impl;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.annotation.ExcelProperty;
import com.yunhai.homework.entity.ClassStudent;
import com.yunhai.homework.entity.Homework;
import com.yunhai.homework.entity.HomeworkAttachment;
import com.yunhai.homework.entity.HomeworkClass;
@@ -15,6 +14,7 @@ import com.yunhai.homework.enums.ReviewStatusEnum;
import com.yunhai.homework.enums.SubmitStatusEnum;
import com.yunhai.homework.model.BasePageResult;
import com.yunhai.homework.model.bsch.SchClassDto;
import com.yunhai.homework.model.bsch.SchGradeDto;
import com.yunhai.homework.model.bsch.SchInfoDto;
import com.yunhai.homework.model.bsch.SchSubjectDto;
import com.yunhai.homework.model.bsch.SchTeachDto;
@@ -35,14 +35,12 @@ import com.yunhai.homework.model.v2.SubmissionVersionVo;
import com.yunhai.homework.model.v2.SubjectDto;
import com.yunhai.homework.model.v2.TeacherHomeworkQuery;
import com.yunhai.homework.model.v2.TeachingClassVo;
import com.yunhai.homework.repository.ClassStudentRepository;
import com.yunhai.homework.repository.HomeworkAttachmentRepository;
import com.yunhai.homework.repository.HomeworkClassRepository;
import com.yunhai.homework.repository.HomeworkReadRecordRepository;
import com.yunhai.homework.repository.HomeworkRepository;
import com.yunhai.homework.repository.HomeworkSubmissionRepository;
import com.yunhai.homework.repository.NotificationRepository;
import com.yunhai.homework.repository.SchClassTeachRepository;
import com.yunhai.homework.repository.SubmissionAttachmentRepository;
import com.yunhai.homework.service.IClassService;
import com.yunhai.homework.utils.PageUtils;
@@ -110,10 +108,7 @@ public class TeacherHomeworkV2Service {
private SubmissionAttachmentRepository submissionAttachmentRepository;
@Autowired
private ClassStudentRepository classStudentRepository;
@Autowired
private SchClassTeachRepository schClassTeachRepository;
private RosterService rosterService;
@Autowired
private NotificationRepository notificationRepository;
@@ -197,21 +192,22 @@ public class TeacherHomeworkV2Service {
homework.setRemind1d(request.getRemind1d() == null ? 1 : request.getRemind1d());
homework.setRemind1h(request.getRemind1h() == null ? 1 : request.getRemind1h());
if (request.getClassIds() != null && !request.getClassIds().isEmpty()) {
homework.setReceiveList(String.join(",", request.getClassIds()));
homework.setReceiveList(request.getClassIds().stream()
.map(String::valueOf).collect(Collectors.joining(",")));
}
}
private void saveChildren(Homework homework, List<String> classIds, List<AttachmentItem> attachments) {
private void saveChildren(Homework homework, List<Integer> classIds, List<AttachmentItem> attachments) {
saveClasses(homework, classIds);
saveAttachments(homework, attachments);
}
private void saveClasses(Homework homework, List<String> classIds) {
private void saveClasses(Homework homework, List<Integer> classIds) {
if (classIds == null || classIds.isEmpty()) {
throw new BizException(ErrorCode.FORBIDDEN, "请至少选择一个参与班级");
}
List<String> distinct = classIds.stream().filter(StringUtils::hasText).distinct().collect(Collectors.toList());
for (String classId : distinct) {
List<Integer> distinct = classIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
for (Integer classId : distinct) {
HomeworkClass hc = new HomeworkClass();
hc.setHomeworkId(homework.getId());
hc.setClassId(classId);
@@ -241,13 +237,9 @@ public class TeacherHomeworkV2Service {
}
}
/** 班级名快照:优先取名册,无名册时回退班级 id */
private String resolveClassName(String classId) {
return classStudentRepository.findByClassIdAndStatus(classId, 1).stream()
.map(ClassStudent::getClassName)
.filter(StringUtils::hasText)
.findFirst()
.orElse(classId);
/** 班级名快照:实时取 bschclass 班级列表映射,缺失时回退班级 id */
private String resolveClassName(Integer classId) {
return rosterService.className(classId);
}
// ============================ 页 2:草稿箱 ============================
@@ -437,15 +429,13 @@ public class TeacherHomeworkV2Service {
public BasePageResult<StudentMatrixVo> students(Integer id, StudentMatrixQuery query) {
Homework homework = stateService.requireOwned(id, UserContext.getUserId());
List<HomeworkClass> classes = homeworkClassRepository.findByHomeworkId(id);
List<String> classIds = classIds(classes);
if (StringUtils.hasText(query.getClassId())) {
List<Integer> classIds = classIds(classes);
if (query.getClassId() != null) {
classIds = classIds.stream().filter(c -> c.equals(query.getClassId())).collect(Collectors.toList());
}
// 名册(班级×学生)
List<ClassStudent> roster = classIds.isEmpty()
? new ArrayList<>()
: classStudentRepository.findByClassIdInAndStatus(classIds, 1);
// 名册(班级×学生,实时 bschclass)
List<RosterService.RosterEntry> roster = rosterService.roster(classIds);
Map<Integer, HomeworkReadRecord> readMap = readRecordRepository.findByHomeworkId(id).stream()
.collect(Collectors.toMap(HomeworkReadRecord::getStudentId, r -> r, (a, b) -> a));
Map<Integer, HomeworkSubmission> latestMap = submissionRepository.findByHomeworkIdAndIsLatest(id, 1).stream()
@@ -457,17 +447,17 @@ public class TeacherHomeworkV2Service {
.collect(Collectors.groupingBy(SubmissionAttachment::getSubmissionId));
List<StudentMatrixVo> rows = new ArrayList<>();
for (ClassStudent cs : roster) {
for (RosterService.RosterEntry cs : roster) {
StudentMatrixVo row = new StudentMatrixVo();
row.setStudentId(cs.getStudentId());
row.setStudentName(cs.getStudentName());
row.setClassId(cs.getClassId());
row.setClassName(cs.getClassName());
HomeworkReadRecord read = readMap.get(cs.getStudentId());
row.setStudentId(cs.studentId);
row.setStudentName(cs.studentName);
row.setClassId(cs.classId);
row.setClassName(cs.className);
HomeworkReadRecord read = readMap.get(cs.studentId);
row.setIsRead(read != null && read.getIsRead() != null && read.getIsRead() == 1 ? 1 : 0);
row.setFirstReadAt(read == null ? null : read.getFirstReadAt());
row.setLastReadAt(read == null ? null : read.getLastReadAt());
HomeworkSubmission sub = latestMap.get(cs.getStudentId());
HomeworkSubmission sub = latestMap.get(cs.studentId);
if (sub == null) {
row.setSubmissionId(0);
row.setSubmitStatus(SubmitStatusEnum.NOT_SUBMITTED.getCode());
@@ -854,31 +844,66 @@ public class TeacherHomeworkV2Service {
// ============================ 班级树 / 名册 ============================
/**
* 年级→班级树(发布选班;来自名册快照聚合)。
* 年级→班级树(发布选班;全校班级,不按学科过滤)。
*/
@Transactional(readOnly = true)
public List<ClassTreeNode> classTree() {
Integer schId = UserContext.getSchId();
if (schId == null) {
return classTree(null);
}
/**
* 年级→班级树(发布选班)。数据源 bschclass 实时接口:班级 GetVClassList、
* 年级 GetSchLenLevellist、在班人数 GetClaArrStulist(一次批量外呼);
* subId 非空时仅保留当前教师该学科的任教班级(GetSchTeachlist),空年级组不保留。
*/
@Transactional(readOnly = true)
public List<ClassTreeNode> classTree(String subId) {
// subId=null 为无参全校树;显式传入空白串视为参数错误
if (subId != null && !StringUtils.hasText(subId.trim())) {
throw new BizException(ErrorCode.PARAM_ERROR, "学科编码(subId)不得为空");
}
String schSerId = currentSchSerId();
if (schSerId == null) {
return new ArrayList<>();
}
List<Object[]> rows = classStudentRepository.findDistinctClasses(schId);
Map<String, List<Object[]>> byGrade = rows.stream()
.collect(Collectors.groupingBy(r -> r[2] == null || !StringUtils.hasText(r[2].toString())
? "未分组" : r[2].toString(), java.util.LinkedHashMap::new, Collectors.toList()));
Set<Integer> scope = null;
if (StringUtils.hasText(subId)) {
scope = teachingClassIds(schSerId, subId.trim());
if (scope.isEmpty()) {
return new ArrayList<>();
}
}
List<SchClassDto> classes = classService.getSchClassList(schSerId);
Map<Integer, String> gradeNames = gradeNames(schSerId);
Map<Integer, List<SchClassDto>> byGrade = new java.util.LinkedHashMap<>();
List<Integer> scopeIds = new ArrayList<>();
for (SchClassDto c : classes) {
if (c == null || c.getClassId() == null || (scope != null && !scope.contains(c.getClassId()))) {
continue;
}
byGrade.computeIfAbsent(c.getGradeId(), k -> new ArrayList<>()).add(c);
scopeIds.add(c.getClassId());
}
if (scopeIds.isEmpty()) {
return new ArrayList<>();
}
Map<Integer, Integer> counts = rosterService.studentCounts(scopeIds);
List<ClassTreeNode> tree = new ArrayList<>();
for (Map.Entry<String, List<Object[]>> entry : byGrade.entrySet()) {
for (Map.Entry<Integer, List<SchClassDto>> entry : byGrade.entrySet()) {
Integer gradeId = entry.getKey();
String gradeName = gradeId != null && gradeNames.containsKey(gradeId)
? gradeNames.get(gradeId) : "未分组";
ClassTreeNode grade = new ClassTreeNode();
grade.setId(entry.getKey());
grade.setName(entry.getKey());
grade.setId(gradeId == null ? gradeName : String.valueOf(gradeId));
grade.setName(gradeName);
grade.setIsParent(true);
for (Object[] r : entry.getValue()) {
String classId = String.valueOf(r[0]);
for (SchClassDto c : entry.getValue()) {
ClassTreeNode leaf = new ClassTreeNode();
leaf.setId(classId);
leaf.setName(r[1] == null || !StringUtils.hasText(r[1].toString()) ? classId : r[1].toString());
leaf.setId(String.valueOf(c.getClassId()));
leaf.setName(StringUtils.hasText(c.getClassName())
? c.getClassName() : String.valueOf(c.getClassId()));
leaf.setIsParent(false);
leaf.setStudentCount(classStudentRepository.findByClassIdAndStatus(classId, 1).size());
leaf.setStudentCount(counts.getOrDefault(c.getClassId(), 0));
grade.getChildren().add(leaf);
}
tree.add(grade);
@@ -886,6 +911,39 @@ public class TeacherHomeworkV2Service {
return tree;
}
/**
* 当前教师在 bschclass 的 subId 学科任教班级ID集合(实时 GetSchTeachlist,
* sch_class.class_id 取值域);远端不可用时异常向上抛,登录身份缺失时返回空集合。
*/
private Set<Integer> teachingClassIds(String schSerId, String subId) {
Set<Integer> ids = new LinkedHashSet<>();
Integer userId = UserContext.getUserId();
if (schSerId == null || userId == null) {
return ids;
}
for (SchTeachDto teach : classService.getSchTeachList(schSerId, userId)) {
if (subId.equals(teach.getSubId()) && teach.getClassId() != null) {
ids.add(teach.getClassId());
}
}
return ids;
}
/** 学校年级ID → 年级名(bschclass GetSchLenLevellist;远端失败降级空映射,班级归入「未分组」) */
private Map<Integer, String> gradeNames(String schSerId) {
Map<Integer, String> names = new java.util.LinkedHashMap<>();
try {
for (SchGradeDto g : classService.getSchGradeList(schSerId)) {
if (g != null && g.getId() != null && StringUtils.hasText(g.getName())) {
names.put(g.getId(), g.getName());
}
}
} catch (BizException e) {
log.warn("[classTree] 年级列表拉取失败, 降级「未分组」展示: {}", e.getMessage());
}
return names;
}
// ============================ 学科(我布置的 / 我任教的) ============================
/**
@@ -909,49 +967,27 @@ public class TeacherHomeworkV2Service {
}
/**
* 我的任教学科:bschclass 任教关系(sch_class_teach)当前教师去重学科,
* 名称经本校学科字典解析。
* 我的任教学科:bschclass 任教关系当前教师去重学科,名称经本校学科字典解析。
*/
public List<SubjectDto> teachingSubjects() {
return toSubjectDtos(teachingSubjectCodes(), subjectDictionary());
}
/**
* 我的任教去重学科编码:bschclass 任教关系(sch_class_teach)当前教师。
* 登录身份缺学校/教师信息时返回空集合;远端不可用时降级读本地任教快照
* (sch_class_teach,同 class_student 口径),快照也无记录则保持原语义向上抛出。
* 我的任教去重学科编码:bschclass 任教关系(GetSchTeachlist)当前教师,实时获取。
* 登录身份缺学校/教师信息时返回空集合;远端不可用时异常向上抛
* (assignedSubjects 捕获后仅返回布置学科)。
*/
private Set<String> teachingSubjectCodes() {
Set<String> codes = new LinkedHashSet<>();
try {
for (SchTeachDto teach : currentTeachList()) {
if (StringUtils.hasText(teach.getSubId())) {
codes.add(teach.getSubId());
}
for (SchTeachDto teach : currentTeachList()) {
if (StringUtils.hasText(teach.getSubId())) {
codes.add(teach.getSubId());
}
} catch (BizException e) {
codes.addAll(localTeachingSubIds());
if (codes.isEmpty()) {
throw e;
}
log.warn("[teachingSubjectCodes] bschclass 任教拉取失败, 降级本地任教快照: {}", e.getMessage());
}
return codes;
}
/**
* 当前教师在本地任教快照(sch_class_teach)中的去重学科编码;
* 缺学校/教师身份时返回空集合。
*/
private Set<String> localTeachingSubIds() {
Integer schId = UserContext.getSchId();
Integer userId = UserContext.getUserId();
if (schId == null || userId == null) {
return Collections.emptySet();
}
return new LinkedHashSet<>(schClassTeachRepository.findDistinctSubIds(schId, userId));
}
/**
* 按学科筛选我的任教班级:任教关系当前教师该学科的班级去重,
* 班级名称取学校班级列表(bschclass GetVClassList)映射并按其排序输出,
@@ -966,12 +1002,7 @@ public class TeacherHomeworkV2Service {
if (schSerId == null || userId == null) {
return new ArrayList<>();
}
Set<Integer> classIds = new LinkedHashSet<>();
for (SchTeachDto teach : classService.getSchTeachList(schSerId, userId)) {
if (subId.equals(teach.getSubId()) && teach.getClassId() != null) {
classIds.add(teach.getClassId());
}
}
Set<Integer> classIds = teachingClassIds(schSerId, subId);
if (classIds.isEmpty()) {
return new ArrayList<>();
}
@@ -994,7 +1025,7 @@ public class TeacherHomeworkV2Service {
private static TeachingClassVo teachingClassVo(Integer classId, String className) {
TeachingClassVo vo = new TeachingClassVo();
vo.setClassId(String.valueOf(classId));
vo.setClassId(classId);
vo.setClassName(StringUtils.hasText(className) ? className : String.valueOf(classId));
return vo;
}
@@ -1012,7 +1043,7 @@ public class TeacherHomeworkV2Service {
}
/**
* 当前教师在 bschclass 的任教关系明细(sch_class_teach)。
* 当前教师在 bschclass 的任教关系明细(GetSchTeachlist 实时)。
* 登录身份缺学校/教师信息时返回空列表;远端不可用时抛 BizException,由调用方决定是否降级。
*/
private List<SchTeachDto> currentTeachList() {
@@ -1132,20 +1163,13 @@ public class TeacherHomeworkV2Service {
// ============================ 名册辅助 ============================
/** 参与班级在班学生 id 集合 */
/** 参与班级在班学生 id 集合(实时 bschclass 名册) */
private List<Integer> rosterStudentIds(Integer homeworkId) {
return roster(homeworkId).stream().map(ClassStudent::getStudentId).collect(Collectors.toList());
return rosterService.roster(classIds(homeworkClassRepository.findByHomeworkId(homeworkId)))
.stream().map(e -> e.studentId).collect(Collectors.toList());
}
private List<ClassStudent> roster(Integer homeworkId) {
List<String> classIds = classIds(homeworkClassRepository.findByHomeworkId(homeworkId));
if (classIds.isEmpty()) {
return new ArrayList<>();
}
return classStudentRepository.findByClassIdInAndStatus(classIds, 1);
}
private static List<String> classIds(List<HomeworkClass> classes) {
private static List<Integer> classIds(List<HomeworkClass> classes) {
return classes.stream().map(HomeworkClass::getClassId).collect(Collectors.toList());
}