commit
build / build-push (push) Failing after 10s

This commit is contained in:
wanghu
2026-09-20 09:13:32 +08:00
parent e52a250455
commit 05cc9cf89a
146 changed files with 12327 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
target/
.idea/
*.iml
*.log
.DS_Store
+57
View File
@@ -0,0 +1,57 @@
# ClassHomework 生产配置(线上以 nacos 的 classhomework.yaml 为准,本文件是 nacos 不可达时的兜底)
# 真正上线请在 Nacos 新建 dataId=classhomework.yaml、group=OA_APP 的配置覆盖以下项
# (尤其 datasource 的库名/账号、yh.sso.client-host-url)。
server:
port: 8070
servlet:
# 对外经 caddy 暴露为 http://base.yunhaixingtong.com/classhomework
context-path: /classhomework
spring:
mvc:
date-format: yyyy-MM-dd HH:mm:ss
jackson:
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
datasource:
# 与 UserBase 共用同一台 mysql8;库名为 classhomework(需先在 mysql8 上建库)
url: jdbc:mysql://mysql8:3306/classhomework?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 'CVf^x$sW*b6CGHxh'
driver-class-name: com.mysql.cj.jdbc.Driver
min-idle: 10
max-active: 30
jpa:
database: MySQL
show-sql: false
hibernate:
ddl-auto: update
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
properties:
hibernate.order_by.default_null_ordering: last
open-in-view: true
# 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth)
yh:
env-config:
source:
# 前端运行时配置(JSON 字符串),线上由 Nacos/projman 下发。
# 端点 /api/env-config 由平台 yh-env-config-spring-boot-starter 提供(免登录 SSO 旁路)。
front: ""
sso:
server-url-prefix: http://sso.yunhaixingtong.com
client-host-url: http://base.yunhaixingtong.com/classhomework
allow-headers: all
allow-origins: all
url-pattern: /api/*
app-auth:
enabled: true
# 本应用自身身份(客户端签名用)
app-key: 100617
app-secret: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
# 受信应用(服务端验签用):appKey -> appSecret
trusted-apps:
100617: 2a3b4c5d6e7f8091a2b3c4d5e6f70819
# 上传文件落容器卷,compose 里挂到宿主机 ./upload-data
FILE_PATH: /data/upload
+9
View File
@@ -0,0 +1,9 @@
#!/bin/sh
# ClassHomework 启动入口:配置统一由 Nacos 配置中心在 bootstrap 阶段下发(见 bootstrap.yml)
# 不再需要手动 curl 拉取;若 nacos 不可达,bootstrap 中 fail-fast=false 会自动回退到
# 镜像内烤好的 application-prod.yml 兜底默认。
set -e
echo "[run-app] starting ClassHomework (profile=prod, nacos=${NACOS_ADDR:-nacos.yunhaixingtong.com:8848}) ..."
exec java ${JAVA_OPTS:--Xms512m -Xmx1024m} -Dfile.encoding=UTF-8 -jar /app/classhomework.jar \
--spring.profiles.active=prod
+198
View File
@@ -0,0 +1,198 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.yunhai</groupId>
<artifactId>classhomework</artifactId>
<version>0.0.1</version>
<name>ClassHomework</name>
<description>课堂作业子应用</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencyManagement>
<!-- 基础平台版本管理(含 yh-* 架包与 spring-boot-dependencies 对齐) -->
<dependencies>
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-platform-dependencies</artifactId>
<version>1.0.0-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring Cloud Alibaba(Nacos 配置中心,对齐 Boot 2.7) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.0.5.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- 仅引入 Spring Security 的加密模块(BCryptPasswordEncoder),不引入整套安全框架 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Nacos 配置中心(替代 Spring Cloud Config)。Spring Cloud Alibaba 2021.0.5 适配 Spring Boot 2.7 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Boot 2.4+ 默认不再处理 bootstrap.yml,需显式引入以继续从 Nacos 拉取配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.1.8</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JSON 序列化(fastjson,主类开启安全模式) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<!-- Swagger / OpenAPI 文档(springdoc,兼容 Boot 2.7) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.7.0</version>
</dependency>
<!-- 基础平台全家桶(聚合 starter):认证(CAS/会话/SSO)+ 应用间签名鉴权 + 用户中心静态门面
+ 统一响应 + actuator 健康探活,一行带齐(模块构成见 BasePlatform/后端项目知识库.md)。
约定:标准业务服务一律引聚合 starter;平台能力提供方(如 UserBase)按需单引。 -->
<dependency>
<groupId>com.yunhai.platform</groupId>
<artifactId>yh-spring-boot-starter</artifactId>
</dependency>
<!-- CAS 客户端(浏览器 SSO 重定向 / ticket 校验 / getRemoteUser 包装)。
版本 3.5.1 已在本地 .m2 仓库。 -->
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.5.1</version>
</dependency>
<!-- Excel 导入导出(EasyExcel 3.x,注解驱动、流式写入内存友好) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.3.2</version>
</dependency>
<!-- 不引 spring-boot-starter-data-redis / cache:代码零使用,data-redis 会自动装配
RedisHealthIndicator,连不上 redis 会把 /actuator/health 整体拉成 DOWN(503)。 -->
</dependencies>
<build>
<finalName>classhomework</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- 关闭增量编译,避免 Windows 下 maven-status 状态文件被锁导致
"Error while storing the mojo status / 拒绝访问" -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
</plugin>
<!-- 打可执行 fat jar(Docker 镜像直接 java -jar 启动) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.yunhai.homework.HomeworkApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>nexus-public</id>
<name>bsch/nexus public</name>
<url>http://nexus.yunhaixingtong.com/repository/maven-public/</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>true</enabled></snapshots>
</repository>
</repositories>
<!--
ci 构建档案:将构建产物重定向到 IDE(IntelliJ/Eclipse)索引目录之外,
避免 IDE 后台进程持续锁定 target/maven-status/.../createdFiles.lst 导致
"Error while storing the mojo status / 拒绝访问"。
输出目录需从未被 IDE 打开过。常规本地构建无需激活此 profile。
-->
<profiles>
<profile>
<id>ci</id>
<build>
<directory>C:/classhomework-build/classhomework</directory>
</build>
</profile>
</profiles>
</project>
@@ -0,0 +1,18 @@
package com.yunhai.homework;
import com.alibaba.fastjson.parser.ParserConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(scanBasePackages = "com.yunhai.homework")
@EnableTransactionManagement
public class HomeworkApplication {
public static void main(String[] args) {
// 开启 fastjson 安全模式
ParserConfig.getGlobalInstance().setSafeMode(true);
SpringApplication.run(HomeworkApplication.class, args);
}
}
@@ -0,0 +1,42 @@
package com.yunhai.homework.common.listener;
import com.yunhai.homework.entity.BaseEntity;
import com.yunhai.homework.utils.UserContext;
import org.springframework.stereotype.Component;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;
/**
* JPA 审计监听器:在实体持久化/更新前自动填充创建人/更新人(平台当前登录人)与时间戳。
* 对应 {@link BaseEntity} 的 createBy / updateBy / createTime / updateTime 字段。
* 当前登录人取自 {@link UserContext}(用户中心数字ID,字符串形式落审计列)。
*/
@Component
public class HomeworkAuditListener {
@PrePersist
public void prePersist(BaseEntity entity) {
LocalDateTime now = LocalDateTime.now();
entity.setCreateTime(now);
entity.setUpdateTime(now);
Integer userId = UserContext.getUserId();
if (userId != null) {
if (entity.getCreateBy() == null) {
entity.setCreateBy(String.valueOf(userId));
}
entity.setUpdateBy(String.valueOf(userId));
}
}
@PreUpdate
public void preUpdate(BaseEntity entity) {
entity.setUpdateTime(LocalDateTime.now());
Integer userId = UserContext.getUserId();
if (userId != null) {
entity.setUpdateBy(String.valueOf(userId));
}
}
}
@@ -0,0 +1,10 @@
package com.yunhai.homework.common.validation;
/**
* JSR-303 分组校验标记接口(对齐 UserBase 规范)。
* Create:新建场景必填字段;Update:编辑场景必填字段(通常含 id)。
*/
public interface ValidationGroups {
interface Create {}
interface Update {}
}
@@ -0,0 +1,11 @@
package com.yunhai.homework.config;
/**
* 全局路径常量。与 UserBase 一致:API 统一挂在 /{platform}/ 之下(platform = api / manage / mobiapi)。
*/
public enum Config {
Inst;
public static final String API_PATH = "/{platform}"; // manage / api / mobiapi,前面有/后面无/,拼接子路径时用"/"分隔
public static final String OPEN_API_PATH = "/oapi";
}
@@ -0,0 +1,35 @@
package com.yunhai.homework.config;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import java.time.format.DateTimeFormatter;
/**
* Jackson 时间序列化全局约定(对齐旧 .NET 版 "yyyy-MM-dd HH:mm:ss"):
* 接口出入参中的 LocalDateTime/LocalDate 统一使用该格式,避免 ISO 的 "T" 分隔符
* (如 2026-09-01T16:38:40)直接暴露给前端表格。
*/
@Configuration
public class JacksonConfig {
private static final DateTimeFormatter DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> builder
.serializers(
new LocalDateTimeSerializer(DATE_TIME),
new LocalDateSerializer(DATE))
.deserializers(
new LocalDateTimeDeserializer(DATE_TIME),
new LocalDateDeserializer(DATE));
}
}
@@ -0,0 +1,13 @@
package com.yunhai.homework.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 开启 @Scheduled 定时任务(作业定时发布 / 到点截止 / 截止提醒 / 名册同步)。
* 单实例假设;多实例部署前需引入分布式锁(计划 §13-7)。
*/
@Configuration
@EnableScheduling
public class SchedulingConfig {
}
@@ -0,0 +1,22 @@
package com.yunhai.homework.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 接口文档(springdoc-openapi,替代旧 Springfox)。
*/
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI homeworkOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("课程作业接口文档")
.description("课程作业子应用:发作业、列表、删除、编辑、管理员查看、统计(springdoc-openapi)")
.version("1.0"));
}
}
@@ -0,0 +1,52 @@
package com.yunhai.homework.config;
import com.yunhai.homework.utils.UserContext;
import com.yunhai.platform.auth.spring.CurrentBoaUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* {@link UserContext} 生命周期配置:
* 启动时向 UserContext 注入平台 BoaUser 供应器(取值在首次访问时惰性解析),
* 并注册拦截器在每个请求结束后清理 ThreadLocal(Tomcat 线程复用防串号)。
*/
@Configuration
public class UserContextConfig {
@Autowired
private CurrentBoaUser currentBoaUser;
@PostConstruct
public void init() {
UserContext.init(currentBoaUser);
}
@Bean
public HandlerInterceptor userContextCleanInterceptor() {
return new HandlerInterceptor() {
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear();
}
};
}
@Bean
public WebMvcConfigurer userContextConfigurer(HandlerInterceptor userContextCleanInterceptor) {
return new WebMvcConfigurer() {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userContextCleanInterceptor).addPathPatterns("/**");
}
};
}
}
@@ -0,0 +1,110 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.model.homework.UpFileResultVo;
import com.yunhai.homework.service.IFileService;
import com.yunhai.homework.service.IUserCenterService;
import com.yunhai.platform.auth.LoginUser;
import com.yunhai.platform.auth.UserContextHolder;
import com.yunhai.platform.utils.model.BoaUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Map;
/**
* 平台代理域接口(base/)。
* <p>路径前缀:{@code Config.API_PATH + "/base/"}(原硬编码已修正)。
* 承载平台代理接口:GetUserInfo / GetUserMod / GetSchoolAll / GetGroupUser / Logout,
* 业务逻辑已下沉 {@link IUserCenterService},本控制器仅做"取参 → 调 Service → 返回"。
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/base/")
public class BaseApiController {
private static final Logger log = LoggerFactory.getLogger(BaseApiController.class);
@Autowired
private IUserCenterService userCenterService;
@Autowired
private IFileService fileService;
// ============================ FileUpload ============================
/**
* 附件上传(单文件)。
* 前端: POST base/FileUpload (form-data: file)
* 返回: { fileid, host, fileurl, name, type, size }
*/
@PostMapping("FileUpload")
public UpFileResultVo fileUpload(@RequestParam("file") MultipartFile file) {
return fileService.uploadToResult(file, "homework");
}
// ============================ GetUserInfo ============================
/**
* 获取当前登录用户信息(CAS 会话注入)。
*/
@GetMapping("GetUserInfo")
public BoaUser getUserInfo(BoaUser authUserWithDept) {
return authUserWithDept;
}
// ============================ GetUserMod ============================
/**
* 获取当前用户已授权的模块列表(左侧菜单树)。
* 模块树组装已下沉 IUserCenterService。
*/
@GetMapping("GetUserMod")
public List<Map<String, Object>> getUserMod(BoaUser authUserWithDept) {
Integer schId = authUserWithDept.getCurrentRole() != null
? authUserWithDept.getCurrentRole().getSchId() : null;
return userCenterService.getUserModuleTree(authUserWithDept.getId(), schId);
}
// ============================ GetSchoolAll ============================
/**
* 获取当前用户学校组及用户(树形结构)。
* 组树组装/教师挂组/递归建树逻辑已下沉 IUserCenterService。
*/
@GetMapping("GetSchoolAll")
public List<Map<String, Object>> getSchoolAll(BoaUser authUserWithDept) {
Integer schId = authUserWithDept.getCurrentRole() != null
? authUserWithDept.getCurrentRole().getSchId() : null;
return userCenterService.getSchoolAllTree(schId);
}
// ============================ GetGroupUser ============================
/**
* 获取指定组内用户(教师 + 管理员)。
* 空桩已改为显式报错(避免前端误以为组内无用户),由 IUserCenterService 统一处理。
*/
@GetMapping("GetGroupUser")
public List<Map<String, Object>> getGroupUser(
@RequestParam("gid") Integer gid,
@RequestParam(value = "role", defaultValue = "2") Integer role) {
return userCenterService.getGroupUser(gid, role);
}
// ============================ Logout ============================
/**
* 退出登录。
* 认证由平台网关处理,此处仅返回成功。
*/
@PostMapping("Logout")
public void logout() {
LoginUser user = UserContextHolder.get();
log.info("[logout] 用户退出登录: {}", user == null ? null : user.getLoginName());
}
}
@@ -0,0 +1,99 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.service.IContactsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 联系组接口(contacts/)。
* <p>路径前缀:{@code Config.API_PATH + "/contacts/"}(原硬编码已修正为 Config 拼接)。
* 方法名已为 PascalCase 且写操作已是 POST,保持不动。
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/contacts/")
public class ContactsController {
@Autowired
private IContactsService contactsService;
/**
* 添加联系组。
*/
@PostMapping("AddGroup")
public void addGroup(
@RequestParam("name") String name,
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
contactsService.addGroup(name, Integer.parseInt(type));
}
/**
* 获取联系组列表。
*/
@GetMapping("GetGroup")
public Map<String, Object> getGroup(
@RequestParam(value = "page", defaultValue = "0") Integer page,
@RequestParam(value = "count", defaultValue = "0") Integer count,
@RequestParam(value = "type", defaultValue = "1") Integer type) {
return contactsService.getGroup(page, count, type);
}
/**
* 编辑联系组。
*/
@PostMapping("EditGroup")
public void editGroup(
@RequestParam("name") String name,
@RequestParam("id") String id,
@RequestParam(value = "type", required = false, defaultValue = "1") String type) {
contactsService.editGroup(Integer.parseInt(id), name, Integer.parseInt(type));
}
/**
* 删除联系组(支持批量)。
*/
@PostMapping("DelGroup")
public void delGroup(@RequestParam("ids") String ids) {
contactsService.delGroup(ids);
}
/**
* 设置联系组教师。
*/
@PostMapping("SetContactsTea")
public void setContactsTea(
@RequestParam("gid") String gid,
@RequestParam(value = "ids", required = false) String ids) {
contactsService.setContactsTea(Integer.parseInt(gid), ids);
}
/**
* 获取联系组教师列表。
*/
@GetMapping("GetContactsTea")
public Map<String, Object> getContactsTea(
@RequestParam("gid") String gid,
@RequestParam(value = "page", defaultValue = "0") Integer page,
@RequestParam(value = "count", defaultValue = "0") Integer count) {
return contactsService.getContactsTea(gid, page, count);
}
/**
* 获取联系组树。
*/
@GetMapping("GetGroupTree")
public List<Map<String, Object>> getGroupTree(@RequestParam("type") Integer type) {
return contactsService.getGroupTree(type);
}
/**
* 获取部门角色组树。
*/
@GetMapping("GetDeptRoleGroupTree")
public List<Map<String, Object>> getDeptRoleGroupTree() {
return contactsService.getDeptRoleGroupTree();
}
}
@@ -0,0 +1,85 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.service.IDictService;
import com.yunhai.platform.utils.model.BoaUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 系统字典接口(发文部门域)。
* <p>路径前缀:{@code Config.API_PATH + "/dict/"}(原 base/ 前缀已修正)。
* 写操作一律 POST(原 DelDept/OrdbyDicIds 为 GET 写操作,已 POST 化)。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/dict/")
public class DictController {
@Autowired
private IDictService dictService;
/**
* 获取系统字典(下拉框用)。
*/
@GetMapping("List")
public List<Map<String, Object>> list(@RequestParam(value = "type", defaultValue = "0") Integer type) {
return dictService.getSysDic(type);
}
/**
* 获取系统字典管理列表。
*/
@GetMapping("ManageList")
public List<Map<String, Object>> manageList(@RequestParam(value = "type", defaultValue = "dept") String type) {
return dictService.getDictMangeList(type);
}
/**
* 添加发文单位。
*/
@PostMapping("Add")
public void add(@RequestParam("name") String name) {
dictService.addDeptDic(name);
}
/**
* 更新发文单位。
*/
@PostMapping("Update")
public void update(
@RequestParam("SerID") Integer serID,
@RequestParam("Name") String name,
@RequestParam(value = "Enable", required = false, defaultValue = "1") Integer enable) {
dictService.updateDeptDic(serID, name, enable);
}
/**
* 删除发文单位(原 GET DelDept,已 POST 化)。
*/
@PostMapping("Delete")
public void delete(@RequestParam("serid") Integer serID) {
dictService.delDept(serID);
}
/**
* 发文单位排序(原 GET OrdbyDicIds,已 POST 化)。
*/
@PostMapping("Sort")
public void sort(@RequestParam("ids") String ids) {
dictService.ordbyDicIds(ids);
}
/**
* 本校是否开通短信模块(前端用作"本校是否开通短信")。
* 取当前登录用户角色所属学校,查 sms_school_config 是否开通本应用模块。
*/
@GetMapping("Sync")
public Boolean sync(BoaUser authUserWithDept) {
Integer schId = authUserWithDept != null && authUserWithDept.getCurrentRole() != null
? authUserWithDept.getCurrentRole().getSchId() : null;
return dictService.getSchModuleIsSms(schId);
}
}
@@ -0,0 +1,88 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.model.homework.UpFileResultVo;
import com.yunhai.homework.model.homework.UpFileVo;
import com.yunhai.homework.service.IFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 附件域接口(file/)。
* <p>路径前缀:{@code Config.API_PATH + "/file/"}(原 base/ 前缀已修正为独立文件域)。
* 方法名统一 PascalCase;写操作一律 POST。
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。</p>
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/file/")
public class FileController {
@Autowired
private IFileService fileService;
/**
* 附件上传(单文件)。
* 前端: POST file/Upload (form-data)
* 返回: { fileid, host, fileurl, name, type, size }
*/
@PostMapping("Upload")
public UpFileResultVo upload(@RequestParam("file") MultipartFile file) {
return fileService.uploadToResult(file, "homework");
}
/**
* 图片上传(多文件,返回数组)。
* 前端: POST file/UploadImage (form-data, accept=image/*)
*/
@PostMapping("UploadImage")
public List<UpFileVo> uploadImage(@RequestParam("file") MultipartFile[] files) {
return fileService.uploadImages(files, "homework");
}
/**
* 文件下载(浏览器直接 location.href 跳转)。
*/
@GetMapping("Down")
public void down(@RequestParam("id") Integer id, HttpServletResponse response) {
fileService.download(id, response);
}
/**
* 文件在线预览(inline 输出,前端取 Blob 交给 open-file-viewer 渲染)。
*/
@GetMapping("Preview")
public void preview(@RequestParam("id") Integer id,
@RequestParam(value = "type", required = false) Integer type,
HttpServletResponse response) {
fileService.preview(id, response);
}
/**
* 删除文件。
*/
@PostMapping("Delete")
public void delete(@RequestParam("fileId") Integer fileId) {
fileService.delete(fileId);
}
/**
* 查询某作业下的附件列表。
*/
@GetMapping("List")
public List<UpFileVo> list(@RequestParam("topicId") Integer topicId) {
return fileService.listByTopic("homework", topicId);
}
/**
* 批量绑定附件到作业。
*/
@PostMapping("Bind")
public void bind(@RequestParam("fileIds") List<Integer> fileIds,
@RequestParam("topicId") Integer topicId) {
fileService.bindToTopic(fileIds, topicId);
}
}
@@ -0,0 +1,217 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.common.validation.ValidationGroups;
import com.yunhai.homework.model.BasePageResult;
import com.yunhai.homework.model.homework.*;
import com.yunhai.homework.service.IHomeworkService;
import com.yunhai.homework.utils.PageUtils;
import com.yunhai.platform.utils.model.BoaUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.SortDefault;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 课程作业接口(合并 HomeworkApiController + 旧 HomeworkController)。
* <p>路径前缀:{@code Config.API_PATH + "/homework/"}(platform = api / manage / mobiapi)。
* 方法动词式 PascalCase(对齐 UserBase);写操作一律 POST。
* 返回裸对象,由 ApiResponseAdvice 自动包装为 {code, msg, data}。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/homework/")
public class HomeworkController {
@Autowired
private IHomeworkService homeworkService;
// ============================ 收件箱列表 ============================
/**
* 已收作业列表(用户端)。
*/
@GetMapping("List")
public HomeworkPageResult<HomeworkVo> list(HomeworkQuery query,
@SortDefault(sort = {"topmost", "createTime"},
direction = Sort.Direction.DESC) Sort sort) {
return homeworkService.userList(query, PageUtils.toPageable(query, sort));
}
// ============================ 已发/全部作业列表 ============================
/**
* 已发/全部作业列表(管理端)。type=user 只看自己。
*/
@GetMapping("SendList")
public BasePageResult<HomeworkVo> sendList(HomeworkQuery query,
@SortDefault(sort = "createTime",
direction = Sort.Direction.DESC) Sort sort) {
return homeworkService.adminList(query, PageUtils.toPageable(query, sort));
}
// ============================ 查看详情(标读) ============================
/**
* 查看作业详情(type=1 普通查看标记已读, type=0 编辑模式不标记已读)。
*/
@GetMapping("Detail")
public HomeworkDetailVo detail(@RequestParam("serid") Integer serid,
@RequestParam(value = "type", defaultValue = "1") Integer type) {
return homeworkService.detailMap(serid, type);
}
// ============================ 编辑回显(不标读) ============================
/**
* 获取作业详情(编辑/转发用,不标记已读)。
*/
@GetMapping("GetDetail")
public HomeworkDetailVo getDetail(@RequestParam("serid") Integer serid) {
return homeworkService.getDetailMap(serid);
}
// ============================ 新建 ============================
/**
* 新建作业(JSON body)。fileIds 为附件ID逗号串,smsFlag 为短信标记。
*/
@PostMapping("Create")
public Integer create(@RequestBody @Validated(ValidationGroups.Create.class) HomeworkRequest request) {
return homeworkService.publish(request);
}
// ============================ 编辑 ============================
/**
* 编辑作业(JSON body,含 id)。
*/
@PostMapping("Edit")
public void edit(@RequestBody @Validated(ValidationGroups.Update.class) HomeworkRequest request) {
homeworkService.edit(request.getSourceId() != null ? request.getSourceId() : 0, request);
}
// ============================ 批量删除 ============================
/**
* 批量删除作业(ids 数组)。
*/
@PostMapping("Delete")
public void delete(@RequestBody List<Integer> ids) {
homeworkService.deleteBatch(ids);
}
// ============================ 撤回 ============================
/**
* 撤回作业。
*/
@PostMapping("UserPull")
public void userPull(@RequestParam("serid") Integer serid) {
homeworkService.pull(serid);
}
// ============================ 置顶 ============================
/**
* 置顶/取消置顶。
*/
@PostMapping("SetTopmost")
public void setTopmost(@RequestParam("serid") Integer serid,
@RequestParam(value = "topmost", defaultValue = "1") Integer topmost) {
homeworkService.setTopmost(serid, topmost);
}
// ============================ 追加接收人 ============================
/**
* 追加接收人。
*/
@PostMapping("ReceiveUsers")
public void receiveUsers(@RequestParam("serid") Integer serid,
@RequestParam("users") String users) {
homeworkService.receiveUsers(serid, users);
}
// ============================ 催收 ============================
/**
* 催收未读。
*/
@PostMapping("Expedite")
public void expedite(@RequestParam("serid") Integer serid) {
homeworkService.expedite(serid);
}
// ============================ 一键已读 ============================
/**
* 一键全部已读(POST 化,原为 GET 写操作)。
*/
@PostMapping("InitializeRed")
public InitializeRedVo initializeRed() {
return homeworkService.initializeRedResult();
}
// ============================ 阅读情况 ============================
/**
* 阅读情况(按已读/未读分组)。
*/
@GetMapping("ReadDetail")
public List<Map<String, Object>> readDetail(@RequestParam("serid") Integer serid) {
return homeworkService.getReadDetailGrouped(serid);
}
// ============================ 上一篇/下一篇 ============================
/**
* 上一篇/下一篇作业ID。
*/
@GetMapping("GetNextPrev")
public Map<String, Object> getNextPrev(@RequestParam("serid") Integer serid,
@RequestParam("type") String type) {
return homeworkService.getNextPrev(serid, type);
}
// ============================ 附件打包下载 ============================
/**
* 批量下载作业全部附件(打包 zip)。
*/
@GetMapping("AllFileDown")
public void allFileDown(@RequestParam("dataid") Integer dataid,
HttpServletResponse response) {
homeworkService.downloadAllFiles(dataid, response);
}
// ============================ 统计 ============================
/**
* 作业统计。
*/
@GetMapping("Statis")
public HomeworkStatisticsVo statis(
@RequestParam(value = "starttime", required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime start,
@RequestParam(value = "endtime", required = false)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime end) {
return homeworkService.statistics(new HomeworkQuery(), start, end);
}
// ============================ 转发(合并旧 REST 壳 forward) ============================
/**
* 转发作业(基于源作业创建新作业)。sourceId 在 request body 中传递。
*/
@PostMapping("Forward")
public Integer forward(@RequestBody @Validated(ValidationGroups.Create.class) HomeworkRequest request) {
return homeworkService.forward(request.getSourceId(), request);
}
}
@@ -0,0 +1,53 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.model.v2.NotificationPageResult;
import com.yunhai.homework.model.v2.NotificationQuery;
import com.yunhai.homework.service.impl.NotificationService;
import com.yunhai.homework.utils.UserContext;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 站内通知接口(PRD §18 未列,按计划 §7.3 补充)。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/notifications")
public class NotificationController {
@Autowired
private NotificationService notificationService;
/** 我的通知分页(含未读数;type/isRead 过滤) */
@GetMapping
public NotificationPageResult page(NotificationQuery query) {
return notificationService.page(currentUserId(), query);
}
/** 标记单条已读 */
@PostMapping("/{id}/read")
public void markRead(@PathVariable("id") Integer id) {
notificationService.markRead(id, currentUserId());
}
/** 全部已读 */
@PostMapping("/read-all")
public void markAllRead() {
notificationService.markAllRead(currentUserId());
}
/** 当前登录人ID(用户中心数字ID),无登录态时拒绝 */
private static Integer currentUserId() {
Integer userId = UserContext.getUserId();
if (userId == null) {
throw new BizException(ErrorCode.FORBIDDEN, "未获取到登录身份");
}
return userId;
}
}
@@ -0,0 +1,88 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.service.ISmsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 短信设置接口(短信域)。
* <p>路径前缀:{@code Config.API_PATH + "/sms/"}(原 schsms/ 已修正)。
* 方法名 PascalCase(原全小写已修正);写操作一律 POST(原 setschallsms/setschmodulesms 为 GET 写操作,已 POST 化)。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/sms/")
public class SmsController {
@Autowired
private ISmsService smsService;
/**
* 获取短信模块列表。
*/
@GetMapping("GetModule")
public List<Map<String, Object>> getModule() {
return smsService.getModuleList();
}
/**
* 获取学校父类组列表。
*/
@GetMapping("GetGroup")
public List<Map<String, Object>> getGroup() {
return smsService.getSchParentGroup();
}
/**
* 分页获取学校短信开关列表。
*/
@GetMapping("GetSchoolSms")
public Map<String, Object> getSchoolSms(
@RequestParam(value = "schpid", required = false) String schpid,
@RequestParam("PageSize") Integer pageSize,
@RequestParam("PageIndex") Integer pageIndex,
@RequestParam(value = "SchName", required = false, defaultValue = "") String schName) {
return smsService.getSchIsSmsList(pageSize, pageIndex, schName);
}
/**
* 获取学校列表。
*/
@GetMapping("GetSchool")
public List<Map<String, Object>> getSchool() {
return smsService.getSchList();
}
/**
* 设置单校单模块短信开关。
*/
@PostMapping("SetSchoolSms")
public void setSchoolSms(@RequestParam("itemsModu") String itemsModuJson) {
smsService.setSchSmsFromJson(itemsModuJson);
}
/**
* 设置某校全部模块短信开关(原 GET setschallsms,已 POST 化)。
*/
@PostMapping("SetAllSchoolSms")
public void setAllSchoolSms(
@RequestParam("SchSerID") Integer schSerID,
@RequestParam("IsSms") Boolean isSms) {
smsService.setSchSmsAll(schSerID, isSms);
}
/**
* 批量设置多校多模块短信开关(原 GET setschmodulesms,已 POST 化)。
*/
@PostMapping("SetSchoolModuleSms")
public void setSchoolModuleSms(
@RequestParam("SchSerID") String schSerID,
@RequestParam("Module") String module,
@RequestParam("IsSms") Boolean isSms) {
smsService.setSchModuleSms(schSerID, module, isSms);
}
}
@@ -0,0 +1,79 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.model.BasePageQuery;
import com.yunhai.homework.model.BasePageResult;
import com.yunhai.homework.model.v2.StudentHomeworkDetailVo;
import com.yunhai.homework.model.v2.StudentHomeworkVo;
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.service.impl.StudentHomeworkService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 课堂作业 V2 学生端接口(PRD §18 路径一字对齐:/student/homeworks…)。
* <p>
* 身份:当前登录用户ID ↔ class_student 名册;仅可见本人班级作业,禁 IDOR。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/student/homeworks")
public class StudentHomeworkController {
@Autowired
private StudentHomeworkService studentHomeworkService;
/** 我的作业列表(tab=all/todo/done/closed) */
@GetMapping
public BasePageResult<StudentHomeworkVo> list(
@RequestParam(value = "tab", defaultValue = "all") String tab,
BasePageQuery query) {
return studentHomeworkService.list(tab, query);
}
/** 作业详情 */
@GetMapping("/{id}")
public StudentHomeworkDetailVo detail(@PathVariable("id") Integer id) {
return studentHomeworkService.detail(id);
}
/** 阅读上报(前端停留 ≥3 秒后调用;幂等) */
@PostMapping("/{id}/read")
public void read(@PathVariable("id") Integer id) {
studentHomeworkService.read(id);
}
/** 提交作业,返回新提交行 id */
@PostMapping("/{id}/submissions")
public Map<String, Integer> submit(@PathVariable("id") Integer id,
@RequestBody @Validated StudentSubmitRequest request) {
Integer submissionId = studentHomeworkService.submit(id, request);
return java.util.Collections.singletonMap("submissionId", submissionId);
}
/** 撤回 / 打回后重交(action=withdraw|resubmit),返回新提交行 id */
@PutMapping("/{id}/submissions/{submissionId}")
public Map<String, Integer> action(@PathVariable("id") Integer id,
@PathVariable("submissionId") Integer submissionId,
@RequestBody @Validated SubmissionActionRequest request) {
Integer newId = studentHomeworkService.action(id, submissionId, request);
return java.util.Collections.singletonMap("submissionId", newId);
}
/** 批阅结果(当前版本 + 历史版本) */
@GetMapping("/{id}/result")
public StudentResultVo result(@PathVariable("id") Integer id) {
return studentHomeworkService.result(id);
}
}
@@ -0,0 +1,219 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.model.BasePageResult;
import com.yunhai.homework.model.v2.ClassTreeNode;
import com.yunhai.homework.model.v2.ExtendRequest;
import com.yunhai.homework.model.v2.HomeworkStatsVo;
import com.yunhai.homework.model.v2.HomeworkV2Request;
import com.yunhai.homework.model.v2.HomeworkV2Vo;
import com.yunhai.homework.model.v2.RemindRequest;
import com.yunhai.homework.model.v2.ReturnRequest;
import com.yunhai.homework.model.v2.ReviewRequest;
import com.yunhai.homework.model.v2.ScheduleRequest;
import com.yunhai.homework.model.v2.StudentMatrixQuery;
import com.yunhai.homework.model.v2.StudentMatrixVo;
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;
import org.springframework.data.web.SortDefault;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* 课堂作业 V2 教师端接口(PRD §18 路径一字对齐:/teacher/homeworks…)。
* <p>
* 与 notice 语义的旧接口(/{platform}/homework/*)并存:旧接口冻结维护,
* 新业务一律走本控制器;返回裸对象由 ApiResponseAdvice 包装 {code,msg,data}。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/teacher/homeworks")
public class TeacherHomeworkV2Controller {
@Autowired
private TeacherHomeworkV2Service teacherHomeworkV2Service;
@Autowired
private ClassStudentService classStudentService;
// ============================ 页 1:发布作业 ============================
/** 创建作业(落草稿) */
@PostMapping
public Integer create(@RequestBody @Validated HomeworkV2Request request) {
return teacherHomeworkV2Service.create(request);
}
/** 编辑作业(仅草稿) */
@PutMapping("/{id}")
public void edit(@PathVariable("id") Integer id,
@RequestBody @Validated HomeworkV2Request request) {
teacherHomeworkV2Service.edit(id, request);
}
/** 删除作业(仅草稿) */
@DeleteMapping("/{id}")
public void delete(@PathVariable("id") Integer id) {
teacherHomeworkV2Service.delete(id);
}
/** 立即发布(草稿 → 进行中) */
@PostMapping("/{id}/publish")
public void publish(@PathVariable("id") Integer id) {
teacherHomeworkV2Service.publish(id);
}
/** 定时发布 / 取消定时(body: {scheduledAt} 或 {cancel:true}) */
@PostMapping("/{id}/schedule")
public void schedule(@PathVariable("id") Integer id, @RequestBody ScheduleRequest request) {
teacherHomeworkV2Service.schedule(id, request);
}
// ============================ 页 2:草稿箱 ============================
/** 草稿箱分页 */
@GetMapping("/drafts")
public BasePageResult<HomeworkV2Vo> drafts(TeacherHomeworkQuery query,
@SortDefault(sort = "updateTime", direction = Sort.Direction.DESC) Sort sort) {
return teacherHomeworkV2Service.drafts(query, sort);
}
// ============================ 页 3:已发布列表 ============================
/** 已发布列表分页(含应交/已读/已交/未读未交聚合) */
@GetMapping("/published")
public BasePageResult<HomeworkV2Vo> published(TeacherHomeworkQuery query,
@SortDefault(sort = "publishedAt", direction = Sort.Direction.DESC) Sort sort) {
return teacherHomeworkV2Service.published(query, sort);
}
/** 提醒未读/未交学生,返回实际提醒人数 */
@PostMapping("/{id}/remind")
public Map<String, Integer> remind(@PathVariable("id") Integer id, @RequestBody RemindRequest request) {
int reminded = teacherHomeworkV2Service.remind(id, request);
return java.util.Collections.singletonMap("reminded", reminded);
}
/** 延长截止时间 */
@PostMapping("/{id}/extend")
public void extend(@PathVariable("id") Integer id, @RequestBody @Validated ExtendRequest request) {
teacherHomeworkV2Service.extend(id, request);
}
/** 复制为新草稿,返回新作业 id */
@PostMapping("/{id}/copy")
public Integer copy(@PathVariable("id") Integer id) {
return teacherHomeworkV2Service.copy(id);
}
/** 归档(只读,不再触发任何提醒) */
@PostMapping("/{id}/archive")
public void archive(@PathVariable("id") Integer id) {
teacherHomeworkV2Service.archive(id);
}
/** 导出学生矩阵 Excel */
@GetMapping("/{id}/export")
public void export(@PathVariable("id") Integer id, HttpServletResponse response) throws IOException {
teacherHomeworkV2Service.export(id, response);
}
// ============================ 页 4:详情 / 统计 ============================
/** 作业详情 */
@GetMapping("/{id}")
public HomeworkV2Vo detail(@PathVariable("id") Integer id) {
return teacherHomeworkV2Service.detail(id);
}
/** 汇总统计卡片(总进度 + 分班进度) */
@GetMapping("/{id}/stats")
public HomeworkStatsVo stats(@PathVariable("id") Integer id) {
return teacherHomeworkV2Service.stats(id);
}
// ============================ 页 5:学生矩阵 ============================
/** 学生矩阵分页(阅读/提交/批阅状态,未交学生生成占位行) */
@GetMapping("/{id}/students")
public BasePageResult<StudentMatrixVo> students(@PathVariable("id") Integer id,
StudentMatrixQuery query) {
return teacherHomeworkV2Service.students(id, query);
}
// ============================ 页 6:批阅 / 打回 / 版本 ============================
/** 保存批阅(→ REVIEWED,推送批阅完成通知) */
@PostMapping("/{id}/submissions/{studentId}/review")
public void review(@PathVariable("id") Integer id, @PathVariable("studentId") Integer studentId,
@RequestBody @Validated ReviewRequest request) {
teacherHomeworkV2Service.review(id, studentId, request);
}
/** 打回(→ RETURNED,推送打回通知) */
@PostMapping("/{id}/submissions/{studentId}/return")
public void returnSubmission(@PathVariable("id") Integer id, @PathVariable("studentId") Integer studentId,
@RequestBody @Validated ReturnRequest request) {
teacherHomeworkV2Service.returnSubmission(id, studentId, request);
}
/** 历史提交版本(批阅弹窗切换版本) */
@GetMapping("/{id}/submissions/{studentId}/versions")
public List<SubmissionVersionVo> versions(@PathVariable("id") Integer id,
@PathVariable("studentId") Integer studentId) {
return teacherHomeworkV2Service.versions(id, studentId);
}
// ============================ 辅助:学科 ============================
/** 我布置过的学科(本人已发布及之后作业的学科 ∪ 我的任教学科,合并去重;草稿不计) */
@GetMapping("/subjects/assigned")
public List<SubjectDto> assignedSubjects() {
return teacherHomeworkV2Service.assignedSubjects();
}
/** 我的任教学科(bschclass 任教关系去重,名称取本校学科字典) */
@GetMapping("/subjects/teaching")
public List<SubjectDto> teachingSubjects() {
return teacherHomeworkV2Service.teachingSubjects();
}
/** 按学科筛选我的任教班级(subId=学科编码;班级名称取学校班级列表) */
@GetMapping("/subjects/classes")
public List<TeachingClassVo> teachingClasses(@RequestParam("subId") String subId) {
return teacherHomeworkV2Service.teachingClasses(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);
}
}
@@ -0,0 +1,88 @@
package com.yunhai.homework.controller.api;
import com.yunhai.homework.config.Config;
import com.yunhai.homework.service.ITemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 作业模板接口(模板域)。
* <p>路径前缀:{@code Config.API_PATH + "/template/"}(原 homework/Template/ 三级路径已修正)。
* 写操作一律 POST(原 EditType/DelTemplate 为 GET 写操作,已 POST 化)。
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/template/")
public class TemplateController {
@Autowired
private ITemplateService templateService;
/**
* 获取模板类型列表。
*/
@GetMapping("Typelist")
public List<Map<String, Object>> getTypelist() {
return templateService.getTypelist();
}
/**
* 新增/编辑模板类型(原 GET EditType,已 POST 化)。
*/
@PostMapping("TypeEdit")
public void typeEdit(
@RequestParam("typeid") Integer typeid,
@RequestParam("TypeName") String typeName) {
templateService.editType(typeid, typeName);
}
/**
* 删除模板类型(原 GET DelType,已 POST 化)。
*/
@PostMapping("TypeDelete")
public void typeDelete(@RequestParam("typeid") Integer typeid) {
templateService.delType(typeid);
}
/**
* 分页获取模板列表。
*/
@GetMapping("List")
public Map<String, Object> list(
@RequestParam("PageSize") Integer pageSize,
@RequestParam("PageIndex") Integer pageIndex,
@RequestParam(value = "typeid", defaultValue = "0") Integer typeid) {
return templateService.getTemplatelist(pageSize, pageIndex, typeid);
}
/**
* 新增/编辑模板。
*/
@PostMapping("TemplateEdit")
public void templateEdit(
@RequestParam(value = "SerID", required = false) Integer serID,
@RequestParam("Title") String title,
@RequestParam("Contents") String contents,
@RequestParam(value = "Type", required = false) Integer type,
@RequestParam(value = "Cover", required = false) Integer cover) {
templateService.editTemplate(serID, title, contents, type, cover);
}
/**
* 删除模板(原 GET DelTemplate,已 POST 化)。
*/
@PostMapping("TemplateDelete")
public void templateDelete(@RequestParam("SerID") Integer serid) {
templateService.delTemplate(serid);
}
/**
* 获取单个模板详情。
*/
@GetMapping("TemplateFirst")
public Map<String, Object> templateFirst(@RequestParam("serid") Integer serid) {
return templateService.getTemplateFist(serid);
}
}
@@ -0,0 +1,49 @@
package com.yunhai.homework.entity;
import com.yunhai.homework.common.listener.HomeworkAuditListener;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 所有实体的公共基类:自增主键 + 创建人/时间 + 更新人/时间(由 {@link HomeworkAuditListener} 自动填充)。
*/
@MappedSuperclass
@Getter
@Setter
@EntityListeners(HomeworkAuditListener.class)
public abstract class BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* 创建人ID(平台用户ID)
*/
@Column(name = "create_by", updatable = false)
private String createBy;
/**
* 创建时间
*/
@Column(name = "create_time", updatable = false)
private LocalDateTime createTime;
/**
* 更新人ID(平台用户ID)
*/
@Column(name = "update_by")
private String updateBy;
/**
* 更新时间
*/
@Column(name = "update_time")
private LocalDateTime updateTime;
}
@@ -0,0 +1,59 @@
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;
}
@@ -0,0 +1,28 @@
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;
/**
* 联系组。对应原 .NET Contacts_Group 表。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "contacts_group")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class ContactsGroup extends BaseEntity {
/** 组名称 */
@Column(name = "group_name", length = 200)
private String groupName;
/** 创建人ID(用户中心数字ID,同教师ID空间) */
@Column(name = "creator_id")
private Integer creatorId;
}
@@ -0,0 +1,35 @@
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;
/**
* 联系组-用户关联。对应原 .NET Contacts_Tea 表。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "contacts_group_user",
uniqueConstraints = @UniqueConstraint(name = "uk_group_user",
columnNames = {"group_id", "user_id"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class ContactsGroupUser extends BaseEntity {
/** 联系组ID */
@Column(name = "group_id", nullable = false)
private Integer groupId;
/** 用户ID(基础平台用户ID) */
@Column(name = "user_id", nullable = false)
private Integer userId;
/** 用户姓名 */
@Column(name = "user_name", length = 64)
private String userName;
}
@@ -0,0 +1,208 @@
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 java.time.LocalDateTime;
/**
* 课程作业主表。对应原 .NET Notify_Message(已剔除短信/模板/门户等无关字段)。
* <p>
* 阅读状态不落主表:采用「懒标记」模型——读者首次查看时写入 {@link HomeworkRead},
* 不存在阅读记录即视为未读,从而无需在发作业时枚举全校用户预插接收行。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class Homework extends BaseEntity {
/**
* 作业标题
*/
@Column(name = "title", nullable = false, length = 200)
private String title;
/**
* 学校ID(多租户隔离:发布时从当前用户角色填充,查询时按校过滤)。
*/
@Column(name = "sch_id")
private Integer schId;
/**
* 作业内容
*/
@Column(name = "content", columnDefinition = "LONGTEXT")
private String content;
/**
* 作业类型:1 普通 / 2 重要(见 {@link com.yunhai.homework.enums.HomeworkTypeEnum})
*/
@Column(name = "type", nullable = false)
private Integer type = 1;
/**
* 发布人ID(用户中心数字ID,同教师ID空间)
*/
@Column(name = "publisher_id")
private Integer publisherId;
/**
* 发布人姓名
*/
@Column(name = "publisher_name", length = 64)
private String publisherName;
/**
* 发布部门ID(原 DeptID)
*/
@Column(name = "dept_id", length = 64)
private String deptId;
/**
* 发布部门名称
*/
@Column(name = "dept_name", length = 128)
private String deptName;
/**
* 状态:1 已发布(无草稿态,见 {@link com.yunhai.homework.enums.HomeworkStatusEnum})
*/
@Column(name = "status", nullable = false)
private Integer status = 1;
/**
* 是否置顶:0 否 / 1 是(原 topmost)
*/
@Column(name = "topmost")
private Integer topmost = 0;
// ==================== 迁移补全字段(对应 C# Notify_Message) ====================
/**
* 接收人列表(JSON 数组,如 ["10012","10013"] 或接收组ID列表)。
* 对应 C# Notify_Message.ReceiveList
*/
@Column(name = "receive_list", columnDefinition = "LONGTEXT")
private String receiveList;
/**
* 密码发送(对应 C# Notify_Message.PwdSend)
*/
@Column(name = "pwd_send", length = 255)
private String pwdSend;
/**
* 抄送人列表(对应 C# Notify_Message.ChaoSend)
*/
@Column(name = "chao_send", length = 2000)
private String chaoSend;
/**
* 已读数(发送时初始化为 0,也可由 HomeworkRead 表 COUNT 实时计算)。
* 对应 C# Notify_Message.readcount
*/
@Column(name = "read_count")
private Integer readCount = 0;
/**
* 应读数(接收人总数)。对应 C# Notify_Message.allowcount
*/
@Column(name = "allow_count")
private Integer allowCount = 0;
/**
* 父作业ID(转发时关联原作业)。对应 C# Notify_Message.fkid
*/
@Column(name = "parent_id")
private Integer parentId;
/**
* 回复ID。对应 C# Notify_Message.reid
*/
@Column(name = "reply_id", length = 255)
private String replyId;
/**
* 是否撤回:0 否 / 1 是。对应 C# Notify_Message.ispull
*/
@Column(name = "is_pulled")
private Integer isPulled = 0;
/**
* 定时发送时间(到时间后自动发布)。对应 C# Notify_Message.DsfsTime。
* PRD 语义中的 scheduled_at 复用本列。
*/
@Column(name = "timing_send_time")
private LocalDateTime timingSendTime;
/**
* 短信发送标记:0 不发短信 / 1 发短信。对应 C# Notify_Message.dxbit
*/
@Column(name = "sms_flag")
private Integer smsFlag = 0;
// ==================== 课堂作业 V3(PRD V1.0)扩展列 ====================
/**
* 学科(字典值)。
*/
@Column(name = "subject", length = 50)
private String subject;
/**
* 截止时间(精确到分钟;空 = 仅查阅型,不设截止,到点关闭任务跳过)。
*/
@Column(name = "deadline_at")
private LocalDateTime deadlineAt;
/**
* 作业流程状态(PRD 五态,见 {@link com.yunhai.homework.enums.HwStatusEnum})。
* notice 原 status 列继续承载发布/撤回语义,两者并存。
*/
@Column(name = "hw_status", nullable = false, length = 20)
private String hwStatus = com.yunhai.homework.enums.HwStatusEnum.DRAFT_UNPUBLISHED.getCode();
/**
* 发布时间(立即发布或定时到点发布时写入)。
*/
@Column(name = "published_at")
private LocalDateTime publishedAt;
/**
* 提交方式,JSON 数组文本,如 ["image","attach"]。
* 取值:text / image / attach / none(PRD 的 图片/文件/两者 映射为
* ["image"] / ["attach"] / ["image","attach"])。
*/
@Column(name = "submit_type", length = 100)
private String submitType;
/**
* 截止后是否允许逾期提交:0 否 / 1 是。
*/
@Column(name = "allow_late", nullable = false)
private Integer allowLate = 0;
/**
* 提醒设置:截止前 1 天提醒(0 关 / 1 开)。
*/
@Column(name = "remind_1d", nullable = false)
private Integer remind1d = 1;
/**
* 提醒设置:截止前 1 小时提醒(0 关 / 1 开)。
*/
@Column(name = "remind_1h", nullable = false)
private Integer remind1h = 1;
/**
* 归档时间(教师手动归档时写入;归档后只读,所有提醒任务跳过)。
*/
@Column(name = "archived_at")
private LocalDateTime archivedAt;
}
@@ -0,0 +1,44 @@
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;
/**
* 作业附件表(PRD homework_attachment)。文件实体仍存 up_file(file_id 关联 + file_url 冗余)。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_attachment")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkAttachment extends BaseEntity {
/** 作业ID */
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/** up_file.id(可空:允许外部 URL 附件) */
@Column(name = "file_id")
private Integer fileId;
/** 文件访问 URL */
@Column(name = "file_url", nullable = false, length = 500)
private String fileUrl;
/** 文件类型:image / file / ... */
@Column(name = "file_type", length = 20)
private String fileType;
/** 文件名 */
@Column(name = "file_name", length = 200)
private String fileName;
/** 排序号(越小越靠前) */
@Column(name = "sort", nullable = false)
private Integer sort = 0;
}
@@ -0,0 +1,35 @@
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;
/**
* 作业-班级关联表(PRD homework_class)。
* 发布/编辑时由 receiveList("all" 或班级 id 串)解析写入,是应交名单与统计的权威关联。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_class",
uniqueConstraints = @UniqueConstraint(name = "uk_hw_class", columnNames = {"homework_id", "class_id"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkClass extends BaseEntity {
/** 作业ID */
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/** 班级ID */
@Column(name = "class_id", nullable = false, length = 64)
private String classId;
/** 班级名快照 */
@Column(name = "class_name", length = 50)
private String className;
}
@@ -0,0 +1,49 @@
package com.yunhai.homework.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
/**
* 作业阅读记录(懒标记)。对应原 .NET Notify_ReceiveList 的「逐用户阅读状态」语义,
* 但仅在读者首次查看时落库,避免发作业时预插全员。
* <p>
* 唯一约束 (homework_id, reader_id):同一用户对同一作业只记一条已读。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_read",
uniqueConstraints = @UniqueConstraint(
name = "uk_homework_reader",
columnNames = {"homework_id", "reader_id"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkRead extends BaseEntity {
/**
* 作业ID(关联 homework.id)
*/
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/**
* 读者ID(用户中心数字ID,同教师ID空间)
*/
@Column(name = "reader_id", nullable = false)
private Integer readerId;
/**
* 读者姓名
*/
@Column(name = "reader_name", length = 64)
private String readerName;
/**
* 阅读时间
*/
@Column(name = "read_time")
private LocalDateTime readTime;
}
@@ -0,0 +1,58 @@
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 homework_read_record)。
* <p>
* 学生维度,含首次/最近阅读时间,与 notice 语义的 {@link HomeworkRead}(教师/阅读者已读)互不影响。
* 行仅在判定已读时写入;未读名单 = 名册左连本表(无记录或 is_read=0)。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_read_record",
uniqueConstraints = @UniqueConstraint(name = "uk_hw_student", columnNames = {"homework_id", "student_id"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkReadRecord extends BaseEntity {
/** 作业ID */
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/** 学生ID(用户中心数字ID,即 SchStudent.UserStuSerID) */
@Column(name = "student_id", nullable = false)
private Integer studentId;
/** 学生姓名快照 */
@Column(name = "student_name", length = 50)
private String studentName;
/** 班级ID */
@Column(name = "class_id", length = 64)
private String classId;
/** 阅读状态:0 UNREAD / 1 READ */
@Column(name = "is_read", nullable = false)
private Integer isRead = 0;
/** 首次阅读时间(写入后不再变更) */
@Column(name = "first_read_at")
private LocalDateTime firstReadAt;
/** 最近阅读时间(每次上报刷新) */
@Column(name = "last_read_at")
private LocalDateTime lastReadAt;
/** 阅读次数 */
@Column(name = "read_count", nullable = false)
private Integer readCount = 0;
}
@@ -0,0 +1,45 @@
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.Index;
import javax.persistence.Table;
/**
* 作业接收人关联表(替代旧 receive_list JSON 字符串存储)。
* <p>
* 每条记录代表一个作业的一个接收人(用户ID + 姓名快照,不含账号)。
* 全员作业(receive_list="ALL")不写明细行,
* 读取时按 receive_list="ALL" 标识走旧逻辑兜底。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_receiver",
indexes = {
@Index(name = "idx_homework_id", columnList = "homework_id"),
@Index(name = "idx_receiver", columnList = "receiver_id")
})
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkReceiver extends BaseEntity {
/** 作业ID(关联 homework.id) */
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/** 接收人ID(用户中心数字ID) */
@Column(name = "receiver_id", nullable = false)
private Integer receiverId;
/** 接收人姓名快照 */
@Column(name = "receiver_name", length = 64)
private String receiverName;
/** 接收类型:1 接收 / 2 抄送 */
@Column(name = "receive_type", nullable = false)
private Integer receiveType = 1;
}
@@ -0,0 +1,98 @@
package com.yunhai.homework.entity;
import com.yunhai.homework.enums.ReviewStatusEnum;
import com.yunhai.homework.enums.SubmitStatusEnum;
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.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 作业提交表,多版本建模(PRD homework_submission)。
* <p>
* 同一学生 (homework_id, student_id) 下 version 单调递增;提交/打回重交/撤回均新增行,
* 旧行 is_latest 置 0 保留历史;is_latest=1 的行是当前有效版本。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_submission",
uniqueConstraints = @UniqueConstraint(name = "uk_hw_student_ver",
columnNames = {"homework_id", "student_id", "version"}))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkSubmission extends BaseEntity {
/** 作业ID */
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/** 学生ID(用户中心数字ID,即 SchStudent.UserStuSerID) */
@Column(name = "student_id", nullable = false)
private Integer studentId;
/** 学生姓名快照 */
@Column(name = "student_name", nullable = false, length = 50)
private String studentName;
/** 班级ID */
@Column(name = "class_id", nullable = false, length = 64)
private String classId;
/** 班级名快照 */
@Column(name = "class_name", nullable = false, length = 50)
private String className;
/** 提交版本,同一学生单调递增(撤回/重交均 +1) */
@Column(name = "version", nullable = false)
private Integer version = 1;
/** 是否当前版本:1 是 / 0 历史 */
@Column(name = "is_latest", nullable = false)
private Integer isLatest = 1;
/** 提交状态(见 {@link SubmitStatusEnum}) */
@Column(name = "status", nullable = false, length = 20)
private String status = SubmitStatusEnum.SUBMITTED.getCode();
/** 是否逾期:0 否 / 1 是 */
@Column(name = "is_late", nullable = false)
private Integer isLate = 0;
/** 提交时间(NOT_SUBMITTED 空行为空) */
@Column(name = "submitted_at")
private LocalDateTime submittedAt;
/** 文本作业内容 */
@Column(name = "text", columnDefinition = "TEXT")
private String text;
/** 评分 */
@Column(name = "score", precision = 6, scale = 2)
private BigDecimal score;
/** 评语(打回时 = 订正原因,必填) */
@Column(name = "comment", columnDefinition = "TEXT")
private String comment;
/** 批阅状态(见 {@link ReviewStatusEnum}) */
@Column(name = "review_status", nullable = false, length = 20)
private String reviewStatus = ReviewStatusEnum.PENDING.getCode();
/** 批阅人ID(用户中心数字ID,同教师ID空间) */
@Column(name = "reviewed_by")
private Integer reviewedBy;
/** 批阅人姓名快照 */
@Column(name = "reviewed_by_name", length = 64)
private String reviewedByName;
/** 批阅时间 */
@Column(name = "reviewed_at")
private LocalDateTime reviewedAt;
}
@@ -0,0 +1,40 @@
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;
/**
* 作业模板。对应原 .NET Notify_Template 表。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "homework_template")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class HomeworkTemplate extends BaseEntity {
/** 模板标题 */
@Column(name = "title", length = 200)
private String title;
/** 模板内容(HTML) */
@Column(name = "content", columnDefinition = "LONGTEXT")
private String content;
/** 封面图片文件ID */
@Column(name = "cover")
private Integer cover;
/** 引用次数 */
@Column(name = "ref_count")
private Integer refCount = 0;
/** 模板类型ID */
@Column(name = "type_id")
private Integer typeId;
}
@@ -0,0 +1,71 @@
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.Index;
import javax.persistence.Table;
import java.time.LocalDateTime;
/**
* 站内通知(PRD notification)。六类通知的统一落表 + 已读状态。
* 通知已读独立于作业阅读记录(本表 is_read 只反映通知列表已读)。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "notification",
indexes = {
@Index(name = "idx_receiver", columnList = "receiver_id, is_read"),
@Index(name = "idx_hw_type", columnList = "homework_id, type")
})
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class Notification extends BaseEntity {
/** 通知类型(见 {@link com.yunhai.homework.enums.NotificationTypeEnum}) */
@Column(name = "type", nullable = false, length = 30)
private String type;
/** 关联作业ID */
@Column(name = "homework_id")
private Integer homeworkId;
/** 关联业务ID(如 submissionId) */
@Column(name = "related_id")
private Integer relatedId;
/** 通知标题 */
@Column(name = "title", nullable = false, length = 200)
private String title;
/** 通知内容 */
@Column(name = "content", length = 1000)
private String content;
/** 接收人ID(学生 = student_id,教师 = 用户中心数字ID,同一 ID 空间) */
@Column(name = "receiver_id", nullable = false)
private Integer receiverId;
/** 接收人姓名快照 */
@Column(name = "receiver_name", length = 64)
private String receiverName;
/** 接收人角色:student / teacher */
@Column(name = "receiver_role", nullable = false, length = 20)
private String receiverRole = "student";
/** 是否已读:0 未读 / 1 已读 */
@Column(name = "is_read", nullable = false)
private Integer isRead = 0;
/** 已读时间 */
@Column(name = "read_at")
private LocalDateTime readAt;
/** 发送通道:inapp 站内 / sms 短信 */
@Column(name = "send_channel", nullable = false, length = 20)
private String sendChannel = "inapp";
}
@@ -0,0 +1,46 @@
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;
}
@@ -0,0 +1,28 @@
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;
/**
* 短信模块配置。对应原 .NET SchSms 表。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "sms_module")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class SmsModule extends BaseEntity {
/** 模块名称 */
@Column(name = "module_name", length = 100)
private String moduleName;
/** 模块标识 */
@Column(name = "module_code", length = 50)
private String moduleCode;
}
@@ -0,0 +1,37 @@
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;
/**
* 学校短信开通配置。对应原 .NET SchSms_Set 表。
* 记录某学校某模块是否开通短信。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "sms_school_config")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class SmsSchoolConfig extends BaseEntity {
/** 学校ID */
@Column(name = "school_id", nullable = false)
private Integer schoolId;
/** 学校名称 */
@Column(name = "school_name", length = 200)
private String schoolName;
/** 模块ID */
@Column(name = "module_id")
private Integer moduleId;
/** 是否开通短信 */
@Column(name = "is_sms")
private Boolean isSms = false;
}
@@ -0,0 +1,54 @@
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.Index;
import javax.persistence.Table;
/**
* 提交附件表(PRD submission_attachment)。替代 JSON 方案,支撑矩阵缩略图与批阅大图预览。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "submission_attachment",
indexes = @Index(name = "idx_sub", columnList = "submission_id"))
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class SubmissionAttachment extends BaseEntity {
/** 提交行ID(homework_submission.id) */
@Column(name = "submission_id", nullable = false)
private Integer submissionId;
/** 作业ID(冗余,便于统计与清理) */
@Column(name = "homework_id", nullable = false)
private Integer homeworkId;
/** 学生ID(用户中心数字ID) */
@Column(name = "student_id")
private Integer studentId;
/** up_file.id(可空:允许外部 URL) */
@Column(name = "file_id")
private Integer fileId;
/** 文件访问 URL */
@Column(name = "file_url", nullable = false, length = 500)
private String fileUrl;
/** 文件类型:image / file */
@Column(name = "file_type", length = 20)
private String fileType;
/** 文件名 */
@Column(name = "file_name", length = 200)
private String fileName;
/** 排序号 */
@Column(name = "sort", nullable = false)
private Integer sort = 0;
}
@@ -0,0 +1,40 @@
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;
/**
* 系统字典(发文部门等)。对应原 .NET Common_DeptDic 表。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "sys_dict")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class SysDict extends BaseEntity {
/** 字典类型(如 "dept"=发文部门) */
@Column(name = "dict_type", length = 50)
private String dictType;
/** 字典 key(原 CodeID) */
@Column(name = "dict_key", length = 100)
private String dictKey;
/** 字典 value(显示名称,原 Name) */
@Column(name = "dict_value", length = 200)
private String dictValue;
/** 排序号 */
@Column(name = "sort_order")
private Integer sortOrder = 0;
/** 是否启用:0 禁用 / 1 启用(原 Enable) */
@Column(name = "enable_flag")
private Integer enableFlag = 1;
}
@@ -0,0 +1,28 @@
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;
/**
* 作业模板类型。对应原 .NET Notify_TemplateType 表。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "template_type")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class TemplateType extends BaseEntity {
/** 类型名称 */
@Column(name = "type_name", length = 100)
private String typeName;
/** 排序号 */
@Column(name = "sort_order")
private Integer sortOrder = 0;
}
@@ -0,0 +1,47 @@
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;
/**
* 附件文件记录。对应原 .NET Common_UpFiles 表。
* <p>
* 存储文件元信息(真实文件名、存储路径、大小、类型),物理文件存储在
* application.yml 的 file.upload-path 目录下。
*/
@NoArgsConstructor
@Data
@Entity
@Table(name = "up_file")
@EqualsAndHashCode(onlyExplicitlyIncluded = true, callSuper = false)
public class UpFile extends BaseEntity {
/** 应用类型(如 "homework"),用于区分不同业务的附件 */
@Column(name = "topic_type", length = 50)
private String topicType;
/** 关联主题ID(如作业 serid) */
@Column(name = "topic_id")
private Integer topicId;
/** 文件真实名称(用户上传时的原始文件名) */
@Column(name = "file_true_name", length = 255)
private String fileTrueName;
/** 文件存储路径(相对路径,如 homework/20260823/uuid.pdf) */
@Column(name = "file_name", length = 500)
private String fileName;
/** 文件大小(字节) */
@Column(name = "file_size")
private Integer fileSize;
/** 文件后缀名(如 pdf, docx, jpg) */
@Column(name = "file_type", length = 20)
private String fileType;
}
@@ -0,0 +1,34 @@
package com.yunhai.homework.enums;
import lombok.Getter;
/**
* 作业状态。无草稿态:发作业一律置为「已发布」,
* 定时发送通过 timingSendTime + 查询侧「已到点」过滤控制可见性。
* 对应原 .NET Notify_Message.status(原系统已发布=2,此处归一化为 1)。
*/
@Getter
public enum HomeworkStatusEnum {
PUBLISHED(1, "已发布");
private final int code;
private final String desc;
HomeworkStatusEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static HomeworkStatusEnum of(Integer code) {
if (code == null) {
return PUBLISHED;
}
for (HomeworkStatusEnum e : values()) {
if (e.code == code) {
return e;
}
}
return PUBLISHED;
}
}
@@ -0,0 +1,33 @@
package com.yunhai.homework.enums;
import lombok.Getter;
/**
* 作业类型。对应原 .NET Notify_Message.type(原系统作业类=2,此处归一化)。
*/
@Getter
public enum HomeworkTypeEnum {
NORMAL(1, "普通作业"),
IMPORTANT(2, "重要作业");
private final int code;
private final String desc;
HomeworkTypeEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}
public static HomeworkTypeEnum of(Integer code) {
if (code == null) {
return NORMAL;
}
for (HomeworkTypeEnum e : values()) {
if (e.code == code) {
return e;
}
}
return NORMAL;
}
}
@@ -0,0 +1,48 @@
package com.yunhai.homework.enums;
import lombok.Getter;
/**
* 作业流程状态(PRD 五态,存 homework.hw_status)。
* <p>
* 与 notice 原 status(Integer,发布/撤回语义)并存:本枚举承载课堂作业业务流转,
* 流转收敛在 HomeworkStateService,条件更新保证幂等。
*/
@Getter
public enum HwStatusEnum {
/** 草稿 - 未发布 */
DRAFT_UNPUBLISHED("DRAFT_UNPUBLISHED", "草稿-未发布"),
/** 草稿 - 定时待发布 */
DRAFT_SCHEDULED("DRAFT_SCHEDULED", "草稿-定时待发布"),
/** 已发布 - 进行中 */
PUBLISHED("PUBLISHED", "已发布-进行中"),
/** 已截止 */
CLOSED("CLOSED", "已截止"),
/** 已归档(只读,不再触发任何提醒) */
ARCHIVED("ARCHIVED", "已归档");
private final String code;
private final String desc;
HwStatusEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public static HwStatusEnum of(String code) {
if (code == null) {
return DRAFT_UNPUBLISHED;
}
for (HwStatusEnum e : values()) {
if (e.code.equals(code)) {
return e;
}
}
return DRAFT_UNPUBLISHED;
}
public boolean isDraft() {
return this == DRAFT_UNPUBLISHED || this == DRAFT_SCHEDULED;
}
}
@@ -0,0 +1,52 @@
package com.yunhai.homework.enums;
import lombok.Getter;
/**
* 站内通知类型(PRD 六类,存 notification.type)。
*/
@Getter
public enum NotificationTypeEnum {
/** 作业发布(对应班级学生) */
HOMEWORK_PUBLISHED("HOMEWORK_PUBLISHED", "作业发布"),
/** 截止提醒(截止前 1 天 / 1 小时) */
DEADLINE_REMINDER("DEADLINE_REMINDER", "截止提醒"),
/** 未交提醒(教师手动) */
NOT_SUBMITTED("NOT_SUBMITTED", "未交提醒"),
/** 未读提醒(教师手动) */
NOT_READ("NOT_READ", "未读提醒"),
/** 批阅完成 */
REVIEW_DONE("REVIEW_DONE", "批阅完成"),
/** 打回通知 */
RETURNED("RETURNED", "打回通知"),
/**
* 定时发布失败回退(PRD §17.1 教师泳道要求通知教师,但 PRD 枚举表未列——
* 按 classwork-homework-plan-v3 §13-2 建议补充,待产品确认命名)。
*/
SCHEDULE_FAILED("SCHEDULE_FAILED", "定时发布失败"),
/**
* 截止时间变更(PRD §17.1 延长截止要求通知学生,PRD 枚举表未列——同上补充)。
*/
DEADLINE_CHANGED("DEADLINE_CHANGED", "截止时间变更");
private final String code;
private final String desc;
NotificationTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public static NotificationTypeEnum of(String code) {
if (code == null) {
return null;
}
for (NotificationTypeEnum e : values()) {
if (e.code.equals(code)) {
return e;
}
}
return null;
}
}
@@ -0,0 +1,39 @@
package com.yunhai.homework.enums;
import lombok.Getter;
/**
* 批阅状态(PRD 三态,存 homework_submission.review_status)。
* 与 SubmitStatusEnum 同源联动:批阅保存 → status=REVIEWED + review_status=REVIEWED;
* 打回 → status=RETURNED + review_status=RETURNED。
*/
@Getter
public enum ReviewStatusEnum {
/** 待批阅 */
PENDING("PENDING", "待批阅"),
/** 已批阅 */
REVIEWED("REVIEWED", "已批阅"),
/** 已打回 */
RETURNED("RETURNED", "已打回");
private final String code;
private final String desc;
ReviewStatusEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public static ReviewStatusEnum of(String code) {
if (code == null) {
return PENDING;
}
for (ReviewStatusEnum e : values()) {
if (e.code.equals(code)) {
return e;
}
}
return PENDING;
}
}
@@ -0,0 +1,50 @@
package com.yunhai.homework.enums;
import lombok.Getter;
/**
* 学生提交状态(PRD 五态,存 homework_submission.status)。
* <p>
* NOT_SUBMITTED 空行仅由「撤回」产生;未提交学生本身不落行(统计时由名册左连补位)。
*/
@Getter
public enum SubmitStatusEnum {
/** 未提交(撤回后产生的空提交行) */
NOT_SUBMITTED("NOT_SUBMITTED", "未提交"),
/** 已提交(含打回后重新提交) */
SUBMITTED("SUBMITTED", "已提交"),
/** 逾期提交(截止后提交,is_late=1) */
LATE_SUBMITTED("LATE_SUBMITTED", "逾期提交"),
/** 已打回(待重新提交) */
RETURNED("RETURNED", "已打回"),
/** 已批阅 */
REVIEWED("REVIEWED", "已批阅");
private final String code;
private final String desc;
SubmitStatusEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
public static SubmitStatusEnum of(String code) {
if (code == null) {
return NOT_SUBMITTED;
}
for (SubmitStatusEnum e : values()) {
if (e.code.equals(code)) {
return e;
}
}
return NOT_SUBMITTED;
}
/**
* 是否计入「已交」口径:打回未重交也算已交(PRD 学生矩阵口径)。
*/
public boolean countsAsSubmitted() {
return this != NOT_SUBMITTED;
}
}
@@ -0,0 +1,161 @@
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;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
/**
* 课堂作业定时任务(PRD §17.1 调度泳道)。
* <p>
* 全部基于状态机 CAS 条件更新,天然幂等,可安全重复执行;
* ARCHIVED 作业被显式跳过(归档后不再触发任何提醒)。
*/
@Component
public class HomeworkScheduledJobs {
private static final Logger log = LoggerFactory.getLogger(HomeworkScheduledJobs.class);
@Autowired
private HomeworkRepository homeworkRepository;
@Autowired
private HomeworkClassRepository homeworkClassRepository;
@Autowired
private HomeworkStateService stateService;
@Autowired
private NotificationService notificationService;
@Autowired
private TeacherHomeworkV2Service teacherHomeworkV2Service;
@Autowired
private ClassStudentService classStudentService;
/**
* 定时发布:每分钟扫描到点的 DRAFT_SCHEDULED 作业 → PUBLISHED + 作业发布通知;
* 失败回退 DRAFT_UNPUBLISHED 并站内通知教师。
*/
@Scheduled(cron = "0 * * * * ?")
public void scheduledPublish() {
List<Homework> due = homeworkRepository
.findByHwStatusAndTimingSendTimeLessThanEqual(HwStatusEnum.DRAFT_SCHEDULED.getCode(),
LocalDateTime.now());
if (due.isEmpty()) {
return;
}
log.info("[scheduledPublish] 到点作业 {} 个", due.size());
for (Homework homework : due) {
Integer id = homework.getId();
try {
teacherHomeworkV2Service.publishInternal(id, HwStatusEnum.DRAFT_SCHEDULED);
log.info("[scheduledPublish] 发布成功 homeworkId={}", id);
} catch (Exception e) {
log.error("[scheduledPublish] 发布失败,回退草稿 homeworkId={}", id, e);
try {
boolean rolledBack = stateService.rollbackFromSchedule(id);
if (rolledBack) {
notificationService.send(NotificationTypeEnum.SCHEDULE_FAILED, id, null,
homework.getPublisherId(), homework.getPublisherName(),
NotificationService.ROLE_TEACHER,
"定时发布失败:《" + homework.getTitle() + "》",
"定时发布任务执行异常,作业已回退为「草稿-未发布」,请重新发布。");
}
} catch (Exception ex) {
log.error("[scheduledPublish] 回退失败 homeworkId={}", id, ex);
}
}
}
}
/**
* 到点截止:每分钟扫描已过截止时间的 PUBLISHED 作业 → CLOSED(无对应通知类型,不推送)。
*/
@Scheduled(cron = "30 * * * * ?")
public void deadlineClose() {
List<Homework> overdue = homeworkRepository
.findByHwStatusAndDeadlineAtBefore(HwStatusEnum.PUBLISHED.getCode(), LocalDateTime.now());
for (Homework homework : overdue) {
if (stateService.cas(homework.getId(), HwStatusEnum.PUBLISHED, HwStatusEnum.CLOSED)) {
log.info("[deadlineClose] 作业已截止 homeworkId={}", homework.getId());
}
}
}
/**
* 截止提醒:每分钟扫描截止时间在 24h 内的 PUBLISHED 作业,
* 命中「1 天档 / 1 小时档」窗口且开关开启时向未交学生推送 DEADLINE_REMINDER;
* 档位去重(related_id:1=1 天档 2=1 小时档),每作业每档至多发一轮。
*/
@Scheduled(cron = "40 * * * * ?")
public void deadlineReminder() {
LocalDateTime now = LocalDateTime.now();
List<Homework> window = homeworkRepository
.findByHwStatusAndDeadlineAtBetween(HwStatusEnum.PUBLISHED.getCode(), now, now.plusHours(24));
for (Homework homework : window) {
if (homework.getDeadlineAt() == null) {
continue;
}
// 1 小时档:截止前 [55, 60) 分钟窗口
if (homework.getRemind1h() != null && homework.getRemind1h() == 1
&& inWindow(homework.getDeadlineAt(), now, 55, 60)
&& !notificationService.alreadySentSlot(homework.getId(), NotificationTypeEnum.DEADLINE_REMINDER, 2)) {
sendReminder(homework, 2, "1 小时");
}
// 1 天档:截止前 [23, 24) 小时窗口
if (homework.getRemind1d() != null && homework.getRemind1d() == 1
&& inWindow(homework.getDeadlineAt(), now, 23 * 60, 24 * 60)
&& !notificationService.alreadySentSlot(homework.getId(), NotificationTypeEnum.DEADLINE_REMINDER, 1)) {
sendReminder(homework, 1, "1 天");
}
}
}
private boolean inWindow(LocalDateTime deadline, LocalDateTime now, int fromMinutes, int toMinutes) {
long minutes = java.time.Duration.between(now, deadline).toMinutes();
return minutes >= fromMinutes && minutes < toMinutes;
}
private void sendReminder(Homework homework, int slot, String slotDesc) {
// 通知接收人直接使用学生ID(名册口径:student_id = 平台用户ID)
List<Integer> targets = teacherHomeworkV2Service.notSubmittedStudentIds(homework.getId()).stream()
.distinct()
.collect(Collectors.toList());
if (targets.isEmpty()) {
return;
}
int sent = notificationService.sendBatch(NotificationTypeEnum.DEADLINE_REMINDER, homework.getId(), slot,
NotificationService.distinct(targets),
NotificationService.deadlineReminderTitle(homework),
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);
}
}
}
@@ -0,0 +1,14 @@
package com.yunhai.homework.model;
import lombok.Data;
/**
* 统一分页查询入参。pageIndex 从 1 起。
*/
@Data
public class BasePageQuery {
private int pageIndex = 1;
private int pageSize = 10;
/** 通用模糊搜索关键词 */
private String keyword;
}
@@ -0,0 +1,33 @@
package com.yunhai.homework.model;
import lombok.Data;
import java.util.List;
/**
* 统一分页响应外壳。
*/
@Data
public class BasePageResult<T> {
/** 总记录数 */
private int total;
/** 当前页数据 */
private List<T> content;
/** 当前页码(从 1 起) */
private int pageIndex;
/** 每页条数 */
private int pageSize;
/** 总页数 */
private int totalPages;
/** 是否还有下一页 */
private boolean hasNext;
public BasePageResult(List<T> content, int total, int pageIndex, int pageSize, int totalPages, boolean hasNext) {
this.content = content;
this.total = total;
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.totalPages = totalPages;
this.hasNext = hasNext;
}
}
@@ -0,0 +1,106 @@
package com.yunhai.homework.model;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* 班级管理接口-教师(bschclass {@code /oapi/DbClass/GetSchteaList} 响应项)。
* 对应 C# BestSchool.Entity.Model.SchTeaList。
*/
public class SchTeaList {
/** 学校 GUID(SchSerID) */
@JsonProperty("SchSerID")
private String schSerID;
@JsonProperty("TeaTel")
private String teaTel;
/** 教师姓名 */
@JsonProperty("TeaName")
private String teaName;
/** 教师用户ID */
@JsonProperty("UserTeaSerID")
private Integer userTeaSerID;
@JsonProperty("TeaIDCard")
private String teaIDCard;
/** 教师工号 */
@JsonProperty("TeaNo")
private String teaNo;
/** 教师所属组ID列表(兼容旧字段 GroupIds) */
@JsonProperty("GroupIds")
private List<Integer> groupIds;
/** 教师所属组ID(接口现行字段 GroupID,单个) */
@JsonProperty("GroupID")
private Integer groupId;
public Integer getGroupId() {
return groupId;
}
public void setGroupId(Integer groupId) {
this.groupId = groupId;
}
public String getSchSerID() {
return schSerID;
}
public void setSchSerID(String schSerID) {
this.schSerID = schSerID;
}
public String getTeaTel() {
return teaTel;
}
public void setTeaTel(String teaTel) {
this.teaTel = teaTel;
}
public String getTeaName() {
return teaName;
}
public void setTeaName(String teaName) {
this.teaName = teaName;
}
public Integer getUserTeaSerID() {
return userTeaSerID;
}
public void setUserTeaSerID(Integer userTeaSerID) {
this.userTeaSerID = userTeaSerID;
}
public String getTeaIDCard() {
return teaIDCard;
}
public void setTeaIDCard(String teaIDCard) {
this.teaIDCard = teaIDCard;
}
public String getTeaNo() {
return teaNo;
}
public void setTeaNo(String teaNo) {
this.teaNo = teaNo;
}
public List<Integer> getGroupIds() {
return groupIds;
}
public void setGroupIds(List<Integer> groupIds) {
this.groupIds = groupIds;
}
}
@@ -0,0 +1,29 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* bschclass 班级 DTO(/oapi/VSchClass/GetVClassList,SchClass 视图)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchClassDto {
/** 班级业务ID(sch_class.class_id,scopeType=班级 的 scopeId;学生 ClassID 指向它) */
@JsonProperty("ClassID")
private Integer classId;
/** 班级名称 */
@JsonProperty("ClassName")
private String className;
/** 年级ID(= gradeId) */
@JsonProperty("Levell")
private Integer gradeId;
/** 排序号 */
@JsonProperty("sx")
private Integer sort;
}
@@ -0,0 +1,25 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* bschclass 年级 DTO(/oapi/Schoolen/GetSchLenLevellist,BaseSchsystemGrade)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchGradeDto {
/** 年级ID(=gradeId,scopeType=年级 的 scopeId;班级 Levell 指向它) */
@JsonProperty("Id")
private Integer id;
/** 年级名称(gradeNickname,如「初一」) */
@JsonProperty("Name")
private String name;
/** 所属学段(学制)ID */
@JsonProperty("OwnerId")
private Integer ownerId;
}
@@ -0,0 +1,25 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* bschclass 学校信息 DTO(/oapi/SchInfo/GetUserSchFirst)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchInfoDto {
/** 学校 GUID(bschclass 侧 SchSerId) */
@JsonProperty("SchSerId")
private String schSerId;
/** 用户中心学校组ID(与 CampusNotice schId 同域) */
@JsonProperty("UserSchSerId")
private Integer userSchSerId;
/** 学校名称 */
@JsonProperty("SchName")
private String schName;
}
@@ -0,0 +1,35 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* bschclass 学生 DTO(/oapi/OAStu/*,SchStudent 视图)。
* account 仅为上游接口契约字段(兼容反序列化),本系统落库/身份映射不再使用;
* 学生身份以 stuId(UserStuSerID,= 平台用户ID)+ stuName 承载。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchStudentDto {
/** 学员登录账号(上游契约字段,仅兼容保留,不落库) */
@JsonProperty("Account")
private String account;
/** 学生ID */
@JsonProperty("UserStuSerID")
private Integer stuId;
/** 学生姓名 */
@JsonProperty("StuName")
private String stuName;
/** 所在班级ID(sch_class.class_id) */
@JsonProperty("ClassID")
private Integer classId;
/** 所在组ID */
@JsonProperty("GroupID")
private Integer groupId;
}
@@ -0,0 +1,29 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* bschclass 学校学科 DTO(/oapi/SubInfo/GetSchSublist)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchSubjectDto {
/** 学校 GUID(bschclass 侧 SchSerId) */
@JsonProperty("SchSerID")
private String schSerId;
/** 学科ID(学科编码;字典无编码时为字典ID字符串) */
@JsonProperty("SubID")
private String subId;
/** 学科名称 */
@JsonProperty("SubName")
private String subName;
/** 排序号 */
@JsonProperty("No")
private Integer no;
}
@@ -0,0 +1,25 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* bschclass 学段(学制)DTO(/oapi/Schoolen/GetSchLenlist,BaseSchsystem)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchSystemDto {
/** 学制ID(scopeType=学部 的 scopeId) */
@JsonProperty("Id")
private Integer id;
/** 学段名称(如「小学部」「初中部」) */
@JsonProperty("Name")
private String name;
/** 学制类型 */
@JsonProperty("Type")
private Integer type;
}
@@ -0,0 +1,44 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.Date;
/**
* bschclass 任教关系 DTO(/oapi/TeachInfo/*,sch_class_teach 视图)。
* 一条记录 = 某学校某班级中某教师任教某学科。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchTeachDto {
/** 任教记录ID(sch_class_teach.id) */
@JsonProperty("SerID")
private Integer serId;
/** 学校 GUID(bschclass 侧 SchSerId) */
@JsonProperty("SchSerID")
private String schSerId;
/** 教师ID(= 平台用户ID) */
@JsonProperty("UserTeaSerID")
private Integer teaId;
/** 学科ID(学科编码) */
@JsonProperty("SubID")
private String subId;
/** 班级业务ID(sch_class.class_id) */
@JsonProperty("ClassID")
private Integer classId;
/** 班级ID(与 ClassID 同值,上游兼容字段) */
@JsonProperty("UserClassID")
private Integer userClassId;
/** 创建时间 */
@JsonProperty("CreationTime")
private Date creationTime;
}
@@ -0,0 +1,41 @@
package com.yunhai.homework.model.bsch;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* bschclass 组织树节点 DTO(/oapi/SchGroup/GetTeaTree、GetStuTree,TeaTreeVO 视图)。
* type=1 组节点(学校/学段/年级/班级、教师组),type=2 人员叶子(学生/教师)。
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class SchTreeNodeDto {
/** 节点ID(学校/学段/年级/班级为对应业务ID,叶子为学生 stuId 或教师 teaId) */
private Integer id;
/** 节点名称 */
private String name;
/** 父节点ID(根节点为 0) */
private Integer pid;
/** 排序号(组取 sort,叶子固定 0) */
private Integer no;
/** 1=组节点,2=人员叶子 */
private Integer type;
/** 是否父节点(组 true,叶子 false) */
@JsonProperty("isParent")
private Boolean isParent;
/** 头像(当前固定空串) */
private String photo;
/** 子节点(叶子为 null,组节点非 null) */
private List<SchTreeNodeDto> children;
}
@@ -0,0 +1,81 @@
package com.yunhai.homework.model.homework;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 作业详情视图对象(替代原 Map&lt;String,Object&gt; 返回)。
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
*/
@Data
public class HomeworkDetailVo {
@JsonProperty("serid")
private Integer serid;
@JsonProperty("title")
private String title;
@JsonProperty("Contents")
private String contents;
@JsonProperty("DeptID")
private String deptId;
@JsonProperty("DeptName")
private String deptName;
@JsonProperty("TeaName")
private String publisherName;
@JsonProperty("EditDate")
private String editDate;
@JsonProperty("numcount")
private Integer numCount;
@JsonProperty("readnumcount")
private Long readNumCount;
@JsonProperty("ReceiveList")
private String receiveList;
@JsonProperty("DsfsTime")
private String dsfsTime;
@JsonProperty("isDsfs")
private Boolean isDsfs;
@JsonProperty("ChaoSend")
private List<FileItem> chaoSend;
/** 接收人列表:"all" 或用户ID列表 */
@JsonProperty("tlist")
private Object tlist;
/**
* 接收对象可读文本("全部班级" 或班级名顿号串)。
* 详情页「接收班级」标签直接展示,避免前端拿 ReceiveList 的 id 串渲染。
*/
@JsonProperty("receiveText")
private String receiveText;
/**
* 附件项(旧 ChaoSend 数组元素)。
*/
@Data
public static class FileItem {
@JsonProperty("id")
private Integer id;
@JsonProperty("name")
private String name;
@JsonProperty("size")
private Integer size;
@JsonProperty("fileid")
private Integer fileid;
@JsonProperty("filename")
private String filename;
}
}
@@ -0,0 +1,26 @@
package com.yunhai.homework.model.homework;
import com.yunhai.homework.model.BasePageResult;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 作业分页结果(扩展 BasePageResult,额外携带 unreadCount)。
* <p>
* 对齐方案 M7:unreadCount 并入分页外壳扩展子类,不改基类契约。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class HomeworkPageResult<T> extends BasePageResult<T> {
/** 当前用户未读作业总数 */
private long unreadCount;
public HomeworkPageResult(List<T> content, int total, int pageIndex, int pageSize,
int totalPages, boolean hasNext, long unreadCount) {
super(content, total, pageIndex, pageSize, totalPages, hasNext);
this.unreadCount = unreadCount;
}
}
@@ -0,0 +1,48 @@
package com.yunhai.homework.model.homework;
import com.yunhai.homework.model.BasePageQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
/**
* 作业列表查询条件。
* <p>
* - 管理端(adminList):用 status / type / deptId / publisherId / 时间范围过滤;
* - 用户端(userList):额外用 readType 过滤未读/已读(0 全部 / 1 未读 / 2 已读)。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class HomeworkQuery extends BasePageQuery {
/** 状态过滤:null 不过滤;1 已发布 */
private Integer status;
/** 类型过滤:1 普通 / 2 重要 */
private Integer type;
/** 列表范围(管理端):"user" 只看自己发布的,null 看全部 */
private String scope;
/** 发布部门ID */
private String deptId;
/** 发布人ID(用户中心数字ID,管理端按发布人查) */
private Integer publisherId;
/** 开始时间(创建时间 >=,前端传 yyyy-MM-dd 短日期) */
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate startTime;
/** 结束时间(创建时间 <= 当天 23:59:59,前端传 yyyy-MM-dd 短日期) */
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endTime;
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
private Integer readType = 0;
/** 排序:1 时间倒序(默认)/ 2 时间正序 */
private String orderByTime = "1";
}
@@ -0,0 +1,65 @@
package com.yunhai.homework.model.homework;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import com.yunhai.homework.common.validation.ValidationGroups;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 作业发布/编辑入参。发布人由当前登录用户上下文填充,不在此入参中传递。
*/
@Data
public class HomeworkRequest {
/** 作业标题 */
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "作业标题不得为空")
private String title;
/** 作业内容 */
private String content;
/** 作业类型:1 普通 / 2 重要 */
@NotNull(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "作业类型不得为空")
private Integer type = 1;
/** 发布部门ID(原 DeptID,必填——对应原系统「发布部门不得为空」校验) */
@NotBlank(groups = {ValidationGroups.Create.class, ValidationGroups.Update.class}, message = "发布部门不得为空")
private String deptId;
/** 发布部门名称 */
private String deptName;
/** 是否置顶:0 否 / 1 是 */
private Integer topmost = 0;
/** 接收人列表(JSON 数组字符串,如 ["10012","10013"]——用户中心数字ID,或 "ALL" 表示全员) */
private String receiveList;
/** 附件文件ID列表(逗号分隔串,如 "1,2,3");旧名 chaoSend,已更名 fileIds(M6) */
private String fileIds;
/** 定时发送时间(为空表示立即发布) */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime timingSendTime;
/** 短信发送标记:0 不发短信 / 1 发短信(旧名 topmost 实指发短信,已正名 smsFlag,M6) */
private Integer smsFlag = 0;
/** 转发源作业ID(非空时表示转发创建,对应旧 REST 壳 forward/{sourceId}) */
private Integer sourceId;
// ==================== 课堂作业 V1/V2 扩展(前端契约字段,兼容层同步写新列) ====================
/** 学科(字典值) */
private String subject;
/** 截止时间 */
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime deadline;
/** 提交方式:text/image/attach/none 组合 */
private java.util.List<String> submitType;
}
@@ -0,0 +1,113 @@
package com.yunhai.homework.model.homework;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 作业统计视图(对应原系统 GetStatisData / 按发布人统计)。
*/
@Data
public class HomeworkStatisticsVo {
/** 作业总数(全部已发布) */
private long total;
/** 已发布数 */
private long publishedCount;
/** 阅读记录总数(累计已读人次) */
private long totalReadRecords;
/** 当前用户未读数(已到点的已发布 - 当前用户已读) */
private long unreadForCurrentUser;
/** 按发布人统计 */
private List<PublisherStat> byPublisher;
/** 按月统计(已发布作业数) */
private List<MonthStat> byMonth;
// ==================== 课程作业口径(前端 /stats 页消费) ====================
/** 我布置的作业数(当前登录人发布的全部作业,含草稿) */
private long myAssignCount;
/** 我的作业待批改总量(已交未批的当前版本行合计) */
private long pendingGrade;
/** 各作业平均提交率(%,仅统计需收集提交的作业;无样本时为 0) */
private double avgSubmitRate;
/** 按作业的提交与批改进度明细(草稿与仅查阅型不计) */
private List<HomeworkStat> byHomework = new ArrayList<>();
/** 已批阅提交的分数分布(四档) */
private List<ScoreBucket> scoreDist = new ArrayList<>();
/** 单作业进度条目(口径对齐前端 HomeworkProgress 契约) */
@Data
public static class HomeworkStat {
private Integer id;
private String title;
private String subject;
/** 旧契约四态派生值:draft / collecting / closed / done */
private String hwStatus;
private String hwStatusDesc;
/** 应交人数 */
private long should;
/** 已读人数 */
private long read;
/** 已交人数 */
private long submitted;
/** 已批阅人数 */
private long graded;
/** 待批改人数 */
private long pendingGrade;
/** 未交人数 */
private long unsubmitted;
/** 提交率(%) */
private double submitRate;
/** 批阅率(%) */
private double gradeRate;
}
/** 分数分布档(key 供前端配色映射:excellent/good/pass/fail) */
@Data
public static class ScoreBucket {
private String label;
private String key;
private long count;
public ScoreBucket(String label, String key, long count) {
this.label = label;
this.key = key;
this.count = count;
}
}
@Data
public static class PublisherStat {
private String publisherName;
private long count;
public PublisherStat(String publisherName, long count) {
this.publisherName = publisherName;
this.count = count;
}
}
@Data
public static class MonthStat {
private int year;
private int month;
private long count;
public MonthStat(int year, int month, long count) {
this.year = year;
this.month = month;
this.count = count;
}
}
}
@@ -0,0 +1,34 @@
package com.yunhai.homework.model.homework;
import lombok.Data;
import java.util.List;
/**
* 用户端作业列表结果:在分页数据之外额外携带「当前用户未读总数」,
* 供前端展示未读角标(对应原系统 OtherData.unReadCount)。
*/
@Data
public class HomeworkUserListResult {
private int total;
private List<HomeworkVo> content;
private int pageIndex;
private int pageSize;
private int totalPages;
private boolean hasNext;
/** 当前用户未读作业总数 */
private long unreadCount;
public HomeworkUserListResult(List<HomeworkVo> content, int total, int pageIndex, int pageSize,
int totalPages, boolean hasNext, long unreadCount) {
this.content = content;
this.total = total;
this.pageIndex = pageIndex;
this.pageSize = pageSize;
this.totalPages = totalPages;
this.hasNext = hasNext;
this.unreadCount = unreadCount;
}
}
@@ -0,0 +1,92 @@
package com.yunhai.homework.model.homework;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 作业视图对象(列表/详情返回)。
*/
@Data
public class HomeworkVo {
private Integer id;
private String title;
private String content;
private Integer type;
private String typeDesc;
/** 发布人ID(用户中心数字ID) */
private Integer publisherId;
private String publisherName;
private String deptId;
private String deptName;
private Integer status;
private String statusDesc;
private Integer topmost;
/** 已读人数(基于阅读记录实时统计) */
private Long readCount = 0L;
/** 应读人数 */
private Integer allowCount;
/** 当前用户是否已读(用户端列表使用;管理端恒为 false 或不关注) */
private boolean read = false;
/** 是否撤回:0 否 / 1 是 */
private Integer isPulled;
/** 定时发送时间 */
private LocalDateTime timingSendTime;
/** 父作业ID(转发时关联原作业) */
private Integer parentId;
/** 短信发送标记 */
private Integer smsFlag;
/** 接收人列表(JSON) */
private String receiveList;
/** 附件ID串("1,2,3",列表页下载入口使用;对齐旧 .NET UpFile 字段) */
private String UpFile;
// ==================== 课堂作业 V1/V2 扩展(前端 types/homework.ts 契约) ====================
/** 学科(字典值) */
private String subject;
/** 截止时间(前端契约字段名 deadline) */
private LocalDateTime deadline;
/** 提交方式数组:text/image/attach/none */
private java.util.List<String> submitType;
/** 作业流程状态——旧契约四态派生值(draft/collecting/closed/done),权威五态见 homework.hw_status */
private String hwStatus;
private String hwStatusDesc;
/**
* 提交/批改进度(管理端列表页「提交进度 / 未读未交 / 批改」列使用)。
* 草稿或仅查阅型为 null;列表场景由 HomeworkProgressCalculator 批量聚合回填。
*/
private com.yunhai.homework.model.v2.ProgressVo progress;
private String createBy;
private LocalDateTime createTime;
private String updateBy;
private LocalDateTime updateTime;
/**
* 阅读明细视图(阅读列表查询用)。
*/
@Data
public static class ReadDetail {
/** 读者ID(用户中心数字ID) */
private Integer id;
private String name;
private boolean read;
private LocalDateTime readTime;
}
}
@@ -0,0 +1,20 @@
package com.yunhai.homework.model.homework;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 一键已读结果视图对象(替代原 Map&lt;String,Object&gt; 返回)。
*/
@Data
public class InitializeRedVo {
@JsonProperty("count")
private Integer count;
public InitializeRedVo() {}
public InitializeRedVo(Integer count) {
this.count = count;
}
}
@@ -0,0 +1,30 @@
package com.yunhai.homework.model.homework;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 文件上传结果视图对象(替代原 Map&lt;String,Object&gt; 返回)。
* 使用 @JsonProperty 保持旧字段名兼容前端渲染。
*/
@Data
public class UpFileResultVo {
@JsonProperty("fileid")
private Integer fileid;
@JsonProperty("name")
private String name;
@JsonProperty("fileurl")
private String fileurl;
@JsonProperty("host")
private String host;
@JsonProperty("type")
private String type;
@JsonProperty("size")
private Integer size;
}
@@ -0,0 +1,31 @@
package com.yunhai.homework.model.homework;
import lombok.Data;
/**
* 附件视图对象(对应原 .NET FileObj)。
*/
@Data
public class UpFileVo {
/** 文件记录ID */
private Integer id;
/** 文件真实名称 */
private String fileTrueName;
/** 文件存储路径(相对路径) */
private String fileName;
/** 文件大小(字节) */
private Integer fileSize;
/** 文件后缀名 */
private String fileType;
/** 关联主题ID */
private Integer topicId;
/** 应用类型 */
private String topicType;
}
@@ -0,0 +1,122 @@
package com.yunhai.homework.model.homework;
import java.util.ArrayList;
import java.util.List;
/**
* 用户可访问模块(左菜单)视图对象。
* <p>将基础平台返回的扁平 {@code List<BoaModule>} 转换为前端 layout(参考 UserBase)期望的树结构:
* {@code modId}(路由高亮匹配)/{@code page}(跳转路径)/{@code children}(子菜单)。</p>
*/
public class UserModuleVo {
/** 模块主键 id(基础平台 module id) */
private Integer id;
/** 菜单高亮匹配用:等价于后端模块 id */
private Integer modId;
/** 模块名称 */
private String name;
/** 父级模块 id(0 表示根节点) */
private Integer parentId;
/** 前端路由路径(由 BoaModule.path 映射而来) */
private String page;
/** 层级(0=根分组) */
private Integer level;
/** 菜单图标 */
private String icon;
/** 排序号 */
private Integer orderNumber;
/** 是否可用 */
private Boolean usable;
/** 子菜单 */
private List<UserModuleVo> children = new ArrayList<>();
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getModId() {
return modId;
}
public void setModId(Integer modId) {
this.modId = modId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getParentId() {
return parentId;
}
public void setParentId(Integer parentId) {
this.parentId = parentId;
}
public String getPage() {
return page;
}
public void setPage(String page) {
this.page = page;
}
public Integer getLevel() {
return level;
}
public void setLevel(Integer level) {
this.level = level;
}
public String getIcon() {
return icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public Integer getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(Integer orderNumber) {
this.orderNumber = orderNumber;
}
public Boolean getUsable() {
return usable;
}
public void setUsable(Boolean usable) {
this.usable = usable;
}
public List<UserModuleVo> getChildren() {
return children;
}
public void setChildren(List<UserModuleVo> children) {
this.children = children;
}
}
@@ -0,0 +1,25 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
/**
* 附件条目(作业附件与提交附件共用)。fileUrl 必传;fileId 为 file/Upload 产物时回传。
*/
@Data
public class AttachmentItem {
/** up_file.id */
private Integer fileId;
/** 文件访问 URL */
private String fileUrl;
/** 文件类型:image / file */
private String fileType;
/** 文件名 */
private String fileName;
/** 排序号 */
private Integer sort = 0;
}
@@ -0,0 +1,18 @@
package com.yunhai.homework.model.v2;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 班级条目(作业参与班级展示)。
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ClassItem {
private String classId;
private String className;
}
@@ -0,0 +1,28 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
/**
* 年级→班级树(发布作业选班 + 应交名单基数来源)。
* 叶子节点 id = 班级 id(class_student 快照聚合)。
*/
@Data
public class ClassTreeNode {
/** 年级节点为 gradeName,班级叶子为 classId */
private String id;
/** 展示名 */
private String name;
/** 是否父节点(年级) */
private Boolean isParent;
/** 叶子节点:在班学生数 */
private Integer studentCount;
private List<ClassTreeNode> children = new ArrayList<>();
}
@@ -0,0 +1,16 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 延长截止请求(新截止时间必须晚于当前时间)。
*/
@Data
public class ExtendRequest {
@NotNull(message = "新截止时间不得为空")
private LocalDateTime deadlineAt;
}
@@ -0,0 +1,36 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.util.List;
/**
* 作业详情汇总统计卡片(PRD §18 页 4 stats 接口出参)。
*/
@Data
public class HomeworkStatsVo {
private Integer homeworkId;
private String title;
private String hwStatus;
private String hwStatusDesc;
/** 总进度 */
private ProgressVo total;
/** 分班进度 */
private List<ByClass> byClass;
@Data
public static class ByClass {
private String classId;
private String className;
private ProgressVo progress;
}
}
@@ -0,0 +1,48 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.time.LocalDateTime;
import java.util.List;
/**
* 创建/编辑作业请求(PRD §18 页 1 表单)。
* 创建一律落草稿(DRAFT_UNPUBLISHED),发布/定时走独立接口。
*/
@Data
public class HomeworkV2Request {
/** 作业标题(必填 ≤50 字) */
@NotBlank(message = "作业标题不得为空")
@Size(max = 50, message = "作业标题不超过50字")
private String title;
/** 学科(字典值) */
private String subject;
/** 作业说明(富文本) */
private String description;
/** 参与班级 id 列表 */
private List<String> classIds;
/** 附件列表 */
private List<AttachmentItem> attachments;
/** 截止时间(精确到分钟;空 = 仅查阅型) */
private LocalDateTime deadlineAt;
/** 提交方式:image / attach / text / none 的组合(PRD 图片/文件/两者) */
private List<String> submitType;
/** 截止后是否允许逾期提交:0 否 / 1 是 */
private Integer allowLate;
/** 提醒设置:截止前 1 天提醒(0 关 / 1 开) */
private Integer remind1d;
/** 提醒设置:截止前 1 小时提醒(0 关 / 1 开) */
private Integer remind1h;
}
@@ -0,0 +1,73 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
/**
* 作业视图对象 V2(草稿箱/已发布列表/详情共用)。
* 列表场景 attachments/classes 可按需填充;progress 仅非草稿且非仅查阅型作业返回。
*/
@Data
public class HomeworkV2Vo {
private Integer id;
private String title;
/** 学科 */
private String subject;
/** 作业说明(富文本;列表场景可置空省流) */
private String description;
/** 作业状态(PRD 五态) */
private String hwStatus;
private String hwStatusDesc;
/** 定时发布时间(复用 timing_send_time) */
private LocalDateTime scheduledAt;
/** 发布时间 */
private LocalDateTime publishedAt;
/** 截止时间 */
private LocalDateTime deadlineAt;
/** 归档时间 */
private LocalDateTime archivedAt;
/** 提交方式数组 */
private List<String> submitType;
/** 截止后允许逾期提交:0/1 */
private Integer allowLate;
/** 提醒设置:截止前 1 天 */
private Integer remind1d;
/** 提醒设置:截止前 1 小时 */
private Integer remind1h;
/** 参与班级 */
private List<ClassItem> classes;
/** 附件列表(详情场景填充) */
private List<AttachmentItem> attachments;
/** 发布人ID(用户中心数字ID) */
private Integer publisherId;
/** 发布人姓名 */
private String publisherName;
/** 创建/更新时间(审计) */
private LocalDateTime createTime;
private LocalDateTime updateTime;
/** 汇总进度(草稿或仅查阅型为 null) */
private ProgressVo progress;
}
@@ -0,0 +1,22 @@
package com.yunhai.homework.model.v2;
import com.yunhai.homework.model.BasePageResult;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知分页结果(含当前用户未读数)。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NotificationPageResult extends BasePageResult<NotificationVo> {
/** 当前用户未读通知总数 */
private long unreadCount;
public NotificationPageResult(BasePageResult<NotificationVo> page, long unreadCount) {
super(page.getContent(), page.getTotal(), page.getPageIndex(), page.getPageSize(),
page.getTotalPages(), page.isHasNext());
this.unreadCount = unreadCount;
}
}
@@ -0,0 +1,19 @@
package com.yunhai.homework.model.v2;
import com.yunhai.homework.model.BasePageQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知列表查询。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NotificationQuery extends BasePageQuery {
/** 类型过滤(NotificationTypeEnum code) */
private String type;
/** 已读过滤:0 未读 / 1 已读(空 = 全部) */
private Integer isRead;
}
@@ -0,0 +1,34 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 通知列表项。
*/
@Data
public class NotificationVo {
private Integer id;
/** 通知类型 */
private String type;
private String typeDesc;
private String title;
private String content;
private Integer homeworkId;
private Integer relatedId;
/** 是否已读:0/1 */
private Integer isRead;
private LocalDateTime readAt;
private LocalDateTime createTime;
}
@@ -0,0 +1,44 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
/**
* 提交/批阅/阅读进度(PRD §18 页 3 汇总口径)。
* should(应交)为分母;打回未重交计入已交。
*/
@Data
public class ProgressVo {
/** 应交人数(参与班级在班学生去重) */
private int should;
/** 已读人数 */
private int read;
/** 已交人数(含逾期/打回/已批阅) */
private int submitted;
/** 已批阅人数 */
private int graded;
/** 待批阅人数(submitted - graded) */
private int pendingGrade;
/** 未交人数 */
private int unsubmitted;
/** 未读人数 */
private int unread;
/** 未读且未交人数(高亮口径:should - 已读∪已交) */
private int unreadUndone;
/** 阅读率(百分数,一位小数) */
private double readRate;
/** 提交率(百分数,一位小数) */
private double submitRate;
/** 批阅率(已批阅 / 已交,百分数一位小数;submitted=0 时为 0) */
private double gradeRate;
}
@@ -0,0 +1,20 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import java.util.List;
/**
* 教师手动提醒请求:type = NOT_READ / NOT_SUBMITTED,studentIds 为空时提醒全部未读/未交学生。
*/
@Data
public class RemindRequest {
/** 提醒类型:NOT_READ / NOT_SUBMITTED */
@NotBlank(message = "提醒类型不得为空")
private String type;
/** 定向学生ID(用户中心数字ID;空 = 全部未读/未交) */
private List<Integer> studentIds;
}
@@ -0,0 +1,15 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 打回请求(PRD 页 6)。comment 为订正原因,必填。
*/
@Data
public class ReturnRequest {
@NotBlank(message = "打回须填写订正原因")
private String comment;
}
@@ -0,0 +1,22 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
/**
* 批阅保存请求(PRD 页 6)。score 与 comment 至少一项有值。
*/
@Data
public class ReviewRequest {
/** 评分 */
private BigDecimal score;
/** 评语 */
private String comment;
/** 圈划标注图片(P2,先收参) */
private List<String> markedImages;
}
@@ -0,0 +1,25 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 定时发布 / 取消定时请求。
* cancel=true 时忽略 scheduledAt,作业回退草稿-未发布并清空定时时间。
*/
@Data
public class ScheduleRequest {
/** 定时发布时间(cancel=false 时必填且须晚于当前时间) */
private LocalDateTime scheduledAt;
/** 是否取消定时 */
private Boolean cancel = false;
@NotNull
public Boolean getCancel() {
return cancel;
}
}
@@ -0,0 +1,29 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 学生端作业详情(PRD §18 学生页 2)。进入详情后由前端停留 ≥3 秒再调 read 上报。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class StudentHomeworkDetailVo extends StudentHomeworkVo {
/** 作业说明(富文本) */
private String description;
/** 作业附件 */
private List<AttachmentItem> attachments;
/** 参与班级(学生所属班级视角) */
private List<ClassItem> classes;
/** 我当前版本的提交附件(未提交为 null) */
private List<AttachmentItem> myAttachments;
/** 我的文本内容(当前版本) */
private String myText;
}
@@ -0,0 +1,69 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 学生端作业列表项(PRD §18 学生页 1)。
*/
@Data
public class StudentHomeworkVo {
private Integer id;
private String title;
/** 学科 */
private String subject;
/** 任课教师(发布人姓名) */
private String teacherName;
/** 截止时间(前端做倒计时) */
private LocalDateTime deadlineAt;
/** 提交方式 */
private java.util.List<String> submitType;
/** 作业状态 */
private String hwStatus;
private String hwStatusDesc;
/** 截止后是否允许逾期补交:0 否 / 1 是(学生端据此决定是否渲染「补交」入口) */
private Integer allowLate;
/** 我的提交概要(未提交为 null) */
private MySubmission my;
@Data
public static class MySubmission {
/** 当前版本提交行ID */
private Integer submissionId;
/** 提交状态(PRD 五态) */
private String submitStatus;
private String submitStatusDesc;
/** 提交时间 */
private LocalDateTime submittedAt;
/** 是否逾期 */
private Integer isLate;
/** 版本号 */
private Integer version;
/** 批阅状态 */
private String reviewStatus;
/** 评分(已批阅才有) */
private java.math.BigDecimal score;
/** 评语 */
private String comment;
}
}
@@ -0,0 +1,25 @@
package com.yunhai.homework.model.v2;
import com.yunhai.homework.model.BasePageQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 学生矩阵查询(PRD §18 页 5)。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class StudentMatrixQuery extends BasePageQuery {
/** 班级筛选(空 = 全部参与班级) */
private String classId;
/** 提交状态筛选:NOT_SUBMITTED/SUBMITTED/LATE_SUBMITTED/RETURNED/REVIEWED */
private String submitStatus;
/** 批阅状态筛选:PENDING/REVIEWED/RETURNED */
private String reviewStatus;
/** 只看待批阅(reviewStatus=PENDING 且已提交)快捷过滤 */
private Boolean pendingGrade;
}
@@ -0,0 +1,57 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 学生矩阵行(PRD §18 页 5)。未交学生也生成占位行(submissionId=0)。
*/
@Data
public class StudentMatrixVo {
/** 当前版本提交行ID(未交占位为 0) */
private Integer submissionId;
/** 学生ID(用户中心数字ID) */
private Integer studentId;
/** 姓名 */
private String studentName;
private String classId;
private String className;
/** 阅读状态:0 未读 / 1 已读 */
private Integer isRead;
/** 首次阅读时间 */
private LocalDateTime firstReadAt;
/** 最近阅读时间 */
private LocalDateTime lastReadAt;
/** 提交状态(PRD 五态;未交为 NOT_SUBMITTED) */
private String submitStatus;
/** 提交时间 */
private LocalDateTime submittedAt;
/** 是否逾期:0/1 */
private Integer isLate;
/** 批阅状态 */
private String reviewStatus;
/** 评分 */
private BigDecimal score;
/** 最新提交附件 URL 列表(缩略图) */
private List<String> thumbnails;
/** 提交内容类型摘要:image=拍照/图片、attach=上传附件、text=文字作答(未交为空列表) */
private List<String> contentTypes;
}
@@ -0,0 +1,22 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.util.List;
/**
* 学生查看批阅结果(PRD §18 学生页 2 result 接口)。
*/
@Data
public class StudentResultVo {
private Integer homeworkId;
private String homeworkTitle;
/** 当前版本批阅结果(未批阅为 null) */
private SubmissionVersionVo latest;
/** 全部版本(含各版评语,新→旧) */
private List<SubmissionVersionVo> versions;
}
@@ -0,0 +1,20 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.Size;
import java.util.List;
/**
* 学生提交作业请求(PRD §18 学生页 3)。附件只接受 file/Upload 产物(fileId 或平台 URL),拒绝 dataURL。
*/
@Data
public class StudentSubmitRequest {
/** 文本内容(submitType 含 text 时有效) */
@Size(max = 20000, message = "文本内容过长")
private String text;
/** 附件列表(图片/文件) */
private List<AttachmentItem> attachments;
}
@@ -0,0 +1,16 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
/**
* 学科 DTO(布置过的学科 / 我的任教学科共用返回结构)。
*/
@Data
public class SubjectDto {
/** 学科编码(homework.subject 字典值 / bschclass SubID) */
private String subId;
/** 学科名称(本校学科字典解析;字典缺失时回退为编码) */
private String subName;
}
@@ -0,0 +1,22 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* 撤回重交动作(PUT /student/homeworks/{id}/submissions/{submissionId})。
* withdraw = 撤回(截止前,产生 NOT_SUBMITTED 新版本行);resubmit = 打回后重交(走提交语义)。
*/
@Data
public class SubmissionActionRequest {
@NotBlank(message = "action 不得为空")
private String action;
/** resubmit 时的提交内容(等同 StudentSubmitRequest) */
private String text;
/** resubmit 时的附件 */
private java.util.List<AttachmentItem> attachments;
}
@@ -0,0 +1,63 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/**
* 提交版本条目(历史版本/批阅弹窗/学生结果页共用)。
*/
@Data
public class SubmissionVersionVo {
private Integer submissionId;
private Integer homeworkId;
/** 学生ID(用户中心数字ID) */
private Integer studentId;
private String studentName;
/** 版本号 */
private Integer version;
/** 是否当前版本:0/1 */
private Integer isLatest;
/** 提交状态 */
private String submitStatus;
private String submitStatusDesc;
/** 提交时间 */
private LocalDateTime submittedAt;
/** 是否逾期 */
private Integer isLate;
/** 文本内容 */
private String text;
/** 附件 */
private List<AttachmentItem> attachments;
/** 评分 */
private BigDecimal score;
/** 评语 */
private String comment;
/** 批阅状态 */
private String reviewStatus;
private String reviewStatusDesc;
/** 批阅人(展示姓名快照,缺失时回退ID) */
private String reviewedBy;
/** 批阅时间 */
private LocalDateTime reviewedAt;
}
@@ -0,0 +1,19 @@
package com.yunhai.homework.model.v2;
import com.yunhai.homework.model.BasePageQuery;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 教师端作业列表查询(草稿箱/已发布列表共用)。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TeacherHomeworkQuery extends BasePageQuery {
/** 学科筛选 */
private String subject;
/** 作业状态筛选(已发布列表:PUBLISHED/CLOSED/ARCHIVED) */
private String hwStatus;
}
@@ -0,0 +1,16 @@
package com.yunhai.homework.model.v2;
import lombok.Data;
/**
* 任教班级 VO(按学科筛选任教班级返回结构)。
*/
@Data
public class TeachingClassVo {
/** 班级业务ID(bschclass sch_class.class_id) */
private String classId;
/** 班级名称(学校班级列表映射;缺失时回退为班级ID) */
private String className;
}
@@ -0,0 +1,31 @@
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);
}
@@ -0,0 +1,27 @@
package com.yunhai.homework.repository;
import com.yunhai.homework.entity.ContactsGroup;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface ContactsGroupRepository extends JpaRepository<ContactsGroup, Integer> {
Page<ContactsGroup> findAllByOrderByCreateTimeDesc(Pageable pageable);
/**
* 按创建人ID(用户中心数字ID)查询联系组列表。
*/
List<ContactsGroup> findByCreatorId(Integer creatorId);
/**
* 按创建人ID查询联系组列表(分页)。
*/
Page<ContactsGroup> findByCreatorId(Integer creatorId, Pageable pageable);
/**
* 查询全部联系组(按创建时间降序)。
*/
List<ContactsGroup> findAllByOrderByCreateTimeDesc();
}
@@ -0,0 +1,42 @@
package com.yunhai.homework.repository;
import com.yunhai.homework.entity.ContactsGroupUser;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ContactsGroupUserRepository extends JpaRepository<ContactsGroupUser, Integer> {
Page<ContactsGroupUser> findByGroupId(Integer groupId, Pageable pageable);
List<ContactsGroupUser> findByGroupId(Integer groupId);
/**
* 按组ID批量删除成员(JPQL bulk delete,SQL 立即执行)。
* 注意:不能用 derived delete——其 DELETE 延迟到 flush 才执行,
* 同事务内先 insert 会触发 uk_group_user 唯一约束冲突。
*/
@Modifying(clearAutomatically = true)
@Query("delete from ContactsGroupUser g where g.groupId = :groupId")
void deleteByGroupIdBulk(@Param("groupId") Integer groupId);
/**
* 按多个组ID批量删除成员。
*/
@Modifying(clearAutomatically = true)
@Query("delete from ContactsGroupUser g where g.groupId in :groupIds")
void deleteByGroupIdInBulk(@Param("groupIds") List<Integer> groupIds);
/**
* 按多个组ID查询所有成员(用于统计每组人数、组树构建)。
*/
List<ContactsGroupUser> findByGroupIdIn(List<Integer> groupIds);
/**
* 按组ID和用户ID查询(判断是否已存在)。
*/
boolean existsByGroupIdAndUserId(Integer groupId, Integer userId);
}

Some files were not shown because too many files have changed in this diff Show More