Files
虾司令 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

412 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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": "密码修改成功",
})
}