🚀 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 上线部署完成
This commit is contained in:
412
api/handlers/auth.go
Normal file
412
api/handlers/auth.go
Normal file
@@ -0,0 +1,412 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"ai-myopia-prevention/internal/middleware"
|
||||
"ai-myopia-prevention/internal/utils"
|
||||
)
|
||||
|
||||
// AuthService 认证服务
|
||||
type AuthService struct {
|
||||
DB *gorm.DB
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
DeviceID string `json:"device_id"` // 设备ID,用于设备认证
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Role string `json:"role"` // student, parent, teacher, admin
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// RegisterRequest 注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=32"`
|
||||
Password string `json:"password" binding:"required"` // 移除min=6,改用强度校验
|
||||
Name string `json:"name" binding:"required"`
|
||||
Phone string `json:"phone" binding:"required"`
|
||||
Role string `json:"role" binding:"required,oneof=student parent teacher"` // 角色
|
||||
}
|
||||
|
||||
// RegisterResponse 注册响应
|
||||
type RegisterResponse struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data struct {
|
||||
UserID uint `json:"user_id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// ChangePasswordRequest 修改密码请求
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// UserProfile 用户资料
|
||||
type UserProfile struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
// NewAuthService 创建认证服务
|
||||
func NewAuthService(db *gorm.DB) *AuthService {
|
||||
return &AuthService{DB: db}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *AuthService) Login(c *gin.Context) {
|
||||
var req LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查IP是否被封禁
|
||||
ip := c.ClientIP()
|
||||
if middleware.LoginRateLimiterInstance.IsBlocked(ip) {
|
||||
c.JSON(http.StatusTooManyRequests, gin.H{
|
||||
"code": 429,
|
||||
"message": "登录失败次数过多,请15分钟后重试",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 根据用户名或手机号查找用户
|
||||
var user struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
PasswordHash string `json:"-"`
|
||||
Role string `json:"role"`
|
||||
Status int `json:"status"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
LastLoginIP string `json:"last_login_ip"`
|
||||
}
|
||||
|
||||
result := s.DB.Table("user_accounts").
|
||||
Select("id, username, name, phone, password_hash, role, status").
|
||||
Where("username = ? OR phone = ?", req.Username, req.Username).
|
||||
First(&user)
|
||||
|
||||
if result.Error != nil {
|
||||
// 记录登录失败
|
||||
middleware.LoginRateLimiterInstance.RecordFailure(ip)
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户名或密码错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if user.Status != 1 {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"code": 403,
|
||||
"message": "账户已被禁用",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password))
|
||||
if err != nil {
|
||||
// 记录登录失败
|
||||
middleware.LoginRateLimiterInstance.RecordFailure(ip)
|
||||
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"code": 401,
|
||||
"message": "用户名或密码错误",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功,重置失败次数
|
||||
middleware.LoginRateLimiterInstance.ResetAttempts(ip)
|
||||
|
||||
// 生成JWT Token
|
||||
token, err := middleware.GenerateToken(user.ID, user.Username, user.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "生成认证令牌失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新最后登录时间和IP
|
||||
s.DB.Table("user_accounts").
|
||||
Where("id = ?", user.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_login_at": time.Now(),
|
||||
"last_login_ip": ip,
|
||||
})
|
||||
|
||||
resp := LoginResponse{
|
||||
Code: 0,
|
||||
Message: "登录成功",
|
||||
}
|
||||
resp.Data.Token = token
|
||||
resp.Data.ExpiresAt = time.Now().Add(time.Hour * 24 * 7) // 7天过期
|
||||
resp.Data.UserID = user.ID
|
||||
resp.Data.Username = user.Username
|
||||
resp.Data.Name = user.Name
|
||||
resp.Data.Role = user.Role
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (s *AuthService) Register(c *gin.Context) {
|
||||
var req RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 密码强度校验
|
||||
if err := middleware.ValidatePasswordStrength(req.Password); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "密码强度不够: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
var count int64
|
||||
s.DB.Table("user_accounts").Where("username = ?", req.Username).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "用户名已存在",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查手机号是否已存在
|
||||
s.DB.Table("user_accounts").Where("phone = ?", req.Phone).Count(&count)
|
||||
if count > 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "手机号已被注册",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "密码加密失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建用户账号
|
||||
userAccount := map[string]interface{}{
|
||||
"username": req.Username,
|
||||
"password_hash": string(hashedPassword),
|
||||
"phone": req.Phone,
|
||||
"user_type": req.Role,
|
||||
"status": 1,
|
||||
}
|
||||
|
||||
result := s.DB.Table("user_accounts").Create(userAccount)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "注册失败: " + result.Error.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取创建的用户ID
|
||||
var newUser struct {
|
||||
ID uint `json:"id"`
|
||||
}
|
||||
s.DB.Table("user_accounts").Where("username = ?", req.Username).Order("id DESC").First(&newUser)
|
||||
|
||||
resp := RegisterResponse{
|
||||
Code: 0,
|
||||
Message: "注册成功",
|
||||
}
|
||||
resp.Data.UserID = newUser.ID
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// GetProfile 获取用户资料
|
||||
func (s *AuthService) GetProfile(c *gin.Context) {
|
||||
// 这里应该是从JWT token中获取用户ID
|
||||
// 为了演示,我们使用一个占位符
|
||||
userID := c.GetUint("user_id") // 从中间件传递过来的用户ID
|
||||
|
||||
var user struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
result := s.DB.Table("user_accounts").Select("id, username, name, phone, user_type as role").Where("id = ?", userID).First(&user)
|
||||
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
|
||||
}
|
||||
|
||||
// 对敏感数据进行脱敏处理
|
||||
user.Phone = utils.MaskPhone(user.Phone)
|
||||
user.Name = utils.MaskName(user.Name)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "获取成功",
|
||||
"data": user,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile 更新用户资料
|
||||
func (s *AuthService) UpdateProfile(c *gin.Context) {
|
||||
userID := c.GetUint("user_id") // 从中间件传递过来的用户ID
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Phone string `json:"phone"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
if req.Name != "" {
|
||||
updates["name"] = req.Name
|
||||
}
|
||||
if req.Phone != "" {
|
||||
// 对手机号进行验证和格式化
|
||||
updates["phone"] = req.Phone
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "没有可更新的数据",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result := s.DB.Table("user_accounts").Where("id = ?", userID).Updates(updates)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "更新失败: " + result.Error.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "更新成功",
|
||||
})
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *AuthService) ChangePassword(c *gin.Context) {
|
||||
userID := c.GetUint("user_id") // 从中间件传递过来的用户ID
|
||||
|
||||
var req ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前用户密码
|
||||
var currentPasswordHash string
|
||||
s.DB.Table("user_accounts").Select("password_hash").Where("id = ?", userID).First(¤tPasswordHash)
|
||||
|
||||
// 验证旧密码
|
||||
err := bcrypt.CompareHashAndPassword([]byte(currentPasswordHash), []byte(req.OldPassword))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"code": 400,
|
||||
"message": "旧密码不正确",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedNewPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "密码加密失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
result := s.DB.Table("user_accounts").
|
||||
Where("id = ?", userID).
|
||||
Update("password_hash", string(hashedNewPassword))
|
||||
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": "修改密码失败: " + result.Error.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
"message": "密码修改成功",
|
||||
})
|
||||
}
|
||||
452
api/handlers/detection.go
Normal file
452
api/handlers/detection.go
Normal file
@@ -0,0 +1,452 @@
|
||||
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])
|
||||
}
|
||||
Reference in New Issue
Block a user