/** * 自然写互动课堂教学管理云平台软件 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 uploadStrokes( @Valid @RequestBody StrokeUploadRequest request) { // 校验数据完整性 if (request.getStrokes() == null || request.getStrokes().isEmpty()) { throw new BusinessException(400, "笔迹数据不能为空"); } // 校验每条笔迹数据的有效性 int validCount = 0; int invalidCount = 0; List 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 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 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 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 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 getStrokes() { return strokes; } public void setStrokes(List 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 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 getPoints() { return points; } public void setPoints(List 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 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 getErrors() { return errors; } public void setErrors(List 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 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 getStrokes() { return strokes; } public void setStrokes(List s) { this.strokes = s; } } /** 回放响应 */ public static class StrokeReplayResponse { private String assignmentId; private String studentId; private long totalDuration; // 总时长(毫秒) private int totalPoints; // 总坐标点数 private List 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 getPages() { return pages; } public void setPages(List p) { this.pages = p; } } /** 页面回放数据 */ public static class PageReplay { private String pageId; private int pageWidth; private int pageHeight; private List 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 getStrokes() { return strokes; } public void setStrokes(List 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; } } }