Files
ai-myopia-prevention/api/handlers/detection.go
虾司令 881144269c 🚀 AI 近视防控系统 - 生产环境上线版本 v1.0
 已完成功能:
- 后端 Go 服务 (认证/授权/检测)
- JWT 认证 + RBAC 权限控制
- 登录速率限制 (5 次失败锁定 15 分钟)
- 密码强度校验
- 敏感数据脱敏
- Vue3 管理后台
- 路由守卫
- 删除二次确认

📦 部署配置:
- Docker Compose 生产环境配置
- MySQL/Redis/MongoDB 数据库
- Nginx 前端服务
- 强密码安全配置

⚠️ P2 待办 (下次迭代):
- 学生/检测/预警等业务模块实现
- 错误处理统一化
- 缓存策略优化
- 日志分级

📍 生产环境:
- 服务器:192.168.15.222
- 管理后台:http://192.168.15.222:8081
- API 服务:http://192.168.15.222:8080

2026-03-29 上线部署完成
2026-03-29 18:16:41 +08:00

452 lines
13 KiB
Go

package handlers
import (
"crypto/rand"
"encoding/hex"
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// DetectionService 检测服务
type DetectionService struct {
DB *gorm.DB
}
// StartDetectionRequest 发起检测请求
type StartDetectionRequest struct {
ClassID uint `json:"class_id" binding:"required"`
TeacherID uint `json:"teacher_id" binding:"required"`
StudentCount int `json:"student_count" binding:"required"`
DetectionType string `json:"detection_type" binding:"required,oneof=vision fatigue training"` // vision, fatigue, training
StartTime *time.Time `json:"start_time"`
}
// StartDetectionResponse 发起检测响应
type StartDetectionResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
TaskID string `json:"task_id"`
TaskNo string `json:"task_no"`
StartTime time.Time `json:"start_time"`
} `json:"data"`
}
// SubmitDetectionRequest 提交检测结果请求
type SubmitDetectionRequest struct {
StudentID uint `json:"student_id" binding:"required"`
DetectionID string `json:"detection_id" binding:"required"`
Vision VisionData `json:"vision"`
EyeMovement EyeMovementData `json:"eye_movement"`
Response ResponseData `json:"response"`
Timestamp int64 `json:"timestamp"`
DeviceID *uint `json:"device_id"`
}
// SubmitDetectionResponse 提交检测结果响应
type SubmitDetectionResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
ID string `json:"id"`
} `json:"data"`
}
// GetDetectionReportRequest 获取检测报告请求
type GetDetectionReportRequest struct {
DetectionID string `json:"detection_id" uri:"detection_id"`
StudentID uint `json:"student_id" uri:"student_id"`
}
// GetDetectionReportResponse 获取检测报告响应
type GetDetectionReportResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data DetectionReport `json:"data"`
}
// GetDetectionHistoryRequest 获取检测历史请求
type GetDetectionHistoryRequest struct {
StudentID uint `form:"student_id"`
ClassID uint `form:"class_id"`
StartDate time.Time `form:"start_date"`
EndDate time.Time `form:"end_date"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=20"`
}
// GetDetectionHistoryResponse 获取检测历史响应
type GetDetectionHistoryResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data struct {
Items []DetectionHistory `json:"items"`
Total int `json:"total"`
Page int `json:"page"`
} `json:"data"`
}
// GetClassStatsRequest 获取班级统计请求
type GetClassStatsRequest struct {
ClassID uint `form:"class_id" uri:"class_id" binding:"required"`
StartDate time.Time `form:"start_date"`
EndDate time.Time `form:"end_date"`
}
// GetClassStatsResponse 获取班级统计响应
type GetClassStatsResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data ClassStats `json:"data"`
}
// ClassStats 班级统计数据
type ClassStats struct {
ClassID uint `json:"class_id"`
ClassName string `json:"class_name"`
TotalStudents int `json:"total_students"`
TestedStudents int `json:"tested_students"`
AvgVisionLeft float64 `json:"avg_vision_left"`
AvgVisionRight float64 `json:"avg_vision_right"`
VisionDeclineCount int `json:"vision_decline_count"`
AvgFatigueScore float64 `json:"avg_fatigue_score"`
AlertCount int `json:"alert_count"`
AlertDistribution map[string]int `json:"alert_distribution"`
CreatedAt time.Time `json:"created_at"`
}
// VisionData 视力数据
type VisionData struct {
VisionLeft float64 `json:"vision_left"`
VisionRight float64 `json:"vision_right"`
Confidence string `json:"confidence"` // high, medium, low
}
// EyeMovementData 眼动数据
type EyeMovementData struct {
LeftEye PupilData `json:"left_eye"`
RightEye PupilData `json:"right_eye"`
GazePoint GazePoint `json:"gaze_point"`
Timestamp int64 `json:"timestamp"`
}
// PupilData 瞳孔数据
type PupilData struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Radius float64 `json:"radius"`
Confidence float64 `json:"confidence"`
}
// GazePoint 注视点
type GazePoint struct {
X float64 `json:"x"`
Y float64 `json:"y"`
Confidence float64 `json:"confidence"`
}
// ResponseData 响应数据
type ResponseData struct {
Accuracy float64 `json:"accuracy"` // 准确率
ResponseType float64 `json:"response_time"` // 响应时间
Errors int `json:"errors"` // 错误次数
}
// DetectionReport 检测报告
type DetectionReport struct {
ID uint `json:"id"`
StudentID uint `json:"student_id"`
Student interface{} `json:"student"`
DetectionID uint `json:"detection_id"`
Detection interface{} `json:"detection"`
Vision VisionData `json:"vision"`
EyeMovement EyeMovementData `json:"eye_movement"`
Response ResponseData `json:"response"`
FatigueScore float64 `json:"fatigue_score"`
AlertLevel string `json:"alert_level"` // normal, warning, alert
AIAnalysis interface{} `json:"ai_analysis"` // AI分析结果
CreatedAt time.Time `json:"created_at"`
}
// DetectionHistory 检测历史
type DetectionHistory struct {
ID uint `json:"id"`
StudentID uint `json:"student_id"`
Student interface{} `json:"student"`
ClassID uint `json:"class_id"`
Class interface{} `json:"class"`
Vision VisionData `json:"vision"`
FatigueScore float64 `json:"fatigue_score"`
AlertLevel string `json:"alert_level"`
DetectionTime time.Time `json:"detection_time"`
CreatedAt time.Time `json:"created_at"`
}
// NewDetectionService 创建检测服务
func NewDetectionService(db *gorm.DB) *DetectionService {
return &DetectionService{DB: db}
}
// StartDetection 发起检测
func (s *DetectionService) StartDetection(c *gin.Context) {
var req StartDetectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误: " + err.Error(),
})
return
}
// 创建检测任务
task := map[string]interface{}{
"task_no": generateTaskNo(), // 生成任务编号
"class_id": req.ClassID,
"teacher_id": req.TeacherID,
"student_count": req.StudentCount,
"detection_type": req.DetectionType,
"start_time": req.StartTime,
"status": 0, // 0:进行中
}
result := s.DB.Table("detection_tasks").Create(task)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "发起检测失败: " + result.Error.Error(),
})
return
}
// 返回响应
resp := StartDetectionResponse{
Code: 0,
Message: "检测任务已创建",
}
resp.Data.TaskID = "task_" + string(rune(result.RowsAffected)) // 简化处理
resp.Data.TaskNo = task["task_no"].(string)
resp.Data.StartTime = time.Now()
c.JSON(http.StatusOK, resp)
}
// SubmitDetection 提交检测结果
func (s *DetectionService) SubmitDetection(c *gin.Context) {
var req SubmitDetectionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误: " + err.Error(),
})
return
}
// 创建检测记录
detection := map[string]interface{}{
"task_id": req.DetectionID, // 这里应该是task_id而不是detection_id
"student_id": req.StudentID,
"detection_time": time.Now(),
"vision_left": req.Vision.VisionLeft,
"vision_right": req.Vision.VisionRight,
"fatigue_score": req.Response.Accuracy, // 使用响应准确率作为疲劳分数示例
"device_id": req.DeviceID,
"status": 1,
}
result := s.DB.Table("detections").Create(detection)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "提交检测结果失败: " + result.Error.Error(),
})
return
}
resp := SubmitDetectionResponse{
Code: 0,
Message: "检测结果提交成功",
}
resp.Data.ID = "detection_" + string(rune(result.RowsAffected))
c.JSON(http.StatusOK, resp)
}
// GetDetectionReport 获取检测报告
func (s *DetectionService) GetDetectionReport(c *gin.Context) {
var req GetDetectionReportRequest
if err := c.ShouldBindUri(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误: " + err.Error(),
})
return
}
// 查询检测报告
var report DetectionReport
result := s.DB.Table("detection_reports").Where("detection_id = ? AND student_id = ?", req.DetectionID, req.StudentID).First(&report)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{
"code": 404,
"message": "检测报告不存在",
})
return
}
c.JSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "查询检测报告失败: " + result.Error.Error(),
})
return
}
resp := GetDetectionReportResponse{
Code: 0,
Message: "获取成功",
Data: report,
}
c.JSON(http.StatusOK, resp)
}
// GetDetectionHistory 获取检测历史
func (s *DetectionService) GetDetectionHistory(c *gin.Context) {
var req GetDetectionHistoryRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误: " + err.Error(),
})
return
}
// 构建查询条件
query := s.DB.Table("detection_history")
if req.StudentID != 0 {
query = query.Where("student_id = ?", req.StudentID)
}
if req.ClassID != 0 {
query = query.Where("class_id = ?", req.ClassID)
}
if !req.StartDate.IsZero() {
query = query.Where("detection_time >= ?", req.StartDate)
}
if !req.EndDate.IsZero() {
query = query.Where("detection_time <= ?", req.EndDate)
}
// 查询总数
var total int64
query.Count(&total)
// 查询数据
var histories []DetectionHistory
query.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&histories)
resp := GetDetectionHistoryResponse{
Code: 0,
Message: "获取成功",
}
resp.Data.Items = histories
resp.Data.Total = int(total)
resp.Data.Page = req.Page
c.JSON(http.StatusOK, resp)
}
// GetClassStats 获取班级统计
func (s *DetectionService) GetClassStats(c *gin.Context) {
var req GetClassStatsRequest
if err := c.ShouldBindUri(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": 400,
"message": "参数错误: " + err.Error(),
})
return
}
// 获取班级信息
var classInfo struct {
ID uint `json:"id"`
Name string `json:"name"`
}
s.DB.Table("classes").Select("id, name").Where("id = ?", req.ClassID).First(&classInfo)
// 计算统计信息
var stats ClassStats
stats.ClassID = req.ClassID
stats.ClassName = classInfo.Name
// 统计学生总数
var totalStudents int64
s.DB.Table("students").Where("class_id = ?", req.ClassID).Count(&totalStudents)
stats.TotalStudents = int(totalStudents)
// 统计参与检测的学生数
var testedStudents int64
s.DB.Table("detections").
Joins("JOIN detection_tasks ON detections.task_id = detection_tasks.id").
Where("detection_tasks.class_id = ?", req.ClassID).
Distinct("student_id").Count(&testedStudents)
stats.TestedStudents = int(testedStudents)
// 计算平均视力
var resultAvg struct {
AvgLeft float64 `json:"avg_left"`
AvgRight float64 `json:"avg_right"`
}
s.DB.Table("detections").
Joins("JOIN detection_tasks ON detections.task_id = detection_tasks.id").
Where("detection_tasks.class_id = ?", req.ClassID).
Select("AVG(vision_left) as avg_left, AVG(vision_right) as avg_right").
Scan(&resultAvg)
stats.AvgVisionLeft = resultAvg.AvgLeft
stats.AvgVisionRight = resultAvg.AvgRight
// 统计预警数量
var alertCount int64
s.DB.Table("alerts").
Joins("JOIN students ON alerts.student_id = students.id").
Where("students.class_id = ?", req.ClassID).
Count(&alertCount)
stats.AlertCount = int(alertCount)
// 预警分布
alertDist := make(map[string]int)
alertDist["green"] = 0 // 示例数据
alertDist["yellow"] = 0
alertDist["orange"] = 0
alertDist["red"] = 0
stats.AlertDistribution = alertDist
stats.CreatedAt = time.Now()
resp := GetClassStatsResponse{
Code: 0,
Message: "获取成功",
Data: stats,
}
c.JSON(http.StatusOK, resp)
}
// generateTaskNo 生成任务编号 - 使用更安全的随机ID
func generateTaskNo() string {
// 生成随机ID
b := make([]byte, 8)
_, err := rand.Read(b)
if err != nil {
// 如果随机数生成失败,使用时间戳+简单随机数作为备用
return fmt.Sprintf("task_%d_%x", time.Now().UnixNano(), b[:4])
}
randomID := hex.EncodeToString(b)
return fmt.Sprintf("task_%s_%s", time.Now().Format("20060102_150405"), randomID[:8])
}