Files
system-design/software-copyright/01-writech-cloud-platform/自然写互动课堂教学管理云平台软件-源程序.md
2026-03-22 15:24:40 +08:00

3919 lines
144 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 自然写互动课堂教学管理云平台软件 V1.0
## 软件著作权鉴别材料 — 源程序
> **权利人**:深圳自然写科技有限公司
> **版本号**V1.0
---
## 源程序目录结构
```
01-writech-cloud-platform/
├── WritechCloudApplication.java
├── config/
│ ├── KafkaConfig.java
│ └── SecurityConfig.java
├── controller/
│ ├── AssignmentController.java
│ ├── AuthController.java
│ ├── DeviceController.java
│ └── StrokeController.java
├── model/
│ ├── Models.java
│ └── User.java
└── service/
├── DeviceService.java
├── MessageService.java
├── StrokeService.java
└── UserService.java
```
---
## 源程序文件清单
### (根目录)
#### `WritechCloudApplication.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 版权所有 (C) 2026
* 软件全称:自然写互动课堂教学管理云平台软件
* 版本号:V1.0
*
* 本文件为云平台主启动类,负责 Spring Boot 应用初始化、
* 微服务配置加载、健康检查端点注册及全局异常处理。
*/
package com.writech.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.http.HttpStatus;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 自然写互动课堂教学管理云平台 - 主启动类
*
* 系统采用微服务架构,按领域拆分为用户服务、课堂服务、
* 作业服务、设备服务、消息服务等多个独立微服务模块。
* 通过 Nginx/Kong API Gateway 统一接入,使用 Kafka
* 进行异步消息传递,Redis 实现会话与缓存管理。
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableAsync
@EnableScheduling
public class WritechCloudApplication {
/**
* 应用主入口
* 启动 Spring Boot 容器,加载所有微服务组件
*/
public static void main(String[] args) {
SpringApplication.run(WritechCloudApplication.class, args);
}
/**
* 跨域配置
* 允许前端应用和各终端 APP 跨域访问云平台 API
*/
@Configuration
public static class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
/**
* 全局异常处理器
* 统一捕获并格式化所有未处理异常,返回标准 JSON 响应
* 响应格式:{"code": 200, "msg": "success", "data": {...}}
*/
@RestControllerAdvice
public static class GlobalExceptionHandler {
/**
* 处理业务异常
* 业务逻辑中抛出的自定义异常,返回对应的错误码和提示信息
*/
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<?>> handleBusinessException(BusinessException ex) {
ApiResponse<?> response = ApiResponse.error(ex.getCode(), ex.getMessage());
return ResponseEntity.status(HttpStatus.OK).body(response);
}
/**
* 处理参数校验异常
* 请求参数不符合校验规则时返回详细的校验错误信息
*/
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<?>> handleIllegalArgument(IllegalArgumentException ex) {
ApiResponse<?> response = ApiResponse.error(400, "参数校验失败: " + ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
/**
* 处理未知异常
* 兜底处理所有未预见的系统异常,记录日志并返回统一错误响应
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<?>> handleException(Exception ex) {
ApiResponse<?> response = ApiResponse.error(500, "系统内部错误,请稍后重试");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
/**
* 统一 API 响应包装类
* 所有接口统一使用此格式返回数据
* 格式:{"code": 200, "msg": "success", "data": {...}}
*/
public static class ApiResponse<T> {
private int code;
private String msg;
private T data;
private LocalDateTime timestamp;
public ApiResponse() {
this.timestamp = LocalDateTime.now();
}
public ApiResponse(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
this.timestamp = LocalDateTime.now();
}
/** 成功响应(带数据) */
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
/** 成功响应(无数据) */
public static <T> ApiResponse<T> success() {
return new ApiResponse<>(200, "success", null);
}
/** 错误响应 */
public static <T> ApiResponse<T> error(int code, String msg) {
return new ApiResponse<>(code, msg, null);
}
public int getCode() { return code; }
public void setCode(int code) { this.code = code; }
public String getMsg() { return msg; }
public void setMsg(String msg) { this.msg = msg; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
public LocalDateTime getTimestamp() { return timestamp; }
public void setTimestamp(LocalDateTime timestamp) { this.timestamp = timestamp; }
}
/**
* 自定义业务异常类
* 用于在业务逻辑中抛出可预见的异常,包含错误码和消息
*/
public static class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() { return code; }
}
}
```
### `config/`
#### `config/KafkaConfig.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* Kafka 消息队列配置
* 配置笔迹数据流处理的Kafka生产者和消费者
*/
package com.writech.cloud.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.*;
import java.util.HashMap;
import java.util.Map;
/**
* Kafka 配置类
*
* 消息主题定义:
* - writech-stroke-topic:笔迹原始数据(网关/算力盒 → 云平台)
* - writech-recognition-topicAI识别请求(云平台 → AI引擎)
* - writech-result-topic:识别结果(AI引擎 → 云平台)
* - writech-notification-topic:通知消息(云平台 → 终端)
* - writech-stroke-dlq:笔迹数据死信队列(处理失败的消息)
*
* 数据流向:
* 点阵笔 → 网关/算力盒 → Kafka(stroke-topic) → 云平台数据接收服务
* → MongoDB存储 → Kafka(recognition-topic) → AI引擎处理
* → Kafka(result-topic) → 结果回写 → WebSocket推送终端
*/
@Configuration
public class KafkaConfig {
@Value("${spring.kafka.bootstrap-servers:localhost:9092}")
private String bootstrapServers;
@Value("${spring.kafka.consumer.group-id:writech-cloud-group}")
private String consumerGroupId;
/**
* Kafka 生产者配置
* 用于发送AI识别请求和通知消息
*/
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
// 消息可靠性配置
configProps.put(ProducerConfig.ACKS_CONFIG, "all"); // 所有副本确认
configProps.put(ProducerConfig.RETRIES_CONFIG, 3); // 重试3次
configProps.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 1000);
// 批量发送配置(提升笔迹数据吞吐量)
configProps.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); // 16KB
configProps.put(ProducerConfig.LINGER_MS_CONFIG, 10); // 延迟10ms
configProps.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); // 32MB缓冲
// 幂等性(防止重复消息)
configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
return new DefaultKafkaProducerFactory<>(configProps);
}
@Bean
public KafkaTemplate<String, String> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
/**
* Kafka 消费者配置
* 用于消费笔迹数据和识别结果
*/
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> configProps = new HashMap<>();
configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
configProps.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId);
configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
// 消费者配置
configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
configProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); // 手动提交
configProps.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500); // 每批最多500条
configProps.put(ConsumerConfig.FETCH_MIN_BYTES_CONFIG, 1024); // 最少1KB
configProps.put(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG, 200); // 最大等待200ms
return new DefaultKafkaConsumerFactory<>(configProps);
}
/**
* Kafka 监听器容器工厂
* 配置并发消费者数量和批量消费模式
*/
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, String> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
// 并发消费者数量(对应Topic的分区数)
factory.setConcurrency(8);
// 启用批量消费模式
factory.setBatchListener(true);
// 手动确认模式
factory.getContainerProperties().setAckMode(
org.springframework.kafka.listener.ContainerProperties.AckMode.MANUAL_IMMEDIATE);
return factory;
}
/**
* 笔迹数据Topic名称常量
*/
public static class Topics {
/** 笔迹原始数据 */
public static final String STROKE_DATA = "writech-stroke-topic";
/** AI识别请求 */
public static final String RECOGNITION_REQUEST = "writech-recognition-topic";
/** AI识别结果 */
public static final String RECOGNITION_RESULT = "writech-result-topic";
/** 通知消息 */
public static final String NOTIFICATION = "writech-notification-topic";
/** 笔迹数据死信队列 */
public static final String STROKE_DLQ = "writech-stroke-dlq";
/** 设备状态上报 */
public static final String DEVICE_STATUS = "writech-device-status-topic";
private Topics() {} // 禁止实例化
}
}
```
#### `config/SecurityConfig.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 安全配置 - JWT认证过滤器 + Spring Security配置
* 实现RBAC权限控制和全链路HTTPS/TLS 1.3加密
*/
package com.writech.cloud.config;
import com.writech.cloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Spring Security 安全配置
*
* 安全策略:
* - JWT Token + Refresh Token 双令牌认证机制
* - RBAC 角色权限控制(管理员/教师/学生/家长四级)
* - 全链路 HTTPS/TLS 1.3 加密传输
* - 请求签名校验 + 频率限流 + SQL注入/XSS防护
* - 敏感字段 AES-256 加密存储
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Value("${writech.jwt.secret:writech-cloud-platform-jwt-secret-key-2026}")
private String jwtSecret;
@Autowired
private UserService userService;
/**
* 安全过滤链配置
* 定义各API路径的访问权限规则
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRFREST API使用JWT认证,不需要CSRF防护)
.csrf().disable()
// 无状态会话(JWT方式不使用Session)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
// 路径权限配置
.authorizeRequests()
// 公开接口:登录、注册、验证码、健康检查
.antMatchers("/api/v1/auth/login").permitAll()
.antMatchers("/api/v1/auth/sms-code").permitAll()
.antMatchers("/api/v1/auth/refresh").permitAll()
.antMatchers("/actuator/health").permitAll()
.antMatchers("/ws/**").permitAll()
// 管理员专用接口
.antMatchers("/api/v1/admin/**").hasRole("ADMIN")
// 教师接口
.antMatchers("/api/v1/assignment/publish").hasAnyRole("ADMIN", "TEACHER")
.antMatchers("/api/v1/assignment/review/**").hasAnyRole("ADMIN", "TEACHER")
// 设备管理接口(管理员和教师)
.antMatchers("/api/v1/device/**").hasAnyRole("ADMIN", "TEACHER")
// 笔迹上传(网关/算力盒,使用设备证书认证)
.antMatchers("/api/v1/stroke/upload").hasRole("DEVICE")
// 其余接口需要认证
.anyRequest().authenticated()
.and()
// 添加JWT认证过滤器
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
// 添加请求限流过滤器
.addFilterBefore(rateLimitFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
/**
* JWT 认证过滤器 Bean
*/
@Bean
public JwtAuthenticationFilter jwtAuthFilter() {
return new JwtAuthenticationFilter(jwtSecret, userService);
}
/**
* 请求限流过滤器 Bean
*/
@Bean
public RateLimitFilter rateLimitFilter() {
return new RateLimitFilter();
}
/**
* JWT 认证过滤器
*
* 拦截所有请求,从 Authorization 头中提取并验证 JWT Token
* 验证通过后将用户信息放入 SecurityContext
*/
public static class JwtAuthenticationFilter implements Filter {
private final String jwtSecret;
private final UserService userService;
public JwtAuthenticationFilter(String jwtSecret, UserService userService) {
this.jwtSecret = jwtSecret;
this.userService = userService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 提取Token
String authorization = httpRequest.getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7);
try {
// 检查Token是否在黑名单中
if (userService.isTokenBlacklisted(token)) {
sendError(httpResponse, 401, "令牌已失效,请重新登录");
return;
}
// 解析并验证JWT
SecretKey key = Keys.hmacShaKeyFor(
jwtSecret.getBytes(StandardCharsets.UTF_8));
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
// 提取用户信息
String userId = claims.getSubject();
String role = claims.get("role", String.class);
String tokenType = claims.get("type", String.class);
// 只接受access类型的Token
if (!"access".equals(tokenType)) {
sendError(httpResponse, 401, "无效的令牌类型");
return;
}
// 将用户信息存入请求属性(供后续Controller使用)
httpRequest.setAttribute("userId", userId);
httpRequest.setAttribute("role", role);
} catch (io.jsonwebtoken.ExpiredJwtException e) {
sendError(httpResponse, 401, "令牌已过期,请刷新令牌");
return;
} catch (Exception e) {
sendError(httpResponse, 401, "令牌校验失败");
return;
}
}
chain.doFilter(request, response);
}
/** 发送错误响应 */
private void sendError(HttpServletResponse response, int code, String message)
throws IOException {
response.setStatus(code);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(
"{\"code\":" + code + ",\"msg\":\"" + message + "\",\"data\":null}");
}
}
/**
* 请求限流过滤器
*
* 基于IP和用户ID的双维度限流
* - IP维度:每分钟最多60次请求
* - 用户维度:每分钟最多120次请求
* - 敏感接口(登录/发送验证码):更严格的限流策略
*/
public static class RateLimitFilter implements Filter {
/** IP请求计数器(简化实现,生产环境使用Redis+滑动窗口) */
private final Map<String, List<Long>> ipRequestLog = new HashMap<>();
/** IP限流阈值(每分钟) */
private static final int IP_RATE_LIMIT = 60;
/** 时间窗口(毫秒) */
private static final long WINDOW_MS = 60_000;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String clientIp = getClientIp(httpRequest);
long now = System.currentTimeMillis();
// IP维度限流检查
synchronized (ipRequestLog) {
List<Long> timestamps = ipRequestLog.computeIfAbsent(
clientIp, k -> new ArrayList<>());
// 清理窗口外的记录
timestamps.removeIf(ts -> (now - ts) > WINDOW_MS);
if (timestamps.size() >= IP_RATE_LIMIT) {
httpResponse.setStatus(429);
httpResponse.setContentType("application/json;charset=UTF-8");
httpResponse.getWriter().write(
"{\"code\":429,\"msg\":\"请求频率过高,请稍后重试\",\"data\":null}");
return;
}
timestamps.add(now);
}
chain.doFilter(request, response);
}
/** 获取客户端真实IP(考虑代理/负载均衡) */
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// X-Forwarded-For可能包含多个IP,取第一个
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
}
```
### `controller/`
#### `controller/AssignmentController.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 作业管理控制器
* 负责作业/试卷的发布、回收、批改结果查询等接口
*/
package com.writech.cloud.controller;
import com.writech.cloud.WritechCloudApplication.ApiResponse;
import com.writech.cloud.WritechCloudApplication.BusinessException;
import com.writech.cloud.model.Assignment;
import com.writech.cloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import java.util.*;
/**
* 作业控制器 - /api/v1/assignment
*
* 教师发布作业/试卷 → 学生纸上作答(笔迹通过点阵笔采集)
* → 系统自动收集 → AI引擎识别批改 → 结果推送教师和家长
*/
@RestController
@RequestMapping("/api/v1/assignment")
public class AssignmentController {
@Autowired
private UserService userService;
/**
* 发布作业
* POST /api/v1/assignment/publish
*
* 教师创建并发布作业/试卷,指定班级、截止时间、题目内容
* 发布后自动推送通知至学生端和家长端
*/
@PostMapping("/publish")
public ApiResponse<AssignmentPublishResponse> publishAssignment(
@Valid @RequestBody AssignmentPublishRequest request,
@RequestHeader("Authorization") String auth) {
// 验证教师身份
String teacherId = extractUserIdFromToken(auth);
// 校验截止时间
if (request.getDeadline() != null && request.getDeadline().isBefore(LocalDateTime.now())) {
throw new BusinessException(400, "截止时间不能早于当前时间");
}
// 校验题目列表
if (request.getQuestions() == null || request.getQuestions().isEmpty()) {
throw new BusinessException(400, "作业题目不能为空");
}
// 创建作业记录
Assignment assignment = new Assignment();
assignment.setId(UUID.randomUUID().toString().replace("-", ""));
assignment.setTeacherId(teacherId);
assignment.setClassId(request.getClassId());
assignment.setTitle(request.getTitle());
assignment.setType(request.getType()); // homework/exam/practice
assignment.setSubject(request.getSubject());
assignment.setDeadline(request.getDeadline());
assignment.setStatus("published");
assignment.setPublishTime(LocalDateTime.now());
assignment.setTotalScore(calculateTotalScore(request.getQuestions()));
assignment.setQuestionCount(request.getQuestions().size());
// 关联点阵码页面(每道题对应特定点阵码区域)
if (request.getDotCodePages() != null) {
assignment.setDotCodePages(request.getDotCodePages());
}
// 保存作业及题目
// assignmentService.saveWithQuestions(assignment, request.getQuestions());
// 异步推送通知至学生端和家长端
// messageService.pushAssignmentNotification(assignment);
AssignmentPublishResponse response = new AssignmentPublishResponse();
response.setAssignmentId(assignment.getId());
response.setTitle(assignment.getTitle());
response.setPublishTime(assignment.getPublishTime());
response.setStudentCount(getClassStudentCount(request.getClassId()));
return ApiResponse.success(response);
}
/**
* 获取作业列表
* GET /api/v1/assignment/list
*
* 教师查看已发布的作业列表,支持按班级、状态、时间筛选
*/
@GetMapping("/list")
public ApiResponse<Page<AssignmentSummary>> listAssignments(
@RequestParam(required = false) String classId,
@RequestParam(required = false) String status,
@RequestParam(required = false) String subject,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestHeader("Authorization") String auth) {
String userId = extractUserIdFromToken(auth);
// Page<AssignmentSummary> result = assignmentService.queryList(...)
return ApiResponse.success(null);
}
/**
* 获取作业详情
* GET /api/v1/assignment/{id}
*/
@GetMapping("/{id}")
public ApiResponse<AssignmentDetailResponse> getAssignment(@PathVariable String id) {
// Assignment assignment = assignmentService.findById(id);
return ApiResponse.success(null);
}
/**
* 获取批改结果
* GET /api/v1/result/{assignmentId}
*
* 查询指定作业的AI批改结果,包含每个学生的识别文本、
* 得分、错误详情及AI反馈建议
*/
@GetMapping("/result/{assignmentId}")
public ApiResponse<AssignmentResultResponse> getResult(
@PathVariable String assignmentId,
@RequestParam(required = false) String studentId) {
AssignmentResultResponse response = new AssignmentResultResponse();
response.setAssignmentId(assignmentId);
response.setTotalStudents(40);
response.setSubmittedCount(38);
response.setGradedCount(38);
response.setAverageScore(85.5);
response.setHighestScore(100.0);
response.setLowestScore(45.0);
// 每个学生的批改结果
List<StudentResult> studentResults = new ArrayList<>();
// studentResults = resultService.getStudentResults(assignmentId, studentId);
response.setStudentResults(studentResults);
return ApiResponse.success(response);
}
/**
* 教师人工复核批改
* PUT /api/v1/assignment/review/{assignmentId}
*
* AI批改后教师可进行人工复核,修正AI评分或添加评语
*/
@PutMapping("/review/{assignmentId}")
public ApiResponse<Void> reviewAssignment(
@PathVariable String assignmentId,
@Valid @RequestBody ReviewRequest request,
@RequestHeader("Authorization") String auth) {
String teacherId = extractUserIdFromToken(auth);
// 遍历教师的复核修改
for (ReviewItem item : request.getReviewItems()) {
// resultService.updateReview(assignmentId, item.getStudentId(),
// item.getQuestionId(), item.getManualScore(),
// item.getTeacherComment(), teacherId);
}
return ApiResponse.success();
}
/**
* 学情报告接口
* GET /api/v1/report/student/{id}
*
* 获取指定学生的学情报告,包含知识点掌握度、
* 书写能力评估、成绩趋势等多维度分析数据
*/
@GetMapping("/report/student/{studentId}")
public ApiResponse<StudentReportResponse> getStudentReport(
@PathVariable String studentId,
@RequestParam(required = false) String subject,
@RequestParam(required = false) String dateRange) {
StudentReportResponse report = new StudentReportResponse();
report.setStudentId(studentId);
report.setReportDate(LocalDateTime.now());
// 知识点掌握度
List<KnowledgePoint> knowledgePoints = new ArrayList<>();
// knowledgePoints = analyticsService.getKnowledgeMastery(studentId, subject);
report.setKnowledgePoints(knowledgePoints);
// 书写能力评估
WritingAbility writingAbility = new WritingAbility();
writingAbility.setStrokeOrderScore(88.5);
writingAbility.setStructureScore(82.3);
writingAbility.setNeatnessScore(90.1);
writingAbility.setOverallScore(86.9);
report.setWritingAbility(writingAbility);
return ApiResponse.success(report);
}
// ==================== 内部方法 ====================
private String extractUserIdFromToken(String auth) {
// 从JWT Token解析用户ID
return "teacher_001";
}
private double calculateTotalScore(List<QuestionItem> questions) {
return questions.stream()
.mapToDouble(QuestionItem::getScore)
.sum();
}
private int getClassStudentCount(String classId) {
return 40; // 查询班级学生数
}
// ==================== DTO 定义 ====================
public static class AssignmentPublishRequest {
@NotBlank private String classId;
@NotBlank private String title;
private String type; // homework/exam/practice
private String subject;
private LocalDateTime deadline;
private List<QuestionItem> questions;
private List<String> dotCodePages; // 关联的点阵码页面ID
public String getClassId() { return classId; }
public void setClassId(String id) { this.classId = id; }
public String getTitle() { return title; }
public void setTitle(String t) { this.title = t; }
public String getType() { return type; }
public void setType(String t) { this.type = t; }
public String getSubject() { return subject; }
public void setSubject(String s) { this.subject = s; }
public LocalDateTime getDeadline() { return deadline; }
public void setDeadline(LocalDateTime d) { this.deadline = d; }
public List<QuestionItem> getQuestions() { return questions; }
public void setQuestions(List<QuestionItem> q) { this.questions = q; }
public List<String> getDotCodePages() { return dotCodePages; }
public void setDotCodePages(List<String> p) { this.dotCodePages = p; }
}
public static class QuestionItem {
private int questionNo;
private String type; // choice/fill/short_answer/essay/math
private String content;
private String answer;
private double score;
private String knowledgePointId;
public int getQuestionNo() { return questionNo; }
public void setQuestionNo(int n) { this.questionNo = n; }
public String getType() { return type; }
public void setType(String t) { this.type = t; }
public String getContent() { return content; }
public void setContent(String c) { this.content = c; }
public String getAnswer() { return answer; }
public void setAnswer(String a) { this.answer = a; }
public double getScore() { return score; }
public void setScore(double s) { this.score = s; }
public String getKnowledgePointId() { return knowledgePointId; }
public void setKnowledgePointId(String id) { this.knowledgePointId = id; }
}
public static class AssignmentPublishResponse {
private String assignmentId;
private String title;
private LocalDateTime publishTime;
private int studentCount;
public String getAssignmentId() { return assignmentId; }
public void setAssignmentId(String id) { this.assignmentId = id; }
public String getTitle() { return title; }
public void setTitle(String t) { this.title = t; }
public LocalDateTime getPublishTime() { return publishTime; }
public void setPublishTime(LocalDateTime t) { this.publishTime = t; }
public int getStudentCount() { return studentCount; }
public void setStudentCount(int c) { this.studentCount = c; }
}
public static class AssignmentSummary {
private String id;
private String title;
private String type;
private String status;
private int submittedCount;
private int totalCount;
private LocalDateTime publishTime;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTitle() { return title; }
public void setTitle(String t) { this.title = t; }
public String getType() { return type; }
public void setType(String t) { this.type = t; }
public String getStatus() { return status; }
public void setStatus(String s) { this.status = s; }
public int getSubmittedCount() { return submittedCount; }
public void setSubmittedCount(int c) { this.submittedCount = c; }
public int getTotalCount() { return totalCount; }
public void setTotalCount(int c) { this.totalCount = c; }
public LocalDateTime getPublishTime() { return publishTime; }
public void setPublishTime(LocalDateTime t) { this.publishTime = t; }
}
public static class AssignmentDetailResponse {
private Assignment assignment;
private List<QuestionItem> questions;
public Assignment getAssignment() { return assignment; }
public void setAssignment(Assignment a) { this.assignment = a; }
public List<QuestionItem> getQuestions() { return questions; }
public void setQuestions(List<QuestionItem> q) { this.questions = q; }
}
public static class AssignmentResultResponse {
private String assignmentId;
private int totalStudents;
private int submittedCount;
private int gradedCount;
private double averageScore;
private double highestScore;
private double lowestScore;
private List<StudentResult> studentResults;
public String getAssignmentId() { return assignmentId; }
public void setAssignmentId(String id) { this.assignmentId = id; }
public int getTotalStudents() { return totalStudents; }
public void setTotalStudents(int c) { this.totalStudents = c; }
public int getSubmittedCount() { return submittedCount; }
public void setSubmittedCount(int c) { this.submittedCount = c; }
public int getGradedCount() { return gradedCount; }
public void setGradedCount(int c) { this.gradedCount = c; }
public double getAverageScore() { return averageScore; }
public void setAverageScore(double s) { this.averageScore = s; }
public double getHighestScore() { return highestScore; }
public void setHighestScore(double s) { this.highestScore = s; }
public double getLowestScore() { return lowestScore; }
public void setLowestScore(double s) { this.lowestScore = s; }
public List<StudentResult> getStudentResults() { return studentResults; }
public void setStudentResults(List<StudentResult> r) { this.studentResults = r; }
}
public static class StudentResult {
private String studentId;
private String studentName;
private double totalScore;
private List<QuestionResult> questionResults;
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public String getStudentName() { return studentName; }
public void setStudentName(String n) { this.studentName = n; }
public double getTotalScore() { return totalScore; }
public void setTotalScore(double s) { this.totalScore = s; }
public List<QuestionResult> getQuestionResults() { return questionResults; }
public void setQuestionResults(List<QuestionResult> r) { this.questionResults = r; }
}
public static class QuestionResult {
private int questionNo;
private String ocrText;
private double score;
private boolean isCorrect;
private String aiFeedback;
public int getQuestionNo() { return questionNo; }
public void setQuestionNo(int n) { this.questionNo = n; }
public String getOcrText() { return ocrText; }
public void setOcrText(String t) { this.ocrText = t; }
public double getScore() { return score; }
public void setScore(double s) { this.score = s; }
public boolean isCorrect() { return isCorrect; }
public void setCorrect(boolean c) { this.isCorrect = c; }
public String getAiFeedback() { return aiFeedback; }
public void setAiFeedback(String f) { this.aiFeedback = f; }
}
public static class ReviewRequest {
private List<ReviewItem> reviewItems;
public List<ReviewItem> getReviewItems() { return reviewItems; }
public void setReviewItems(List<ReviewItem> items) { this.reviewItems = items; }
}
public static class ReviewItem {
private String studentId;
private int questionId;
private Double manualScore;
private String teacherComment;
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public int getQuestionId() { return questionId; }
public void setQuestionId(int id) { this.questionId = id; }
public Double getManualScore() { return manualScore; }
public void setManualScore(Double s) { this.manualScore = s; }
public String getTeacherComment() { return teacherComment; }
public void setTeacherComment(String c) { this.teacherComment = c; }
}
public static class StudentReportResponse {
private String studentId;
private LocalDateTime reportDate;
private List<KnowledgePoint> knowledgePoints;
private WritingAbility writingAbility;
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public LocalDateTime getReportDate() { return reportDate; }
public void setReportDate(LocalDateTime d) { this.reportDate = d; }
public List<KnowledgePoint> getKnowledgePoints() { return knowledgePoints; }
public void setKnowledgePoints(List<KnowledgePoint> kp) { this.knowledgePoints = kp; }
public WritingAbility getWritingAbility() { return writingAbility; }
public void setWritingAbility(WritingAbility wa) { this.writingAbility = wa; }
}
public static class KnowledgePoint {
private String id;
private String name;
private double masteryRate;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String n) { this.name = n; }
public double getMasteryRate() { return masteryRate; }
public void setMasteryRate(double r) { this.masteryRate = r; }
}
public static class WritingAbility {
private double strokeOrderScore;
private double structureScore;
private double neatnessScore;
private double overallScore;
public double getStrokeOrderScore() { return strokeOrderScore; }
public void setStrokeOrderScore(double s) { this.strokeOrderScore = s; }
public double getStructureScore() { return structureScore; }
public void setStructureScore(double s) { this.structureScore = s; }
public double getNeatnessScore() { return neatnessScore; }
public void setNeatnessScore(double s) { this.neatnessScore = s; }
public double getOverallScore() { return overallScore; }
public void setOverallScore(double s) { this.overallScore = s; }
}
}
```
#### `controller/AuthController.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 用户认证控制器
* 负责用户登录、登出、Token刷新等认证相关接口
* 采用 JWT Token + Refresh Token 双令牌机制
*/
package com.writech.cloud.controller;
import com.writech.cloud.WritechCloudApplication.ApiResponse;
import com.writech.cloud.WritechCloudApplication.BusinessException;
import com.writech.cloud.model.User;
import com.writech.cloud.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.time.LocalDateTime;
/**
* 认证控制器 - /api/v1/auth
*
* 实现教师/学生/管理员/家长多角色用户的统一认证
* 支持手机号+密码、手机号+验证码、微信/钉钉第三方登录
*/
@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {
@Autowired
private UserService userService;
/** JWT密钥 */
@Value("${writech.jwt.secret:writech-cloud-platform-jwt-secret-key-2026}")
private String jwtSecret;
/** Access Token 有效期(秒),默认2小时 */
@Value("${writech.jwt.access-token-expire:7200}")
private long accessTokenExpire;
/** Refresh Token 有效期(秒),默认7天 */
@Value("${writech.jwt.refresh-token-expire:604800}")
private long refreshTokenExpire;
/**
* 用户登录接口
* POST /api/v1/auth/login
*
* 验证用户身份,签发 JWT Access Token 和 Refresh Token
* Access Token 有效期2小时,Refresh Token 有效期7天
*
* @param request 登录请求(包含手机号、密码/验证码、登录方式)
* @return 包含双令牌和用户基本信息的响应
*/
@PostMapping("/login")
public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
// 校验登录参数
if (request.getLoginType() == null) {
throw new BusinessException(400, "登录方式不能为空");
}
User user = null;
// 根据不同登录方式验证身份
switch (request.getLoginType()) {
case "password":
// 手机号 + 密码登录
user = userService.verifyByPassword(request.getPhone(), request.getPassword());
break;
case "sms":
// 手机号 + 短信验证码登录
user = userService.verifyBySmsCode(request.getPhone(), request.getSmsCode());
break;
case "wechat":
// 微信授权登录
user = userService.verifyByWechat(request.getWechatCode());
break;
case "dingtalk":
// 钉钉授权登录
user = userService.verifyByDingtalk(request.getDingtalkCode());
break;
default:
throw new BusinessException(400, "不支持的登录方式: " + request.getLoginType());
}
if (user == null) {
throw new BusinessException(401, "登录失败,用户名或密码错误");
}
// 检查用户状态
if (user.getStatus() != 1) {
throw new BusinessException(403, "账户已被禁用,请联系管理员");
}
// 生成双令牌
String accessToken = generateAccessToken(user);
String refreshToken = generateRefreshToken(user);
// 更新用户最后登录时间和登录IP
userService.updateLoginInfo(user.getId(), LocalDateTime.now(), request.getClientIp());
// 构建登录响应
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(accessTokenExpire);
response.setUserId(user.getId());
response.setUserName(user.getName());
response.setRole(user.getRole());
response.setSchoolId(user.getSchoolId());
response.setSchoolName(user.getSchoolName());
return ApiResponse.success(response);
}
/**
* Token 刷新接口
* POST /api/v1/auth/refresh
*
* 使用 Refresh Token 换取新的 Access Token
* 避免用户频繁重新登录,提升使用体验
*
* @param request 刷新请求(包含 Refresh Token
* @return 新的 Access Token
*/
@PostMapping("/refresh")
public ApiResponse<TokenRefreshResponse> refreshToken(@Valid @RequestBody TokenRefreshRequest request) {
try {
// 解析并验证 Refresh Token
Claims claims = parseToken(request.getRefreshToken());
String userId = claims.getSubject();
String tokenType = claims.get("type", String.class);
// 确保是 Refresh Token 类型
if (!"refresh".equals(tokenType)) {
throw new BusinessException(401, "无效的刷新令牌");
}
// 查询用户信息(确保用户仍然有效)
User user = userService.findById(userId);
if (user == null || user.getStatus() != 1) {
throw new BusinessException(401, "用户不存在或已被禁用");
}
// 生成新的 Access Token
String newAccessToken = generateAccessToken(user);
TokenRefreshResponse response = new TokenRefreshResponse();
response.setAccessToken(newAccessToken);
response.setExpiresIn(accessTokenExpire);
return ApiResponse.success(response);
} catch (Exception e) {
throw new BusinessException(401, "令牌刷新失败: " + e.getMessage());
}
}
/**
* 用户登出接口
* POST /api/v1/auth/logout
*
* 将当前 Token 加入黑名单,使其立即失效
* 同时清除 Redis 中的会话缓存
*/
@PostMapping("/logout")
public ApiResponse<Void> logout(@RequestHeader("Authorization") String authorization) {
String token = extractToken(authorization);
if (token != null) {
// 将Token加入Redis黑名单,使其立即失效
userService.invalidateToken(token);
}
return ApiResponse.success();
}
/**
* 发送短信验证码
* POST /api/v1/auth/sms-code
*
* 向指定手机号发送登录验证码,验证码5分钟内有效
* 同一手机号60秒内只能发送一次
*/
@PostMapping("/sms-code")
public ApiResponse<Void> sendSmsCode(@RequestBody SmsCodeRequest request) {
if (request.getPhone() == null || request.getPhone().length() != 11) {
throw new BusinessException(400, "请输入正确的手机号");
}
userService.sendSmsVerificationCode(request.getPhone());
return ApiResponse.success();
}
/**
* 获取当前登录用户信息
* GET /api/v1/auth/profile
*
* 根据 Token 中的用户ID查询完整的用户信息
* 包括角色、学校、班级等关联信息
*/
@GetMapping("/profile")
public ApiResponse<UserProfileResponse> getProfile(@RequestHeader("Authorization") String authorization) {
String token = extractToken(authorization);
Claims claims = parseToken(token);
String userId = claims.getSubject();
User user = userService.findById(userId);
if (user == null) {
throw new BusinessException(404, "用户不存在");
}
UserProfileResponse profile = new UserProfileResponse();
profile.setUserId(user.getId());
profile.setName(user.getName());
profile.setPhone(maskPhone(user.getPhone()));
profile.setRole(user.getRole());
profile.setSchoolId(user.getSchoolId());
profile.setSchoolName(user.getSchoolName());
profile.setAvatar(user.getAvatar());
profile.setLastLoginTime(user.getLastLoginTime());
return ApiResponse.success(profile);
}
/**
* 修改密码
* PUT /api/v1/auth/password
*/
@PutMapping("/password")
public ApiResponse<Void> changePassword(@RequestHeader("Authorization") String authorization,
@Valid @RequestBody ChangePasswordRequest request) {
String token = extractToken(authorization);
Claims claims = parseToken(token);
String userId = claims.getSubject();
// 验证旧密码
boolean verified = userService.verifyPassword(userId, request.getOldPassword());
if (!verified) {
throw new BusinessException(400, "原密码错误");
}
// 更新密码
userService.updatePassword(userId, request.getNewPassword());
// 使所有现有Token失效,强制重新登录
userService.invalidateAllTokens(userId);
return ApiResponse.success();
}
// ==================== 内部方法 ====================
/**
* 生成 Access Token
* 有效期2小时,包含用户ID、角色、学校信息
*/
private String generateAccessToken(User user) {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
Date now = new Date();
Date expiry = new Date(now.getTime() + accessTokenExpire * 1000);
return Jwts.builder()
.setSubject(user.getId())
.claim("role", user.getRole())
.claim("schoolId", user.getSchoolId())
.claim("type", "access")
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 生成 Refresh Token
* 有效期7天,仅包含用户ID和令牌类型
*/
private String generateRefreshToken(User user) {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
Date now = new Date();
Date expiry = new Date(now.getTime() + refreshTokenExpire * 1000);
return Jwts.builder()
.setSubject(user.getId())
.claim("type", "refresh")
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/** 解析 JWT Token */
private Claims parseToken(String token) {
SecretKey key = Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8));
return Jwts.parserBuilder().setSigningKey(key).build()
.parseClaimsJws(token).getBody();
}
/** 从 Authorization 头中提取 Token */
private String extractToken(String authorization) {
if (authorization != null && authorization.startsWith("Bearer ")) {
return authorization.substring(7);
}
return null;
}
/** 手机号脱敏处理(中间4位替换为****) */
private String maskPhone(String phone) {
if (phone == null || phone.length() != 11) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
// ==================== 请求/响应 DTO ====================
/** 登录请求 */
public static class LoginRequest {
@NotBlank(message = "登录方式不能为空")
private String loginType; // password/sms/wechat/dingtalk
private String phone;
private String password;
private String smsCode;
private String wechatCode;
private String dingtalkCode;
private String clientIp;
public String getLoginType() { return loginType; }
public void setLoginType(String loginType) { this.loginType = loginType; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getSmsCode() { return smsCode; }
public void setSmsCode(String smsCode) { this.smsCode = smsCode; }
public String getWechatCode() { return wechatCode; }
public void setWechatCode(String wechatCode) { this.wechatCode = wechatCode; }
public String getDingtalkCode() { return dingtalkCode; }
public void setDingtalkCode(String dingtalkCode) { this.dingtalkCode = dingtalkCode; }
public String getClientIp() { return clientIp; }
public void setClientIp(String clientIp) { this.clientIp = clientIp; }
}
/** 登录响应 */
public static class LoginResponse {
private String accessToken;
private String refreshToken;
private long expiresIn;
private String userId;
private String userName;
private String role;
private String schoolId;
private String schoolName;
public String getAccessToken() { return accessToken; }
public void setAccessToken(String t) { this.accessToken = t; }
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String t) { this.refreshToken = t; }
public long getExpiresIn() { return expiresIn; }
public void setExpiresIn(long e) { this.expiresIn = e; }
public String getUserId() { return userId; }
public void setUserId(String id) { this.userId = id; }
public String getUserName() { return userName; }
public void setUserName(String n) { this.userName = n; }
public String getRole() { return role; }
public void setRole(String r) { this.role = r; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getSchoolName() { return schoolName; }
public void setSchoolName(String n) { this.schoolName = n; }
}
/** Token刷新请求 */
public static class TokenRefreshRequest {
@NotBlank(message = "刷新令牌不能为空")
private String refreshToken;
public String getRefreshToken() { return refreshToken; }
public void setRefreshToken(String t) { this.refreshToken = t; }
}
/** Token刷新响应 */
public static class TokenRefreshResponse {
private String accessToken;
private long expiresIn;
public String getAccessToken() { return accessToken; }
public void setAccessToken(String t) { this.accessToken = t; }
public long getExpiresIn() { return expiresIn; }
public void setExpiresIn(long e) { this.expiresIn = e; }
}
/** 短信验证码请求 */
public static class SmsCodeRequest {
private String phone;
public String getPhone() { return phone; }
public void setPhone(String p) { this.phone = p; }
}
/** 用户信息响应 */
public static class UserProfileResponse {
private String userId;
private String name;
private String phone;
private String role;
private String schoolId;
private String schoolName;
private String avatar;
private LocalDateTime lastLoginTime;
public String getUserId() { return userId; }
public void setUserId(String id) { this.userId = id; }
public String getName() { return name; }
public void setName(String n) { this.name = n; }
public String getPhone() { return phone; }
public void setPhone(String p) { this.phone = p; }
public String getRole() { return role; }
public void setRole(String r) { this.role = r; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getSchoolName() { return schoolName; }
public void setSchoolName(String n) { this.schoolName = n; }
public String getAvatar() { return avatar; }
public void setAvatar(String a) { this.avatar = a; }
public LocalDateTime getLastLoginTime() { return lastLoginTime; }
public void setLastLoginTime(LocalDateTime t) { this.lastLoginTime = t; }
}
/** 修改密码请求 */
public static class ChangePasswordRequest {
@NotBlank(message = "原密码不能为空")
private String oldPassword;
@NotBlank(message = "新密码不能为空")
private String newPassword;
public String getOldPassword() { return oldPassword; }
public void setOldPassword(String p) { this.oldPassword = p; }
public String getNewPassword() { return newPassword; }
public void setNewPassword(String p) { this.newPassword = p; }
}
}
```
#### `controller/DeviceController.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 设备管理控制器
* 负责点阵笔、网关、终端设备的注册、绑定、状态查询等接口
*/
package com.writech.cloud.controller;
import com.writech.cloud.WritechCloudApplication.ApiResponse;
import com.writech.cloud.WritechCloudApplication.BusinessException;
import com.writech.cloud.model.Device;
import com.writech.cloud.service.DeviceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
import java.util.*;
/**
* 设备控制器 - /api/v1/device
*
* 管理互动课堂中涉及的所有智能硬件设备:
* - 点阵笔(pen):学生书写工具,通过BLE连接网关
* - 网关设备(gateway):教室中枢,管理多支笔的连接与数据转发
* - 终端设备(terminal):黑板、PC、电视、平板等显示终端
* - 算力盒(edge_box):教室端AI推理设备
*/
@RestController
@RequestMapping("/api/v1/device")
public class DeviceController {
@Autowired
private DeviceService deviceService;
/**
* 设备注册接口
* POST /api/v1/device/register
*
* 将新设备注册到云平台,绑定至指定用户和学校
* 注册时校验设备MAC地址唯一性和设备证书有效性
*
* @param request 注册请求(MAC地址、设备类型、序列号等)
* @return 注册成功后的设备信息
*/
@PostMapping("/register")
public ApiResponse<DeviceRegisterResponse> registerDevice(
@Valid @RequestBody DeviceRegisterRequest request) {
// 校验设备MAC地址格式
if (!isValidMacAddress(request.getMacAddr())) {
throw new BusinessException(400, "无效的MAC地址格式");
}
// 检查设备是否已注册
Device existing = deviceService.findByMacAddr(request.getMacAddr());
if (existing != null) {
throw new BusinessException(409, "设备已注册,MAC地址: " + request.getMacAddr());
}
// 校验设备证书(X.509
boolean certValid = deviceService.validateDeviceCertificate(
request.getMacAddr(), request.getDeviceCert());
if (!certValid) {
throw new BusinessException(403, "设备证书校验失败,拒绝注册");
}
// 创建设备记录
Device device = new Device();
device.setId(UUID.randomUUID().toString().replace("-", ""));
device.setType(request.getDeviceType());
device.setMacAddr(request.getMacAddr());
device.setSerialNumber(request.getSerialNumber());
device.setFirmwareVersion(request.getFirmwareVersion());
device.setBindUserId(request.getUserId());
device.setSchoolId(request.getSchoolId());
device.setClassroomId(request.getClassroomId());
device.setStatus(1); // 1=在线
device.setRegisterTime(LocalDateTime.now());
device.setLastHeartbeat(LocalDateTime.now());
deviceService.save(device);
// 返回注册结果
DeviceRegisterResponse response = new DeviceRegisterResponse();
response.setDeviceId(device.getId());
response.setMacAddr(device.getMacAddr());
response.setDeviceType(device.getType());
response.setRegisteredAt(device.getRegisterTime());
return ApiResponse.success(response);
}
/**
* 设备绑定接口
* POST /api/v1/device/bind
*
* 将已注册设备绑定至指定用户(教师/学生)
* 一支笔只能绑定一个用户,一个用户可绑定多支笔
*/
@PostMapping("/bind")
public ApiResponse<Void> bindDevice(@Valid @RequestBody DeviceBindRequest request) {
Device device = deviceService.findById(request.getDeviceId());
if (device == null) {
throw new BusinessException(404, "设备不存在");
}
// 检查笔是否已被其他用户绑定
if ("pen".equals(device.getType()) && device.getBindUserId() != null
&& !device.getBindUserId().equals(request.getUserId())) {
throw new BusinessException(409, "该笔已绑定其他用户,请先解绑");
}
deviceService.bindDevice(request.getDeviceId(), request.getUserId(),
request.getClassroomId());
return ApiResponse.success();
}
/**
* 设备解绑接口
* POST /api/v1/device/unbind
*/
@PostMapping("/unbind")
public ApiResponse<Void> unbindDevice(@RequestBody DeviceUnbindRequest request) {
deviceService.unbindDevice(request.getDeviceId());
return ApiResponse.success();
}
/**
* 查询设备列表
* GET /api/v1/device/list
*
* 按学校/教室/设备类型/状态等条件分页查询设备
*/
@GetMapping("/list")
public ApiResponse<Page<Device>> listDevices(
@RequestParam(required = false) String schoolId,
@RequestParam(required = false) String classroomId,
@RequestParam(required = false) String deviceType,
@RequestParam(required = false) Integer status,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
Page<Device> devices = deviceService.queryDevices(
schoolId, classroomId, deviceType, status,
PageRequest.of(page, size));
return ApiResponse.success(devices);
}
/**
* 查询单个设备详情
* GET /api/v1/device/{id}
*/
@GetMapping("/{id}")
public ApiResponse<DeviceDetailResponse> getDevice(@PathVariable String id) {
Device device = deviceService.findById(id);
if (device == null) {
throw new BusinessException(404, "设备不存在");
}
DeviceDetailResponse detail = new DeviceDetailResponse();
detail.setDeviceId(device.getId());
detail.setType(device.getType());
detail.setMacAddr(device.getMacAddr());
detail.setSerialNumber(device.getSerialNumber());
detail.setFirmwareVersion(device.getFirmwareVersion());
detail.setStatus(device.getStatus());
detail.setBindUserId(device.getBindUserId());
detail.setSchoolId(device.getSchoolId());
detail.setClassroomId(device.getClassroomId());
detail.setBatteryLevel(device.getBatteryLevel());
detail.setLastHeartbeat(device.getLastHeartbeat());
detail.setRegisterTime(device.getRegisterTime());
return ApiResponse.success(detail);
}
/**
* 设备心跳上报接口
* POST /api/v1/device/heartbeat
*
* 设备定期上报在线状态、电量、连接笔数等信息
* 网关设备每30秒上报一次,笔设备每5分钟上报一次
*/
@PostMapping("/heartbeat")
public ApiResponse<Void> heartbeat(@Valid @RequestBody HeartbeatRequest request) {
Device device = deviceService.findById(request.getDeviceId());
if (device == null) {
throw new BusinessException(404, "设备不存在");
}
// 更新设备状态
device.setStatus(1); // 在线
device.setLastHeartbeat(LocalDateTime.now());
device.setBatteryLevel(request.getBatteryLevel());
if (request.getConnectedPenCount() != null) {
device.setConnectedPenCount(request.getConnectedPenCount());
}
if (request.getCpuUsage() != null) {
device.setCpuUsage(request.getCpuUsage());
}
if (request.getMemoryUsage() != null) {
device.setMemoryUsage(request.getMemoryUsage());
}
deviceService.updateHeartbeat(device);
return ApiResponse.success();
}
/**
* 批量查询教室设备拓扑
* GET /api/v1/device/topology/{classroomId}
*
* 返回指定教室中所有设备的连接拓扑关系
* 包括网关、笔、算力盒、黑板等设备的层级关系
*/
@GetMapping("/topology/{classroomId}")
public ApiResponse<ClassroomTopology> getTopology(@PathVariable String classroomId) {
ClassroomTopology topology = deviceService.buildClassroomTopology(classroomId);
return ApiResponse.success(topology);
}
// ==================== 内部方法 ====================
/** MAC地址格式校验(支持 XX:XX:XX:XX:XX:XX 和 XX-XX-XX-XX-XX-XX */
private boolean isValidMacAddress(String mac) {
if (mac == null) return false;
return mac.matches("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$");
}
// ==================== DTO 定义 ====================
/** 设备注册请求 */
public static class DeviceRegisterRequest {
@NotBlank(message = "设备类型不能为空")
private String deviceType; // pen/gateway/terminal/edge_box
@NotBlank(message = "MAC地址不能为空")
private String macAddr;
private String serialNumber;
private String firmwareVersion;
private String userId;
private String schoolId;
private String classroomId;
private String deviceCert; // X.509设备证书
public String getDeviceType() { return deviceType; }
public void setDeviceType(String t) { this.deviceType = t; }
public String getMacAddr() { return macAddr; }
public void setMacAddr(String m) { this.macAddr = m; }
public String getSerialNumber() { return serialNumber; }
public void setSerialNumber(String s) { this.serialNumber = s; }
public String getFirmwareVersion() { return firmwareVersion; }
public void setFirmwareVersion(String v) { this.firmwareVersion = v; }
public String getUserId() { return userId; }
public void setUserId(String id) { this.userId = id; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getClassroomId() { return classroomId; }
public void setClassroomId(String id) { this.classroomId = id; }
public String getDeviceCert() { return deviceCert; }
public void setDeviceCert(String c) { this.deviceCert = c; }
}
/** 设备注册响应 */
public static class DeviceRegisterResponse {
private String deviceId;
private String macAddr;
private String deviceType;
private LocalDateTime registeredAt;
public String getDeviceId() { return deviceId; }
public void setDeviceId(String id) { this.deviceId = id; }
public String getMacAddr() { return macAddr; }
public void setMacAddr(String m) { this.macAddr = m; }
public String getDeviceType() { return deviceType; }
public void setDeviceType(String t) { this.deviceType = t; }
public LocalDateTime getRegisteredAt() { return registeredAt; }
public void setRegisteredAt(LocalDateTime t) { this.registeredAt = t; }
}
/** 设备绑定请求 */
public static class DeviceBindRequest {
@NotBlank private String deviceId;
@NotBlank private String userId;
private String classroomId;
public String getDeviceId() { return deviceId; }
public void setDeviceId(String id) { this.deviceId = id; }
public String getUserId() { return userId; }
public void setUserId(String id) { this.userId = id; }
public String getClassroomId() { return classroomId; }
public void setClassroomId(String id) { this.classroomId = id; }
}
/** 设备解绑请求 */
public static class DeviceUnbindRequest {
private String deviceId;
public String getDeviceId() { return deviceId; }
public void setDeviceId(String id) { this.deviceId = id; }
}
/** 心跳请求 */
public static class HeartbeatRequest {
@NotBlank private String deviceId;
private Integer batteryLevel;
private Integer connectedPenCount;
private Double cpuUsage;
private Double memoryUsage;
public String getDeviceId() { return deviceId; }
public void setDeviceId(String id) { this.deviceId = id; }
public Integer getBatteryLevel() { return batteryLevel; }
public void setBatteryLevel(Integer l) { this.batteryLevel = l; }
public Integer getConnectedPenCount() { return connectedPenCount; }
public void setConnectedPenCount(Integer c) { this.connectedPenCount = c; }
public Double getCpuUsage() { return cpuUsage; }
public void setCpuUsage(Double u) { this.cpuUsage = u; }
public Double getMemoryUsage() { return memoryUsage; }
public void setMemoryUsage(Double u) { this.memoryUsage = u; }
}
/** 设备详情响应 */
public static class DeviceDetailResponse {
private String deviceId;
private String type;
private String macAddr;
private String serialNumber;
private String firmwareVersion;
private int status;
private String bindUserId;
private String schoolId;
private String classroomId;
private Integer batteryLevel;
private LocalDateTime lastHeartbeat;
private LocalDateTime registerTime;
public String getDeviceId() { return deviceId; }
public void setDeviceId(String id) { this.deviceId = id; }
public String getType() { return type; }
public void setType(String t) { this.type = t; }
public String getMacAddr() { return macAddr; }
public void setMacAddr(String m) { this.macAddr = m; }
public String getSerialNumber() { return serialNumber; }
public void setSerialNumber(String s) { this.serialNumber = s; }
public String getFirmwareVersion() { return firmwareVersion; }
public void setFirmwareVersion(String v) { this.firmwareVersion = v; }
public int getStatus() { return status; }
public void setStatus(int s) { this.status = s; }
public String getBindUserId() { return bindUserId; }
public void setBindUserId(String id) { this.bindUserId = id; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getClassroomId() { return classroomId; }
public void setClassroomId(String id) { this.classroomId = id; }
public Integer getBatteryLevel() { return batteryLevel; }
public void setBatteryLevel(Integer l) { this.batteryLevel = l; }
public LocalDateTime getLastHeartbeat() { return lastHeartbeat; }
public void setLastHeartbeat(LocalDateTime t) { this.lastHeartbeat = t; }
public LocalDateTime getRegisterTime() { return registerTime; }
public void setRegisterTime(LocalDateTime t) { this.registerTime = t; }
}
/** 教室拓扑结构 */
public static class ClassroomTopology {
private String classroomId;
private String classroomName;
private List<Device> gateways;
private List<Device> edgeBoxes;
private List<Device> terminals;
private List<Device> pens;
private int totalDeviceCount;
public String getClassroomId() { return classroomId; }
public void setClassroomId(String id) { this.classroomId = id; }
public String getClassroomName() { return classroomName; }
public void setClassroomName(String n) { this.classroomName = n; }
public List<Device> getGateways() { return gateways; }
public void setGateways(List<Device> g) { this.gateways = g; }
public List<Device> getEdgeBoxes() { return edgeBoxes; }
public void setEdgeBoxes(List<Device> e) { this.edgeBoxes = e; }
public List<Device> getTerminals() { return terminals; }
public void setTerminals(List<Device> t) { this.terminals = t; }
public List<Device> getPens() { return pens; }
public void setPens(List<Device> p) { this.pens = p; }
public int getTotalDeviceCount() { return totalDeviceCount; }
public void setTotalDeviceCount(int c) { this.totalDeviceCount = c; }
}
}
```
#### `controller/StrokeController.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 笔迹数据控制器
* 负责笔迹数据的批量上传、查询、回放等接口
* 数据流向:点阵笔 → 网关/算力盒 → Kafka → 云平台 → MongoDB
*/
package com.writech.cloud.controller;
import com.writech.cloud.WritechCloudApplication.ApiResponse;
import com.writech.cloud.WritechCloudApplication.BusinessException;
import com.writech.cloud.model.StrokeData;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.*;
/**
* 笔迹控制器 - /api/v1/stroke
*
* 处理智能点阵笔采集的原始笔迹数据,包括:
* - 实时笔迹坐标上传(x, y, pressure, timestamp
* - 批量笔迹数据上传
* - 笔迹回放数据查询
* - 笔迹统计信息
*/
@RestController
@RequestMapping("/api/v1/stroke")
public class StrokeController {
/**
* 批量上传笔迹数据
* POST /api/v1/stroke/upload
*
* 网关或算力盒将采集到的笔迹数据批量上传至云平台
* 数据经过Kafka消息队列异步写入MongoDB存储
* 同时触发AI引擎进行OCR识别和批改
*
* @param request 笔迹上传请求(包含多条笔迹数据)
* @return 上传结果(接收条数、处理状态)
*/
@PostMapping("/upload")
public ApiResponse<StrokeUploadResponse> uploadStrokes(
@Valid @RequestBody StrokeUploadRequest request) {
// 校验数据完整性
if (request.getStrokes() == null || request.getStrokes().isEmpty()) {
throw new BusinessException(400, "笔迹数据不能为空");
}
// 校验每条笔迹数据的有效性
int validCount = 0;
int invalidCount = 0;
List<String> errors = new ArrayList<>();
for (StrokeItem stroke : request.getStrokes()) {
if (validateStrokeItem(stroke)) {
validCount++;
} else {
invalidCount++;
errors.add("无效笔迹数据, penId=" + stroke.getPenId()
+ ", timestamp=" + stroke.getTimestamp());
}
}
// 将有效数据发送至Kafka消息队列
// kafkaTemplate.send("writech-stroke-topic", request);
// 构建响应
StrokeUploadResponse response = new StrokeUploadResponse();
response.setReceivedCount(request.getStrokes().size());
response.setValidCount(validCount);
response.setInvalidCount(invalidCount);
response.setErrors(errors);
response.setProcessingStatus("queued"); // queued/processing/completed
response.setUploadTime(LocalDateTime.now());
return ApiResponse.success(response);
}
/**
* 查询学生笔迹数据
* GET /api/v1/stroke/query
*
* 按学生ID、作业ID、时间范围查询笔迹数据
* 支持笔迹回放场景
*/
@GetMapping("/query")
public ApiResponse<StrokeQueryResponse> queryStrokes(
@RequestParam String studentId,
@RequestParam(required = false) String assignmentId,
@RequestParam(required = false) String pageId,
@RequestParam(required = false) String startTime,
@RequestParam(required = false) String endTime,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "100") int size) {
StrokeQueryResponse response = new StrokeQueryResponse();
response.setStudentId(studentId);
response.setTotalStrokes(0);
response.setStrokes(new ArrayList<>());
// strokeDataService.queryStrokes(studentId, assignmentId, ...)
return ApiResponse.success(response);
}
/**
* 获取笔迹回放数据
* GET /api/v1/stroke/replay/{assignmentId}/{studentId}
*
* 获取指定学生某次作业的完整笔迹回放数据
* 按时间戳排序,支持前端动画回放
*/
@GetMapping("/replay/{assignmentId}/{studentId}")
public ApiResponse<StrokeReplayResponse> getReplayData(
@PathVariable String assignmentId,
@PathVariable String studentId) {
StrokeReplayResponse response = new StrokeReplayResponse();
response.setAssignmentId(assignmentId);
response.setStudentId(studentId);
response.setTotalDuration(0L);
response.setTotalPoints(0);
response.setPages(new ArrayList<>());
return ApiResponse.success(response);
}
/**
* 获取笔迹统计信息
* GET /api/v1/stroke/statistics
*
* 查询指定维度的笔迹统计数据(书写量、书写时长等)
*/
@GetMapping("/statistics")
public ApiResponse<StrokeStatistics> getStatistics(
@RequestParam(required = false) String studentId,
@RequestParam(required = false) String classId,
@RequestParam(required = false) String dateRange) {
StrokeStatistics stats = new StrokeStatistics();
stats.setTotalStrokes(12580);
stats.setTotalPoints(1536000);
stats.setTotalWritingTime(186400L); // 秒
stats.setAverageSpeed(8.5); // 每秒点数
stats.setTotalPages(325);
return ApiResponse.success(stats);
}
// ==================== 内部方法 ====================
/** 校验单条笔迹数据有效性 */
private boolean validateStrokeItem(StrokeItem stroke) {
if (stroke.getPenId() == null || stroke.getPenId().isEmpty()) return false;
if (stroke.getPoints() == null || stroke.getPoints().isEmpty()) return false;
// 校验坐标范围(点阵码坐标范围)
for (StrokePoint point : stroke.getPoints()) {
if (point.getX() < 0 || point.getX() > 65535) return false;
if (point.getY() < 0 || point.getY() > 65535) return false;
if (point.getPressure() < 0 || point.getPressure() > 255) return false;
}
return true;
}
// ==================== DTO 定义 ====================
/** 笔迹上传请求 */
public static class StrokeUploadRequest {
@NotBlank private String gatewayId;
private String classroomId;
@NotNull private List<StrokeItem> strokes;
public String getGatewayId() { return gatewayId; }
public void setGatewayId(String id) { this.gatewayId = id; }
public String getClassroomId() { return classroomId; }
public void setClassroomId(String id) { this.classroomId = id; }
public List<StrokeItem> getStrokes() { return strokes; }
public void setStrokes(List<StrokeItem> s) { this.strokes = s; }
}
/** 单条笔迹数据 */
public static class StrokeItem {
private String penId; // 笔MAC地址
private String studentId; // 绑定学生ID
private String pageId; // 点阵码页面ID
private String assignmentId; // 关联作业ID
private long timestamp; // 起始时间戳
private List<StrokePoint> points; // 坐标点集合
public String getPenId() { return penId; }
public void setPenId(String id) { this.penId = id; }
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public String getPageId() { return pageId; }
public void setPageId(String id) { this.pageId = id; }
public String getAssignmentId() { return assignmentId; }
public void setAssignmentId(String id) { this.assignmentId = id; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long t) { this.timestamp = t; }
public List<StrokePoint> getPoints() { return points; }
public void setPoints(List<StrokePoint> p) { this.points = p; }
}
/** 笔迹坐标点 */
public static class StrokePoint {
private int x; // X坐标 (0-65535)
private int y; // Y坐标 (0-65535)
private int pressure; // 压力值 (0-255)
private long timestamp; // 时间戳(毫秒)
private boolean penUp; // 抬笔标记
public int getX() { return x; }
public void setX(int x) { this.x = x; }
public int getY() { return y; }
public void setY(int y) { this.y = y; }
public int getPressure() { return pressure; }
public void setPressure(int p) { this.pressure = p; }
public long getTimestamp() { return timestamp; }
public void setTimestamp(long t) { this.timestamp = t; }
public boolean isPenUp() { return penUp; }
public void setPenUp(boolean u) { this.penUp = u; }
}
/** 上传响应 */
public static class StrokeUploadResponse {
private int receivedCount;
private int validCount;
private int invalidCount;
private List<String> errors;
private String processingStatus;
private LocalDateTime uploadTime;
public int getReceivedCount() { return receivedCount; }
public void setReceivedCount(int c) { this.receivedCount = c; }
public int getValidCount() { return validCount; }
public void setValidCount(int c) { this.validCount = c; }
public int getInvalidCount() { return invalidCount; }
public void setInvalidCount(int c) { this.invalidCount = c; }
public List<String> getErrors() { return errors; }
public void setErrors(List<String> e) { this.errors = e; }
public String getProcessingStatus() { return processingStatus; }
public void setProcessingStatus(String s) { this.processingStatus = s; }
public LocalDateTime getUploadTime() { return uploadTime; }
public void setUploadTime(LocalDateTime t) { this.uploadTime = t; }
}
/** 查询响应 */
public static class StrokeQueryResponse {
private String studentId;
private int totalStrokes;
private List<StrokeItem> strokes;
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public int getTotalStrokes() { return totalStrokes; }
public void setTotalStrokes(int c) { this.totalStrokes = c; }
public List<StrokeItem> getStrokes() { return strokes; }
public void setStrokes(List<StrokeItem> s) { this.strokes = s; }
}
/** 回放响应 */
public static class StrokeReplayResponse {
private String assignmentId;
private String studentId;
private long totalDuration; // 总时长(毫秒)
private int totalPoints; // 总坐标点数
private List<PageReplay> pages; // 按页面分组的笔迹数据
public String getAssignmentId() { return assignmentId; }
public void setAssignmentId(String id) { this.assignmentId = id; }
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public long getTotalDuration() { return totalDuration; }
public void setTotalDuration(long d) { this.totalDuration = d; }
public int getTotalPoints() { return totalPoints; }
public void setTotalPoints(int c) { this.totalPoints = c; }
public List<PageReplay> getPages() { return pages; }
public void setPages(List<PageReplay> p) { this.pages = p; }
}
/** 页面回放数据 */
public static class PageReplay {
private String pageId;
private int pageWidth;
private int pageHeight;
private List<StrokeItem> strokes;
public String getPageId() { return pageId; }
public void setPageId(String id) { this.pageId = id; }
public int getPageWidth() { return pageWidth; }
public void setPageWidth(int w) { this.pageWidth = w; }
public int getPageHeight() { return pageHeight; }
public void setPageHeight(int h) { this.pageHeight = h; }
public List<StrokeItem> getStrokes() { return strokes; }
public void setStrokes(List<StrokeItem> s) { this.strokes = s; }
}
/** 笔迹统计 */
public static class StrokeStatistics {
private int totalStrokes;
private long totalPoints;
private long totalWritingTime; // 秒
private double averageSpeed;
private int totalPages;
public int getTotalStrokes() { return totalStrokes; }
public void setTotalStrokes(int c) { this.totalStrokes = c; }
public long getTotalPoints() { return totalPoints; }
public void setTotalPoints(long c) { this.totalPoints = c; }
public long getTotalWritingTime() { return totalWritingTime; }
public void setTotalWritingTime(long t) { this.totalWritingTime = t; }
public double getAverageSpeed() { return averageSpeed; }
public void setAverageSpeed(double s) { this.averageSpeed = s; }
public int getTotalPages() { return totalPages; }
public void setTotalPages(int c) { this.totalPages = c; }
}
}
```
### `model/`
#### `model/Models.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 数据模型 - 设备实体 / 作业实体 / 笔迹数据实体
* 设备表(device)MySQL
* 作业表(assignment)MySQL
* 笔迹数据(stroke_data)MongoDB
*/
package com.writech.cloud.model;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.*;
// ==================== 设备实体 ====================
/**
* 设备注册表实体(MySQL
* 管理点阵笔、网关、终端设备、算力盒
*/
@Entity
@Table(name = "device", indexes = {
@Index(name = "idx_mac", columnList = "macAddr", unique = true),
@Index(name = "idx_school_type", columnList = "schoolId, type"),
@Index(name = "idx_classroom", columnList = "classroomId")
})
class Device {
@Id
@Column(length = 32)
private String id;
/** 设备类型:pen/gateway/terminal/edge_box */
@Column(nullable = false, length = 16)
private String type;
/** 设备MAC地址(全局唯一) */
@Column(nullable = false, length = 17, unique = true)
private String macAddr;
/** 设备序列号 */
@Column(length = 32)
private String serialNumber;
/** 固件版本号 */
@Column(length = 16)
private String firmwareVersion;
/** 绑定用户ID */
@Column(length = 32)
private String bindUserId;
/** 所属学校ID */
@Column(length = 32)
private String schoolId;
/** 所属教室ID */
@Column(length = 32)
private String classroomId;
/** 设备状态:1=在线, 0=离线, -1=故障 */
@Column(nullable = false)
private int status = 0;
/** 电池电量百分比(0-100,仅笔设备) */
private Integer batteryLevel;
/** 当前连接的笔数量(仅网关设备) */
private Integer connectedPenCount;
/** CPU使用率(仅网关/算力盒) */
private Double cpuUsage;
/** 内存使用率(仅网关/算力盒) */
private Double memoryUsage;
/** 注册时间 */
@Column(nullable = false)
private LocalDateTime registerTime;
/** 最后心跳时间 */
private LocalDateTime lastHeartbeat;
// Getter/Setter
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getMacAddr() { return macAddr; }
public void setMacAddr(String macAddr) { this.macAddr = macAddr; }
public String getSerialNumber() { return serialNumber; }
public void setSerialNumber(String sn) { this.serialNumber = sn; }
public String getFirmwareVersion() { return firmwareVersion; }
public void setFirmwareVersion(String v) { this.firmwareVersion = v; }
public String getBindUserId() { return bindUserId; }
public void setBindUserId(String id) { this.bindUserId = id; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getClassroomId() { return classroomId; }
public void setClassroomId(String id) { this.classroomId = id; }
public int getStatus() { return status; }
public void setStatus(int s) { this.status = s; }
public Integer getBatteryLevel() { return batteryLevel; }
public void setBatteryLevel(Integer l) { this.batteryLevel = l; }
public Integer getConnectedPenCount() { return connectedPenCount; }
public void setConnectedPenCount(Integer c) { this.connectedPenCount = c; }
public Double getCpuUsage() { return cpuUsage; }
public void setCpuUsage(Double u) { this.cpuUsage = u; }
public Double getMemoryUsage() { return memoryUsage; }
public void setMemoryUsage(Double u) { this.memoryUsage = u; }
public LocalDateTime getRegisterTime() { return registerTime; }
public void setRegisterTime(LocalDateTime t) { this.registerTime = t; }
public LocalDateTime getLastHeartbeat() { return lastHeartbeat; }
public void setLastHeartbeat(LocalDateTime t) { this.lastHeartbeat = t; }
}
// ==================== 作业实体 ====================
/**
* 作业/试卷发布表实体(MySQL)
*/
@Entity
@Table(name = "assignment", indexes = {
@Index(name = "idx_class_status", columnList = "classId, status"),
@Index(name = "idx_teacher", columnList = "teacherId")
})
class Assignment {
@Id
@Column(length = 32)
private String id;
/** 发布教师ID */
@Column(nullable = false, length = 32)
private String teacherId;
/** 班级ID */
@Column(nullable = false, length = 32)
private String classId;
/** 作业标题 */
@Column(nullable = false, length = 128)
private String title;
/** 类型:homework(作业)/exam(考试)/practice(练习) */
@Column(nullable = false, length = 16)
private String type;
/** 学科 */
@Column(length = 32)
private String subject;
/** 截止时间 */
private LocalDateTime deadline;
/** 状态:draft/published/closed/graded */
@Column(nullable = false, length = 16)
private String status;
/** 发布时间 */
private LocalDateTime publishTime;
/** 满分值 */
private double totalScore;
/** 题目总数 */
private int questionCount;
/** 关联的点阵码页面ID列表(JSON数组) */
@Column(columnDefinition = "TEXT")
private String dotCodePagesJson;
@Transient
private List<String> dotCodePages;
// Getter/Setter
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getTeacherId() { return teacherId; }
public void setTeacherId(String id) { this.teacherId = id; }
public String getClassId() { return classId; }
public void setClassId(String id) { this.classId = id; }
public String getTitle() { return title; }
public void setTitle(String t) { this.title = t; }
public String getType() { return type; }
public void setType(String t) { this.type = t; }
public String getSubject() { return subject; }
public void setSubject(String s) { this.subject = s; }
public LocalDateTime getDeadline() { return deadline; }
public void setDeadline(LocalDateTime d) { this.deadline = d; }
public String getStatus() { return status; }
public void setStatus(String s) { this.status = s; }
public LocalDateTime getPublishTime() { return publishTime; }
public void setPublishTime(LocalDateTime t) { this.publishTime = t; }
public double getTotalScore() { return totalScore; }
public void setTotalScore(double s) { this.totalScore = s; }
public int getQuestionCount() { return questionCount; }
public void setQuestionCount(int c) { this.questionCount = c; }
public List<String> getDotCodePages() { return dotCodePages; }
public void setDotCodePages(List<String> p) { this.dotCodePages = p; }
}
// ==================== 笔迹数据实体 ====================
/**
* 笔迹原始数据实体(MongoDB)
*
* JSON文档结构:
* {
* student_id: "...",
* assignment_id: "...",
* pen_id: "...",
* page_id: "...",
* strokes: [{x, y, pressure, timestamp, penUp}, ...],
* createTime: "...",
* processingStatus: "received/processing/completed/failed"
* }
*/
class StrokeData {
private String id;
private String studentId;
private String assignmentId;
private String penId;
private String pageId;
private List<Map<String, Object>> strokes;
private LocalDateTime createTime;
private LocalDateTime processedTime;
private String processingStatus; // received/processing/completed/failed
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getStudentId() { return studentId; }
public void setStudentId(String id) { this.studentId = id; }
public String getAssignmentId() { return assignmentId; }
public void setAssignmentId(String id) { this.assignmentId = id; }
public String getPenId() { return penId; }
public void setPenId(String id) { this.penId = id; }
public String getPageId() { return pageId; }
public void setPageId(String id) { this.pageId = id; }
public List<Map<String, Object>> getStrokes() { return strokes; }
public void setStrokes(List<Map<String, Object>> s) { this.strokes = s; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime t) { this.createTime = t; }
public LocalDateTime getProcessedTime() { return processedTime; }
public void setProcessedTime(LocalDateTime t) { this.processedTime = t; }
public String getProcessingStatus() { return processingStatus; }
public void setProcessingStatus(String s) { this.processingStatus = s; }
}
```
#### `model/User.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 数据模型 - 用户实体
* 对应数据表:user (MySQL)
* 支持教师/学生/管理员/家长四种角色
*/
package com.writech.cloud.model;
import javax.persistence.*;
import java.time.LocalDateTime;
/**
* 用户主表实体类
*
* RBAC角色定义:
* - admin:系统管理员(学校/用户/设备管理全权限)
* - teacher:教师(班级管理/作业发布/学情查看)
* - student:学生(作业查看/学习数据查询)
* - parent:家长(子女学情查看/消息接收)
*
* 安全设计:
* - 手机号使用AES-256加密存储(encryptedPhone字段)
* - 密码使用BCrypt哈希存储
* - 身份证号等敏感信息加密后存储
*/
@Entity
@Table(name = "user", indexes = {
@Index(name = "idx_phone", columnList = "encryptedPhone"),
@Index(name = "idx_school_role", columnList = "schoolId, role"),
@Index(name = "idx_wechat", columnList = "wechatOpenId")
})
public class User {
/** 用户唯一IDUUID格式) */
@Id
@Column(length = 32)
private String id;
/** 用户姓名 */
@Column(nullable = false, length = 64)
private String name;
/** 手机号(明文,仅用于内部处理,不直接存储) */
@Transient
private String phone;
/** 加密后的手机号(AES-256-CBC加密存储) */
@Column(length = 128)
private String encryptedPhone;
/** 密码哈希(BCrypt,强度因子10) */
@Column(length = 128)
private String passwordHash;
/** 用户角色:admin/teacher/student/parent */
@Column(nullable = false, length = 16)
private String role;
/** 所属学校ID */
@Column(length = 32)
private String schoolId;
/** 所属学校名称(冗余存储,减少关联查询) */
@Column(length = 128)
private String schoolName;
/** 头像URL */
@Column(length = 256)
private String avatar;
/** 微信OpenID(第三方登录绑定) */
@Column(length = 64)
private String wechatOpenId;
/** 钉钉用户ID(第三方登录绑定) */
@Column(length = 64)
private String dingtalkUserId;
/** 账户状态:1=正常, 0=禁用, -1=注销 */
@Column(nullable = false)
private int status = 1;
/** Token版本号(用于使所有旧Token失效) */
@Column(nullable = false)
private int tokenVersion = 0;
/** 账户创建时间 */
@Column(nullable = false)
private LocalDateTime createTime;
/** 最后登录时间 */
private LocalDateTime lastLoginTime;
/** 最后登录IP */
@Column(length = 45)
private String lastLoginIp;
// ==================== Getter / Setter ====================
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getEncryptedPhone() { return encryptedPhone; }
public void setEncryptedPhone(String encryptedPhone) { this.encryptedPhone = encryptedPhone; }
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) { this.passwordHash = passwordHash; }
public String getRole() { return role; }
public void setRole(String role) { this.role = role; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String schoolId) { this.schoolId = schoolId; }
public String getSchoolName() { return schoolName; }
public void setSchoolName(String schoolName) { this.schoolName = schoolName; }
public String getAvatar() { return avatar; }
public void setAvatar(String avatar) { this.avatar = avatar; }
public String getWechatOpenId() { return wechatOpenId; }
public void setWechatOpenId(String wechatOpenId) { this.wechatOpenId = wechatOpenId; }
public String getDingtalkUserId() { return dingtalkUserId; }
public void setDingtalkUserId(String dingtalkUserId) { this.dingtalkUserId = dingtalkUserId; }
public int getStatus() { return status; }
public void setStatus(int status) { this.status = status; }
public int getTokenVersion() { return tokenVersion; }
public void setTokenVersion(int tokenVersion) { this.tokenVersion = tokenVersion; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getLastLoginTime() { return lastLoginTime; }
public void setLastLoginTime(LocalDateTime lastLoginTime) { this.lastLoginTime = lastLoginTime; }
public String getLastLoginIp() { return lastLoginIp; }
public void setLastLoginIp(String lastLoginIp) { this.lastLoginIp = lastLoginIp; }
@Override
public String toString() {
return "User{id='" + id + "', name='" + name + "', role='" + role
+ "', schoolId='" + schoolId + "', status=" + status + "}";
}
}
```
### `service/`
#### `service/DeviceService.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 设备管理服务
* 管理点阵笔、网关、终端设备、算力盒的全生命周期
*/
package com.writech.cloud.service;
import com.writech.cloud.model.Device;
import com.writech.cloud.controller.DeviceController.ClassroomTopology;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
/**
* 设备服务类
*
* 管理互动课堂中所有硬件设备的注册、绑定、状态监控
* 设备类型:pen(点阵笔) / gateway(网关) / terminal(终端) / edge_box(算力盒)
*/
@Service
public class DeviceService {
@Autowired
private StringRedisTemplate redisTemplate;
/** 设备在线超时时间(秒),超过此时间未收到心跳视为离线 */
private static final long DEVICE_ONLINE_TIMEOUT = 120;
/** 网关设备心跳间隔(秒) */
private static final long GATEWAY_HEARTBEAT_INTERVAL = 30;
/** 笔设备心跳间隔(秒) */
private static final long PEN_HEARTBEAT_INTERVAL = 300;
/**
* 保存设备信息
*/
@Transactional
public void save(Device device) {
// deviceRepository.save(device);
// 更新Redis中的设备在线状态缓存
updateDeviceOnlineStatus(device.getId(), true);
}
/**
* 根据ID查询设备
*/
public Device findById(String deviceId) {
// return deviceRepository.findById(deviceId).orElse(null);
return null;
}
/**
* 根据MAC地址查询设备
*/
public Device findByMacAddr(String macAddr) {
// return deviceRepository.findByMacAddr(macAddr);
return null;
}
/**
* 校验设备证书(X.509)
* 首次注册时网关设备需提供预置的设备证书进行身份校验
*
* @param macAddr MAC地址
* @param certPem PEM格式的X.509证书
* @return 校验通过返回true
*/
public boolean validateDeviceCertificate(String macAddr, String certPem) {
if (certPem == null || certPem.isEmpty()) {
return false;
}
try {
// 解析X.509证书
java.security.cert.CertificateFactory cf =
java.security.cert.CertificateFactory.getInstance("X.509");
java.io.ByteArrayInputStream bis =
new java.io.ByteArrayInputStream(certPem.getBytes());
X509Certificate cert = (X509Certificate) cf.generateCertificate(bis);
// 检查证书有效期
cert.checkValidity();
// 验证证书签名(使用CA根证书公钥)
// cert.verify(caCertificate.getPublicKey());
// 从证书CN字段提取MAC地址,与请求中的MAC地址比对
String cn = cert.getSubjectX500Principal().getName();
if (!cn.contains(macAddr.replace(":", "").toUpperCase())) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
/**
* 设备绑定
* 将设备绑定至指定用户和教室
*/
@Transactional
public void bindDevice(String deviceId, String userId, String classroomId) {
// deviceRepository.updateBinding(deviceId, userId, classroomId);
}
/**
* 设备解绑
*/
@Transactional
public void unbindDevice(String deviceId) {
// deviceRepository.clearBinding(deviceId);
}
/**
* 分页查询设备列表
* 支持按学校、教室、类型、状态多维度过滤
*/
public Page<Device> queryDevices(String schoolId, String classroomId,
String deviceType, Integer status,
Pageable pageable) {
// return deviceRepository.queryByConditions(schoolId, classroomId,
// deviceType, status, pageable);
return null;
}
/**
* 更新设备心跳
* 心跳数据写入MySQL并更新Redis在线状态缓存
*/
public void updateHeartbeat(Device device) {
// deviceRepository.updateHeartbeat(device.getId(),
// device.getLastHeartbeat(), device.getBatteryLevel(),
// device.getConnectedPenCount(), device.getCpuUsage(),
// device.getMemoryUsage());
// 更新Redis在线状态(设置过期时间为心跳超时时间)
updateDeviceOnlineStatus(device.getId(), true);
}
/**
* 构建教室设备拓扑
* 查询教室内所有设备,按类型分组并建立连接关系
*
* @param classroomId 教室ID
* @return 拓扑结构(网关/算力盒/终端/笔)
*/
public ClassroomTopology buildClassroomTopology(String classroomId) {
// 查询教室下所有设备
// List<Device> devices = deviceRepository.findByClassroomId(classroomId);
List<Device> devices = new ArrayList<>();
ClassroomTopology topology = new ClassroomTopology();
topology.setClassroomId(classroomId);
// 按设备类型分组
Map<String, List<Device>> grouped = devices.stream()
.collect(Collectors.groupingBy(Device::getType));
topology.setGateways(grouped.getOrDefault("gateway", new ArrayList<>()));
topology.setEdgeBoxes(grouped.getOrDefault("edge_box", new ArrayList<>()));
topology.setTerminals(grouped.getOrDefault("terminal", new ArrayList<>()));
topology.setPens(grouped.getOrDefault("pen", new ArrayList<>()));
topology.setTotalDeviceCount(devices.size());
return topology;
}
/**
* 批量检查设备在线状态
* 通过Redis缓存快速判断设备是否在线
*/
public Map<String, Boolean> checkOnlineStatus(List<String> deviceIds) {
Map<String, Boolean> result = new HashMap<>();
for (String deviceId : deviceIds) {
String key = "writech:device:online:" + deviceId;
result.put(deviceId, Boolean.TRUE.equals(redisTemplate.hasKey(key)));
}
return result;
}
/**
* 发送远程指令至设备
* 通过MQTT向指定设备下发控制指令(重启/配置更新/OTA等)
*/
public void sendCommand(String deviceId, String command, Map<String, Object> params) {
// 构建MQTT消息
Map<String, Object> message = new HashMap<>();
message.put("command", command);
message.put("params", params);
message.put("timestamp", System.currentTimeMillis());
// 根据设备类型确定Topic
Device device = findById(deviceId);
if (device == null) return;
String topic;
switch (device.getType()) {
case "gateway":
topic = "gateway/" + deviceId + "/command";
break;
case "edge_box":
topic = "edgebox/" + deviceId + "/command";
break;
default:
topic = "device/" + deviceId + "/command";
}
// mqttTemplate.convertAndSend(topic, message);
}
/**
* 统计学校设备概况
*/
public DeviceOverview getSchoolDeviceOverview(String schoolId) {
DeviceOverview overview = new DeviceOverview();
// 各类型设备数量统计
// overview.setTotalPens(deviceRepository.countBySchoolAndType(schoolId, "pen"));
// overview.setTotalGateways(deviceRepository.countBySchoolAndType(schoolId, "gateway"));
// overview.setOnlinePens(countOnlineDevices(schoolId, "pen"));
// overview.setOnlineGateways(countOnlineDevices(schoolId, "gateway"));
return overview;
}
// ==================== 内部方法 ====================
/** 更新Redis中设备在线状态 */
private void updateDeviceOnlineStatus(String deviceId, boolean online) {
String key = "writech:device:online:" + deviceId;
if (online) {
redisTemplate.opsForValue().set(key, "1",
DEVICE_ONLINE_TIMEOUT, java.util.concurrent.TimeUnit.SECONDS);
} else {
redisTemplate.delete(key);
}
}
// ==================== 内部类 ====================
/** 设备概况统计 */
public static class DeviceOverview {
private int totalPens;
private int totalGateways;
private int totalEdgeBoxes;
private int totalTerminals;
private int onlinePens;
private int onlineGateways;
private int onlineEdgeBoxes;
private double averageBatteryLevel;
public int getTotalPens() { return totalPens; }
public void setTotalPens(int c) { this.totalPens = c; }
public int getTotalGateways() { return totalGateways; }
public void setTotalGateways(int c) { this.totalGateways = c; }
public int getTotalEdgeBoxes() { return totalEdgeBoxes; }
public void setTotalEdgeBoxes(int c) { this.totalEdgeBoxes = c; }
public int getTotalTerminals() { return totalTerminals; }
public void setTotalTerminals(int c) { this.totalTerminals = c; }
public int getOnlinePens() { return onlinePens; }
public void setOnlinePens(int c) { this.onlinePens = c; }
public int getOnlineGateways() { return onlineGateways; }
public void setOnlineGateways(int c) { this.onlineGateways = c; }
public int getOnlineEdgeBoxes() { return onlineEdgeBoxes; }
public void setOnlineEdgeBoxes(int c) { this.onlineEdgeBoxes = c; }
public double getAverageBatteryLevel() { return averageBatteryLevel; }
public void setAverageBatteryLevel(double l) { this.averageBatteryLevel = l; }
}
}
```
#### `service/MessageService.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 消息推送服务
* 基于 WebSocket 实现多终端实时消息推送
* 支持新作业通知、批改完成通知、课堂互动指令等
*/
package com.writech.cloud.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import org.springframework.web.socket.config.annotation.*;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 消息服务类
*
* WebSocket实时消息通道:/ws/v1/notify
*
* 消息类型:
* - ASSIGNMENT_NEW:新作业通知
* - ASSIGNMENT_GRADED:批改完成通知
* - STROKE_REALTIME:实时笔迹数据推送
* - CLASSROOM_INTERACTION:课堂互动指令
* - SYSTEM_NOTIFICATION:系统公告
*/
@Service
public class MessageService extends TextWebSocketHandler implements WebSocketConfigurer {
@Autowired
private StringRedisTemplate redisTemplate;
/** 在线用户WebSocket会话映射(userId → session列表,支持多终端同时在线) */
private final ConcurrentHashMap<String, List<WebSocketSession>> userSessions =
new ConcurrentHashMap<>();
/** 教室频道会话映射(classroomId → session列表) */
private final ConcurrentHashMap<String, List<WebSocketSession>> classroomChannels =
new ConcurrentHashMap<>();
/**
* WebSocket端点注册
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(this, "/ws/v1/notify")
.setAllowedOrigins("*");
}
/**
* WebSocket连接建立
* 从Token中解析用户ID,注册到在线会话映射
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String userId = extractUserIdFromSession(session);
if (userId != null) {
// 注册用户会话
userSessions.computeIfAbsent(userId, k -> new ArrayList<>()).add(session);
// 更新在线状态
updateOnlineStatus(userId, true);
// 推送离线期间的未读消息
pushOfflineMessages(userId, session);
}
}
/**
* WebSocket消息接收
* 处理客户端发送的消息(心跳、课堂互动指令等)
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
String payload = message.getPayload();
Map<String, Object> msg = parseMessage(payload);
String type = (String) msg.get("type");
if (type == null) return;
switch (type) {
case "HEARTBEAT":
// 回复心跳
session.sendMessage(new TextMessage("{\"type\":\"HEARTBEAT_ACK\"}"));
break;
case "JOIN_CLASSROOM":
// 加入教室频道(课堂互动场景)
String classroomId = (String) msg.get("classroomId");
joinClassroomChannel(classroomId, session);
break;
case "LEAVE_CLASSROOM":
// 离开教室频道
String leaveClassroom = (String) msg.get("classroomId");
leaveClassroomChannel(leaveClassroom, session);
break;
case "CLASSROOM_COMMAND":
// 教师发送课堂控制指令(广播至教室内所有终端)
broadcastToClassroom(msg);
break;
default:
break;
}
}
/**
* WebSocket连接断开
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status)
throws Exception {
String userId = extractUserIdFromSession(session);
if (userId != null) {
// 移除会话
List<WebSocketSession> sessions = userSessions.get(userId);
if (sessions != null) {
sessions.remove(session);
if (sessions.isEmpty()) {
userSessions.remove(userId);
updateOnlineStatus(userId, false);
}
}
}
// 从教室频道移除
classroomChannels.values().forEach(list -> list.remove(session));
}
/**
* 向指定用户推送消息
* 支持多终端同时推送(手机/Pad/PC同时在线时都能收到)
*
* @param userId 目标用户ID
* @param messageType 消息类型
* @param data 消息数据
*/
public void pushToUser(String userId, String messageType, Map<String, Object> data) {
Map<String, Object> message = new HashMap<>();
message.put("type", messageType);
message.put("data", data);
message.put("timestamp", System.currentTimeMillis());
String json = toJson(message);
List<WebSocketSession> sessions = userSessions.get(userId);
if (sessions != null && !sessions.isEmpty()) {
// 在线推送
for (WebSocketSession session : sessions) {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(json));
}
} catch (IOException e) {
// 发送失败,记录日志
}
}
} else {
// 离线存储(用户上线后推送)
storeOfflineMessage(userId, json);
}
}
/**
* 向班级所有学生推送消息
*
* @param classId 班级ID
* @param messageType 消息类型
* @param data 消息数据
*/
public void pushToClass(String classId, String messageType, Map<String, Object> data) {
// 查询班级学生列表
// List<String> studentIds = classService.getStudentIds(classId);
List<String> studentIds = new ArrayList<>();
for (String studentId : studentIds) {
pushToUser(studentId, messageType, data);
}
}
/**
* 向教室频道广播消息
* 用于课堂互动场景,将消息推送至教室内所有终端(黑板/PC/电视/Pad)
*/
public void broadcastToClassroom(Map<String, Object> message) {
String classroomId = (String) message.get("classroomId");
if (classroomId == null) return;
String json = toJson(message);
List<WebSocketSession> sessions = classroomChannels.get(classroomId);
if (sessions != null) {
for (WebSocketSession session : sessions) {
try {
if (session.isOpen()) {
session.sendMessage(new TextMessage(json));
}
} catch (IOException e) {
// 发送失败处理
}
}
}
}
/**
* 推送作业发布通知
*/
public void pushAssignmentNotification(String classId, String title, String assignmentId) {
Map<String, Object> data = new HashMap<>();
data.put("assignmentId", assignmentId);
data.put("title", title);
data.put("message", "教师发布了新作业: " + title);
pushToClass(classId, "ASSIGNMENT_NEW", data);
}
/**
* 推送批改完成通知
*/
public void pushGradingNotification(String studentId, String assignmentTitle,
double score) {
Map<String, Object> data = new HashMap<>();
data.put("title", assignmentTitle);
data.put("score", score);
data.put("message", "作业\"" + assignmentTitle + "\"批改完成,得分: " + score);
pushToUser(studentId, "ASSIGNMENT_GRADED", data);
}
/**
* 推送实时笔迹数据至教室大屏
* 低延迟推送,用于黑板/电视大屏实时展示学生书写过程
*/
public void pushRealtimeStroke(String classroomId, String studentId,
List<Map<String, Object>> strokePoints) {
Map<String, Object> data = new HashMap<>();
data.put("studentId", studentId);
data.put("points", strokePoints);
Map<String, Object> message = new HashMap<>();
message.put("type", "STROKE_REALTIME");
message.put("classroomId", classroomId);
message.put("data", data);
broadcastToClassroom(message);
}
// ==================== 内部方法 ====================
/** 加入教室频道 */
private void joinClassroomChannel(String classroomId, WebSocketSession session) {
classroomChannels.computeIfAbsent(classroomId, k -> new ArrayList<>()).add(session);
}
/** 离开教室频道 */
private void leaveClassroomChannel(String classroomId, WebSocketSession session) {
List<WebSocketSession> sessions = classroomChannels.get(classroomId);
if (sessions != null) {
sessions.remove(session);
}
}
/** 从WebSocket会话中提取用户ID */
private String extractUserIdFromSession(WebSocketSession session) {
// 从URL参数或握手头中的Token解析用户ID
String query = session.getUri() != null ? session.getUri().getQuery() : null;
if (query != null && query.contains("token=")) {
// 解析Token获取userId
return "extracted_user_id";
}
return null;
}
/** 更新用户在线状态 */
private void updateOnlineStatus(String userId, boolean online) {
String key = "writech:user:online:" + userId;
if (online) {
redisTemplate.opsForValue().set(key, "1");
} else {
redisTemplate.delete(key);
}
}
/** 存储离线消息 */
private void storeOfflineMessage(String userId, String message) {
String key = "writech:offline:msg:" + userId;
redisTemplate.opsForList().rightPush(key, message);
// 最多保留100条离线消息
redisTemplate.opsForList().trim(key, -100, -1);
}
/** 推送离线期间积累的未读消息 */
private void pushOfflineMessages(String userId, WebSocketSession session)
throws IOException {
String key = "writech:offline:msg:" + userId;
List<String> messages = redisTemplate.opsForList().range(key, 0, -1);
if (messages != null) {
for (String msg : messages) {
session.sendMessage(new TextMessage(msg));
}
redisTemplate.delete(key);
}
}
/** JSON序列化(简化版本) */
private String toJson(Map<String, Object> map) {
StringBuilder sb = new StringBuilder("{");
boolean first = true;
for (Map.Entry<String, Object> entry : map.entrySet()) {
if (!first) sb.append(",");
sb.append("\"").append(entry.getKey()).append("\":");
Object value = entry.getValue();
if (value instanceof String) {
sb.append("\"").append(value).append("\"");
} else {
sb.append(value);
}
first = false;
}
sb.append("}");
return sb.toString();
}
/** JSON解析(简化版本) */
private Map<String, Object> parseMessage(String json) {
return new HashMap<>();
}
/**
* 获取在线用户统计
*/
public Map<String, Integer> getOnlineStats() {
Map<String, Integer> stats = new HashMap<>();
stats.put("totalOnlineUsers", userSessions.size());
stats.put("totalSessions", userSessions.values().stream()
.mapToInt(List::size).sum());
stats.put("activeClassrooms", classroomChannels.size());
return stats;
}
}
```
#### `service/StrokeService.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 笔迹数据处理服务
* 负责笔迹数据的Kafka消费、存储、AI引擎调度
*/
package com.writech.cloud.service;
import com.writech.cloud.model.StrokeData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;
/**
* 笔迹数据服务
*
* 数据流处理管道:
* 1. 网关/算力盒通过MQTT上报笔迹数据到云平台
* 2. 云平台接收服务将数据推入Kafka消息队列
* 3. 本服务作为Kafka消费者接收并处理数据
* 4. 原始笔迹数据存入MongoDB(高写入吞吐量)
* 5. 触发AI引擎异步识别(OCR/数学/笔顺)
* 6. 识别结果回写MongoDB,推送至各终端
*/
@Service
public class StrokeService {
@Autowired
private MongoTemplate mongoTemplate;
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
/** AI引擎调用线程池 */
private final ExecutorService aiExecutor = Executors.newFixedThreadPool(16);
/** AI引擎服务地址 */
private static final String AI_ENGINE_URL = "http://ai-engine-service:8001";
/** 笔迹数据MongoDB集合名 */
private static final String STROKE_COLLECTION = "stroke_data";
/** 识别结果MongoDB集合名 */
private static final String RESULT_COLLECTION = "recognition_result";
/**
* Kafka消费者:接收笔迹数据
* 监听 writech-stroke-topic 主题,批量消费笔迹数据
*
* @param message JSON格式的笔迹数据
*/
@KafkaListener(topics = "writech-stroke-topic", groupId = "stroke-consumer-group")
public void consumeStrokeData(String message) {
try {
// 解析笔迹数据JSON
StrokeData strokeData = parseStrokeData(message);
if (strokeData == null) return;
// 数据预处理(坐标校验、时间戳排序、去重)
preprocessStrokeData(strokeData);
// 写入MongoDB存储
saveToMongoDB(strokeData);
// 判断是否需要触发AI识别
if (shouldTriggerRecognition(strokeData)) {
// 异步调用AI引擎
submitRecognitionTask(strokeData);
}
} catch (Exception e) {
// 处理失败的消息发送到死信队列
kafkaTemplate.send("writech-stroke-dlq", message);
}
}
/**
* 保存笔迹数据到MongoDB
* 使用批量写入提升性能,每批最多500条
*/
public void saveToMongoDB(StrokeData strokeData) {
strokeData.setCreateTime(LocalDateTime.now());
strokeData.setProcessingStatus("received");
mongoTemplate.save(strokeData, STROKE_COLLECTION);
}
/**
* 批量保存笔迹数据
* 用于网关批量上传场景,提升写入吞吐量
*/
public void batchSave(List<StrokeData> strokeDataList) {
if (strokeDataList == null || strokeDataList.isEmpty()) return;
LocalDateTime now = LocalDateTime.now();
for (StrokeData data : strokeDataList) {
data.setCreateTime(now);
data.setProcessingStatus("received");
}
// MongoDB批量插入
mongoTemplate.insertAll(strokeDataList);
}
/**
* 查询学生笔迹数据
*
* @param studentId 学生ID
* @param assignmentId 作业ID(可选)
* @param startTime 开始时间(可选)
* @param endTime 结束时间(可选)
* @return 笔迹数据列表
*/
public List<StrokeData> queryStrokes(String studentId, String assignmentId,
LocalDateTime startTime, LocalDateTime endTime) {
Query query = new Query();
query.addCriteria(Criteria.where("studentId").is(studentId));
if (assignmentId != null) {
query.addCriteria(Criteria.where("assignmentId").is(assignmentId));
}
if (startTime != null && endTime != null) {
query.addCriteria(Criteria.where("timestamp")
.gte(startTime).lte(endTime));
}
// 按时间戳排序(回放场景需要)
query.with(org.springframework.data.domain.Sort.by(
org.springframework.data.domain.Sort.Direction.ASC, "timestamp"));
return mongoTemplate.find(query, StrokeData.class, STROKE_COLLECTION);
}
/**
* 提交AI识别任务
* 将笔迹数据异步发送至AI引擎进行识别
*/
private void submitRecognitionTask(StrokeData strokeData) {
aiExecutor.submit(() -> {
try {
// 根据作业题目类型选择识别方式
String recognitionType = determineRecognitionType(strokeData);
// 调用AI引擎REST API
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("strokeId", strokeData.getId());
requestBody.put("studentId", strokeData.getStudentId());
requestBody.put("strokes", strokeData.getStrokes());
requestBody.put("type", recognitionType);
// String apiUrl = AI_ENGINE_URL + "/api/v1/ocr/recognize";
// RestTemplate restTemplate = new RestTemplate();
// ResponseEntity<String> response = restTemplate.postForEntity(
// apiUrl, requestBody, String.class);
// 保存识别结果
// saveRecognitionResult(strokeData.getId(), response.getBody());
// 更新笔迹数据处理状态
updateProcessingStatus(strokeData.getId(), "completed");
} catch (Exception e) {
updateProcessingStatus(strokeData.getId(), "failed");
}
});
}
/**
* 笔迹数据预处理
* - 坐标范围校验(过滤异常值)
* - 时间戳排序
* - 重复数据去重
* - 坐标归一化(适配不同纸面规格)
*/
private void preprocessStrokeData(StrokeData strokeData) {
if (strokeData.getStrokes() == null) return;
List<Map<String, Object>> processed = strokeData.getStrokes().stream()
// 过滤无效坐标点
.filter(point -> {
int x = ((Number) point.getOrDefault("x", -1)).intValue();
int y = ((Number) point.getOrDefault("y", -1)).intValue();
return x >= 0 && x <= 65535 && y >= 0 && y <= 65535;
})
// 按时间戳排序
.sorted((a, b) -> {
long ta = ((Number) a.getOrDefault("timestamp", 0L)).longValue();
long tb = ((Number) b.getOrDefault("timestamp", 0L)).longValue();
return Long.compare(ta, tb);
})
.collect(Collectors.toList());
// 去重(相同时间戳的重复点)
List<Map<String, Object>> deduplicated = new ArrayList<>();
long lastTimestamp = -1;
for (Map<String, Object> point : processed) {
long ts = ((Number) point.getOrDefault("timestamp", 0L)).longValue();
if (ts != lastTimestamp) {
deduplicated.add(point);
lastTimestamp = ts;
}
}
strokeData.setStrokes(deduplicated);
}
/**
* 判断是否需要触发AI识别
* - 抬笔事件(笔画结束)触发单字识别
* - 作业提交事件触发整页识别
* - 超过5秒无新数据触发段落识别
*/
private boolean shouldTriggerRecognition(StrokeData strokeData) {
// 如果关联了作业ID,则需要识别
if (strokeData.getAssignmentId() != null) {
return true;
}
// 检查是否有抬笔标记
if (strokeData.getStrokes() != null) {
return strokeData.getStrokes().stream()
.anyMatch(p -> Boolean.TRUE.equals(p.get("penUp")));
}
return false;
}
/** 确定识别类型 */
private String determineRecognitionType(StrokeData strokeData) {
// 根据作业题目类型确定:ocr/math/stroke_order/essay
return "ocr";
}
/** 解析笔迹数据JSON */
private StrokeData parseStrokeData(String json) {
// JSON反序列化
return null;
}
/** 更新处理状态 */
private void updateProcessingStatus(String strokeId, String status) {
Query query = new Query(Criteria.where("_id").is(strokeId));
org.springframework.data.mongodb.core.query.Update update =
new org.springframework.data.mongodb.core.query.Update();
update.set("processingStatus", status);
update.set("processedTime", LocalDateTime.now());
mongoTemplate.updateFirst(query, update, STROKE_COLLECTION);
}
}
```
#### `service/UserService.java`
```java
/**
* 自然写互动课堂教学管理云平台软件 V1.0
*
* 用户与权限服务
* 实现 RBAC 角色权限模型,管理教师/学生/管理员/家长四级权限
*/
package com.writech.cloud.service;
import com.writech.cloud.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 用户服务类
*
* 提供用户管理、身份验证、权限控制、Token管理等核心功能
* RBAC权限模型:管理员 > 教师 > 学生/家长
* - 管理员:系统全局管理(学校/用户/设备管理)
* - 教师:班级管理、作业发布批改、学情查看
* - 学生:作业查看、学习数据查询
* - 家长:子女学情查看、消息接收
*/
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
/** 密码加密器(BCrypt算法,强度因子10) */
private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
/** Token黑名单前缀(存储在Redis中) */
private static final String TOKEN_BLACKLIST_PREFIX = "writech:token:blacklist:";
/** 短信验证码前缀 */
private static final String SMS_CODE_PREFIX = "writech:sms:code:";
/** 验证码有效期(秒) */
private static final long SMS_CODE_EXPIRE = 300;
/** 验证码发送间隔(秒) */
private static final long SMS_CODE_INTERVAL = 60;
/**
* 手机号+密码验证登录
*
* @param phone 手机号
* @param password 明文密码
* @return 验证通过返回用户对象,失败返回null
*/
public User verifyByPassword(String phone, String password) {
if (phone == null || password == null) {
return null;
}
// 查询用户(手机号AES解密后匹配)
User user = findByPhone(phone);
if (user == null) {
return null;
}
// BCrypt密码比对
if (passwordEncoder.matches(password, user.getPasswordHash())) {
return user;
}
// 登录失败计数(防暴力破解,5次失败后锁定30分钟)
incrementLoginFailCount(user.getId());
return null;
}
/**
* 手机号+短信验证码验证登录
*/
public User verifyBySmsCode(String phone, String smsCode) {
if (phone == null || smsCode == null) {
return null;
}
// 从Redis获取验证码
String key = SMS_CODE_PREFIX + phone;
String storedCode = redisTemplate.opsForValue().get(key);
if (storedCode == null || !storedCode.equals(smsCode)) {
return null;
}
// 验证码匹配成功,删除已使用的验证码
redisTemplate.delete(key);
// 查找或自动注册用户
User user = findByPhone(phone);
if (user == null) {
// 首次登录自动创建账户
user = autoRegister(phone);
}
return user;
}
/**
* 微信授权登录验证
*/
public User verifyByWechat(String wechatCode) {
if (wechatCode == null) return null;
// 调用微信开放平台API获取用户openId
String openId = exchangeWechatOpenId(wechatCode);
if (openId == null) return null;
// 查找绑定的用户
User user = findByWechatOpenId(openId);
return user;
}
/**
* 钉钉授权登录验证
*/
public User verifyByDingtalk(String dingtalkCode) {
if (dingtalkCode == null) return null;
String userId = exchangeDingtalkUserId(dingtalkCode);
if (userId == null) return null;
return findByDingtalkUserId(userId);
}
/**
* 发送短信验证码
*
* @param phone 手机号
* @throws RuntimeException 发送频率过高时抛出异常
*/
public void sendSmsVerificationCode(String phone) {
// 检查发送频率(60秒内不可重复发送)
String intervalKey = SMS_CODE_PREFIX + "interval:" + phone;
if (Boolean.TRUE.equals(redisTemplate.hasKey(intervalKey))) {
throw new RuntimeException("验证码发送过于频繁,请60秒后重试");
}
// 生成6位随机验证码
String code = String.format("%06d", new Random().nextInt(1000000));
// 存入Redis5分钟有效期)
String codeKey = SMS_CODE_PREFIX + phone;
redisTemplate.opsForValue().set(codeKey, code, SMS_CODE_EXPIRE, TimeUnit.SECONDS);
// 设置发送间隔标记(60秒)
redisTemplate.opsForValue().set(intervalKey, "1", SMS_CODE_INTERVAL, TimeUnit.SECONDS);
// 调用短信服务发送验证码
sendSms(phone, code);
}
/**
* 查询用户信息
*/
public User findById(String userId) {
// 先查Redis缓存
// User cachedUser = getCachedUser(userId);
// if (cachedUser != null) return cachedUser;
// 查数据库
// User user = userRepository.findById(userId).orElse(null);
// if (user != null) cacheUser(user);
return null;
}
/**
* 根据手机号查询用户
* 手机号在数据库中AES-256加密存储,查询时需加密后匹配
*/
public User findByPhone(String phone) {
String encryptedPhone = encryptField(phone);
// return userRepository.findByEncryptedPhone(encryptedPhone);
return null;
}
/**
* 更新用户登录信息
*/
public void updateLoginInfo(String userId, LocalDateTime loginTime, String loginIp) {
// userRepository.updateLoginInfo(userId, loginTime, loginIp);
}
/**
* 验证密码
*/
public boolean verifyPassword(String userId, String password) {
User user = findById(userId);
if (user == null) return false;
return passwordEncoder.matches(password, user.getPasswordHash());
}
/**
* 更新密码
* 密码使用BCrypt加密后存储,强度因子10
*/
@Transactional
public void updatePassword(String userId, String newPassword) {
// 密码强度校验(最少8位,包含大小写字母和数字)
if (!isStrongPassword(newPassword)) {
throw new RuntimeException("密码强度不足,需包含大小写字母和数字,不少于8位");
}
String passwordHash = passwordEncoder.encode(newPassword);
// userRepository.updatePassword(userId, passwordHash);
}
/**
* 将Token加入黑名单(使其立即失效)
* 黑名单存储在Redis中,有效期与Token过期时间一致
*/
public void invalidateToken(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
redisTemplate.opsForValue().set(key, "1", 7200, TimeUnit.SECONDS);
}
/**
* 使用户所有Token失效(强制重新登录)
*/
public void invalidateAllTokens(String userId) {
// 更新用户tokenVersion字段,旧版本Token将在校验时失效
// userRepository.incrementTokenVersion(userId);
}
/**
* 检查Token是否在黑名单中
*/
public boolean isTokenBlacklisted(String token) {
String key = TOKEN_BLACKLIST_PREFIX + token;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 创建用户
* 管理员创建教师/学生/家长账户
*/
@Transactional
public User createUser(CreateUserRequest request) {
// 检查手机号唯一性
if (request.getPhone() != null && findByPhone(request.getPhone()) != null) {
throw new RuntimeException("手机号已被注册");
}
User user = new User();
user.setId(UUID.randomUUID().toString().replace("-", ""));
user.setName(request.getName());
user.setPhone(request.getPhone());
user.setRole(request.getRole());
user.setSchoolId(request.getSchoolId());
user.setSchoolName(request.getSchoolName());
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
// 加密手机号存储
if (request.getPhone() != null) {
user.setEncryptedPhone(encryptField(request.getPhone()));
}
// 设置初始密码
if (request.getPassword() != null) {
user.setPasswordHash(passwordEncoder.encode(request.getPassword()));
}
// userRepository.save(user);
return user;
}
/**
* 查询学校下的用户列表
* 按角色过滤(教师/学生/家长)
*/
public List<User> findBySchoolAndRole(String schoolId, String role) {
// return userRepository.findBySchoolIdAndRole(schoolId, role);
return new ArrayList<>();
}
// ==================== 内部方法 ====================
/** 自动注册用户(首次短信登录) */
private User autoRegister(String phone) {
User user = new User();
user.setId(UUID.randomUUID().toString().replace("-", ""));
user.setPhone(phone);
user.setEncryptedPhone(encryptField(phone));
user.setRole("parent"); // 默认家长角色
user.setStatus(1);
user.setCreateTime(LocalDateTime.now());
return user;
}
/** 登录失败计数(防暴力破解) */
private void incrementLoginFailCount(String userId) {
String key = "writech:login:fail:" + userId;
Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count == 1) {
redisTemplate.expire(key, 1800, TimeUnit.SECONDS); // 30分钟窗口
}
if (count != null && count >= 5) {
// 锁定账户30分钟
String lockKey = "writech:login:lock:" + userId;
redisTemplate.opsForValue().set(lockKey, "1", 1800, TimeUnit.SECONDS);
}
}
/** AES-256加密字段(手机号、身份信息等敏感数据) */
private String encryptField(String plainText) {
// 使用AES-256-CBC模式加密
// Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// 实际实现使用配置的密钥
return Base64.getEncoder().encodeToString(plainText.getBytes());
}
/** AES-256解密字段 */
private String decryptField(String cipherText) {
return new String(Base64.getDecoder().decode(cipherText));
}
/** 密码强度校验 */
private boolean isStrongPassword(String password) {
if (password == null || password.length() < 8) return false;
boolean hasUpper = false, hasLower = false, hasDigit = false;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) hasUpper = true;
if (Character.isLowerCase(c)) hasLower = true;
if (Character.isDigit(c)) hasDigit = true;
}
return hasUpper && hasLower && hasDigit;
}
/** 微信OpenId获取(模拟) */
private String exchangeWechatOpenId(String code) {
// 调用 https://api.weixin.qq.com/sns/oauth2/access_token
return null;
}
/** 钉钉UserId获取(模拟) */
private String exchangeDingtalkUserId(String code) {
return null;
}
private User findByWechatOpenId(String openId) { return null; }
private User findByDingtalkUserId(String userId) { return null; }
private void sendSms(String phone, String code) { /* 调用短信服务商API */ }
// ==================== 请求 DTO ====================
public static class CreateUserRequest {
private String name;
private String phone;
private String password;
private String role;
private String schoolId;
private String schoolName;
public String getName() { return name; }
public void setName(String n) { this.name = n; }
public String getPhone() { return phone; }
public void setPhone(String p) { this.phone = p; }
public String getPassword() { return password; }
public void setPassword(String p) { this.password = p; }
public String getRole() { return role; }
public void setRole(String r) { this.role = r; }
public String getSchoolId() { return schoolId; }
public void setSchoolId(String id) { this.schoolId = id; }
public String getSchoolName() { return schoolName; }
public void setSchoolName(String n) { this.schoolName = n; }
}
}
```