software copyright
This commit is contained in:
@@ -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; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user