software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,456 @@
/**
* 自然写互动课堂教学管理云平台软件 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; }
}
}
@@ -0,0 +1,442 @@
/**
* 自然写互动课堂教学管理云平台软件 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; }
}
}
@@ -0,0 +1,391 @@
/**
* 自然写互动课堂教学管理云平台软件 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; }
}
}
@@ -0,0 +1,322 @@
/**
* 自然写互动课堂教学管理云平台软件 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; }
}
}