323 lines
13 KiB
Java
323 lines
13 KiB
Java
/**
|
||
* 自然写互动课堂教学管理云平台软件 V1.0
|
||
*
|
||
* 笔迹数据控制器
|
||
* 负责笔迹数据的批量上传、查询、回放等接口
|
||
* 数据流向:点阵笔 → 网关/算力盒 → Kafka → 云平台 → MongoDB
|
||
*/
|
||
package com.writech.cloud.controller;
|
||
|
||
import com.writech.cloud.WritechCloudApplication.ApiResponse;
|
||
import com.writech.cloud.WritechCloudApplication.BusinessException;
|
||
import com.writech.cloud.model.StrokeData;
|
||
|
||
import org.springframework.web.bind.annotation.*;
|
||
|
||
import javax.validation.Valid;
|
||
import javax.validation.constraints.NotBlank;
|
||
import javax.validation.constraints.NotNull;
|
||
import java.time.LocalDateTime;
|
||
import java.util.*;
|
||
|
||
/**
|
||
* 笔迹控制器 - /api/v1/stroke
|
||
*
|
||
* 处理智能点阵笔采集的原始笔迹数据,包括:
|
||
* - 实时笔迹坐标上传(x, y, pressure, timestamp)
|
||
* - 批量笔迹数据上传
|
||
* - 笔迹回放数据查询
|
||
* - 笔迹统计信息
|
||
*/
|
||
@RestController
|
||
@RequestMapping("/api/v1/stroke")
|
||
public class StrokeController {
|
||
|
||
/**
|
||
* 批量上传笔迹数据
|
||
* POST /api/v1/stroke/upload
|
||
*
|
||
* 网关或算力盒将采集到的笔迹数据批量上传至云平台
|
||
* 数据经过Kafka消息队列异步写入MongoDB存储
|
||
* 同时触发AI引擎进行OCR识别和批改
|
||
*
|
||
* @param request 笔迹上传请求(包含多条笔迹数据)
|
||
* @return 上传结果(接收条数、处理状态)
|
||
*/
|
||
@PostMapping("/upload")
|
||
public ApiResponse<StrokeUploadResponse> uploadStrokes(
|
||
@Valid @RequestBody StrokeUploadRequest request) {
|
||
|
||
// 校验数据完整性
|
||
if (request.getStrokes() == null || request.getStrokes().isEmpty()) {
|
||
throw new BusinessException(400, "笔迹数据不能为空");
|
||
}
|
||
|
||
// 校验每条笔迹数据的有效性
|
||
int validCount = 0;
|
||
int invalidCount = 0;
|
||
List<String> errors = new ArrayList<>();
|
||
|
||
for (StrokeItem stroke : request.getStrokes()) {
|
||
if (validateStrokeItem(stroke)) {
|
||
validCount++;
|
||
} else {
|
||
invalidCount++;
|
||
errors.add("无效笔迹数据, penId=" + stroke.getPenId()
|
||
+ ", timestamp=" + stroke.getTimestamp());
|
||
}
|
||
}
|
||
|
||
// 将有效数据发送至Kafka消息队列
|
||
// kafkaTemplate.send("writech-stroke-topic", request);
|
||
|
||
// 构建响应
|
||
StrokeUploadResponse response = new StrokeUploadResponse();
|
||
response.setReceivedCount(request.getStrokes().size());
|
||
response.setValidCount(validCount);
|
||
response.setInvalidCount(invalidCount);
|
||
response.setErrors(errors);
|
||
response.setProcessingStatus("queued"); // queued/processing/completed
|
||
response.setUploadTime(LocalDateTime.now());
|
||
|
||
return ApiResponse.success(response);
|
||
}
|
||
|
||
/**
|
||
* 查询学生笔迹数据
|
||
* GET /api/v1/stroke/query
|
||
*
|
||
* 按学生ID、作业ID、时间范围查询笔迹数据
|
||
* 支持笔迹回放场景
|
||
*/
|
||
@GetMapping("/query")
|
||
public ApiResponse<StrokeQueryResponse> queryStrokes(
|
||
@RequestParam String studentId,
|
||
@RequestParam(required = false) String assignmentId,
|
||
@RequestParam(required = false) String pageId,
|
||
@RequestParam(required = false) String startTime,
|
||
@RequestParam(required = false) String endTime,
|
||
@RequestParam(defaultValue = "0") int page,
|
||
@RequestParam(defaultValue = "100") int size) {
|
||
|
||
StrokeQueryResponse response = new StrokeQueryResponse();
|
||
response.setStudentId(studentId);
|
||
response.setTotalStrokes(0);
|
||
response.setStrokes(new ArrayList<>());
|
||
|
||
// strokeDataService.queryStrokes(studentId, assignmentId, ...)
|
||
return ApiResponse.success(response);
|
||
}
|
||
|
||
/**
|
||
* 获取笔迹回放数据
|
||
* GET /api/v1/stroke/replay/{assignmentId}/{studentId}
|
||
*
|
||
* 获取指定学生某次作业的完整笔迹回放数据
|
||
* 按时间戳排序,支持前端动画回放
|
||
*/
|
||
@GetMapping("/replay/{assignmentId}/{studentId}")
|
||
public ApiResponse<StrokeReplayResponse> getReplayData(
|
||
@PathVariable String assignmentId,
|
||
@PathVariable String studentId) {
|
||
|
||
StrokeReplayResponse response = new StrokeReplayResponse();
|
||
response.setAssignmentId(assignmentId);
|
||
response.setStudentId(studentId);
|
||
response.setTotalDuration(0L);
|
||
response.setTotalPoints(0);
|
||
response.setPages(new ArrayList<>());
|
||
|
||
return ApiResponse.success(response);
|
||
}
|
||
|
||
/**
|
||
* 获取笔迹统计信息
|
||
* GET /api/v1/stroke/statistics
|
||
*
|
||
* 查询指定维度的笔迹统计数据(书写量、书写时长等)
|
||
*/
|
||
@GetMapping("/statistics")
|
||
public ApiResponse<StrokeStatistics> getStatistics(
|
||
@RequestParam(required = false) String studentId,
|
||
@RequestParam(required = false) String classId,
|
||
@RequestParam(required = false) String dateRange) {
|
||
|
||
StrokeStatistics stats = new StrokeStatistics();
|
||
stats.setTotalStrokes(12580);
|
||
stats.setTotalPoints(1536000);
|
||
stats.setTotalWritingTime(186400L); // 秒
|
||
stats.setAverageSpeed(8.5); // 每秒点数
|
||
stats.setTotalPages(325);
|
||
|
||
return ApiResponse.success(stats);
|
||
}
|
||
|
||
// ==================== 内部方法 ====================
|
||
|
||
/** 校验单条笔迹数据有效性 */
|
||
private boolean validateStrokeItem(StrokeItem stroke) {
|
||
if (stroke.getPenId() == null || stroke.getPenId().isEmpty()) return false;
|
||
if (stroke.getPoints() == null || stroke.getPoints().isEmpty()) return false;
|
||
// 校验坐标范围(点阵码坐标范围)
|
||
for (StrokePoint point : stroke.getPoints()) {
|
||
if (point.getX() < 0 || point.getX() > 65535) return false;
|
||
if (point.getY() < 0 || point.getY() > 65535) return false;
|
||
if (point.getPressure() < 0 || point.getPressure() > 255) return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ==================== DTO 定义 ====================
|
||
|
||
/** 笔迹上传请求 */
|
||
public static class StrokeUploadRequest {
|
||
@NotBlank private String gatewayId;
|
||
private String classroomId;
|
||
@NotNull private List<StrokeItem> strokes;
|
||
|
||
public String getGatewayId() { return gatewayId; }
|
||
public void setGatewayId(String id) { this.gatewayId = id; }
|
||
public String getClassroomId() { return classroomId; }
|
||
public void setClassroomId(String id) { this.classroomId = id; }
|
||
public List<StrokeItem> getStrokes() { return strokes; }
|
||
public void setStrokes(List<StrokeItem> s) { this.strokes = s; }
|
||
}
|
||
|
||
/** 单条笔迹数据 */
|
||
public static class StrokeItem {
|
||
private String penId; // 笔MAC地址
|
||
private String studentId; // 绑定学生ID
|
||
private String pageId; // 点阵码页面ID
|
||
private String assignmentId; // 关联作业ID
|
||
private long timestamp; // 起始时间戳
|
||
private List<StrokePoint> points; // 坐标点集合
|
||
|
||
public String getPenId() { return penId; }
|
||
public void setPenId(String id) { this.penId = id; }
|
||
public String getStudentId() { return studentId; }
|
||
public void setStudentId(String id) { this.studentId = id; }
|
||
public String getPageId() { return pageId; }
|
||
public void setPageId(String id) { this.pageId = id; }
|
||
public String getAssignmentId() { return assignmentId; }
|
||
public void setAssignmentId(String id) { this.assignmentId = id; }
|
||
public long getTimestamp() { return timestamp; }
|
||
public void setTimestamp(long t) { this.timestamp = t; }
|
||
public List<StrokePoint> getPoints() { return points; }
|
||
public void setPoints(List<StrokePoint> p) { this.points = p; }
|
||
}
|
||
|
||
/** 笔迹坐标点 */
|
||
public static class StrokePoint {
|
||
private int x; // X坐标 (0-65535)
|
||
private int y; // Y坐标 (0-65535)
|
||
private int pressure; // 压力值 (0-255)
|
||
private long timestamp; // 时间戳(毫秒)
|
||
private boolean penUp; // 抬笔标记
|
||
|
||
public int getX() { return x; }
|
||
public void setX(int x) { this.x = x; }
|
||
public int getY() { return y; }
|
||
public void setY(int y) { this.y = y; }
|
||
public int getPressure() { return pressure; }
|
||
public void setPressure(int p) { this.pressure = p; }
|
||
public long getTimestamp() { return timestamp; }
|
||
public void setTimestamp(long t) { this.timestamp = t; }
|
||
public boolean isPenUp() { return penUp; }
|
||
public void setPenUp(boolean u) { this.penUp = u; }
|
||
}
|
||
|
||
/** 上传响应 */
|
||
public static class StrokeUploadResponse {
|
||
private int receivedCount;
|
||
private int validCount;
|
||
private int invalidCount;
|
||
private List<String> errors;
|
||
private String processingStatus;
|
||
private LocalDateTime uploadTime;
|
||
|
||
public int getReceivedCount() { return receivedCount; }
|
||
public void setReceivedCount(int c) { this.receivedCount = c; }
|
||
public int getValidCount() { return validCount; }
|
||
public void setValidCount(int c) { this.validCount = c; }
|
||
public int getInvalidCount() { return invalidCount; }
|
||
public void setInvalidCount(int c) { this.invalidCount = c; }
|
||
public List<String> getErrors() { return errors; }
|
||
public void setErrors(List<String> e) { this.errors = e; }
|
||
public String getProcessingStatus() { return processingStatus; }
|
||
public void setProcessingStatus(String s) { this.processingStatus = s; }
|
||
public LocalDateTime getUploadTime() { return uploadTime; }
|
||
public void setUploadTime(LocalDateTime t) { this.uploadTime = t; }
|
||
}
|
||
|
||
/** 查询响应 */
|
||
public static class StrokeQueryResponse {
|
||
private String studentId;
|
||
private int totalStrokes;
|
||
private List<StrokeItem> strokes;
|
||
|
||
public String getStudentId() { return studentId; }
|
||
public void setStudentId(String id) { this.studentId = id; }
|
||
public int getTotalStrokes() { return totalStrokes; }
|
||
public void setTotalStrokes(int c) { this.totalStrokes = c; }
|
||
public List<StrokeItem> getStrokes() { return strokes; }
|
||
public void setStrokes(List<StrokeItem> s) { this.strokes = s; }
|
||
}
|
||
|
||
/** 回放响应 */
|
||
public static class StrokeReplayResponse {
|
||
private String assignmentId;
|
||
private String studentId;
|
||
private long totalDuration; // 总时长(毫秒)
|
||
private int totalPoints; // 总坐标点数
|
||
private List<PageReplay> pages; // 按页面分组的笔迹数据
|
||
|
||
public String getAssignmentId() { return assignmentId; }
|
||
public void setAssignmentId(String id) { this.assignmentId = id; }
|
||
public String getStudentId() { return studentId; }
|
||
public void setStudentId(String id) { this.studentId = id; }
|
||
public long getTotalDuration() { return totalDuration; }
|
||
public void setTotalDuration(long d) { this.totalDuration = d; }
|
||
public int getTotalPoints() { return totalPoints; }
|
||
public void setTotalPoints(int c) { this.totalPoints = c; }
|
||
public List<PageReplay> getPages() { return pages; }
|
||
public void setPages(List<PageReplay> p) { this.pages = p; }
|
||
}
|
||
|
||
/** 页面回放数据 */
|
||
public static class PageReplay {
|
||
private String pageId;
|
||
private int pageWidth;
|
||
private int pageHeight;
|
||
private List<StrokeItem> strokes;
|
||
|
||
public String getPageId() { return pageId; }
|
||
public void setPageId(String id) { this.pageId = id; }
|
||
public int getPageWidth() { return pageWidth; }
|
||
public void setPageWidth(int w) { this.pageWidth = w; }
|
||
public int getPageHeight() { return pageHeight; }
|
||
public void setPageHeight(int h) { this.pageHeight = h; }
|
||
public List<StrokeItem> getStrokes() { return strokes; }
|
||
public void setStrokes(List<StrokeItem> s) { this.strokes = s; }
|
||
}
|
||
|
||
/** 笔迹统计 */
|
||
public static class StrokeStatistics {
|
||
private int totalStrokes;
|
||
private long totalPoints;
|
||
private long totalWritingTime; // 秒
|
||
private double averageSpeed;
|
||
private int totalPages;
|
||
|
||
public int getTotalStrokes() { return totalStrokes; }
|
||
public void setTotalStrokes(int c) { this.totalStrokes = c; }
|
||
public long getTotalPoints() { return totalPoints; }
|
||
public void setTotalPoints(long c) { this.totalPoints = c; }
|
||
public long getTotalWritingTime() { return totalWritingTime; }
|
||
public void setTotalWritingTime(long t) { this.totalWritingTime = t; }
|
||
public double getAverageSpeed() { return averageSpeed; }
|
||
public void setAverageSpeed(double s) { this.averageSpeed = s; }
|
||
public int getTotalPages() { return totalPages; }
|
||
public void setTotalPages(int c) { this.totalPages = c; }
|
||
}
|
||
}
|