🚀 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:
虾司令
2026-03-29 18:16:41 +08:00
commit 881144269c
38 changed files with 4967 additions and 0 deletions

412
api/handlers/auth.go Normal file
View 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(&currentPasswordHash)
// 验证旧密码
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": "密码修改成功",
})
}