package main import ( "net/http" "os" "strconv" "strings" "time" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" "github.com/sirupsen/logrus" "golang.org/x/crypto/bcrypt" ) var logger = logrus.New() // 在全局变量后新增用户模型 type Region struct { ID uint `gorm:"primary_key"` Name string `gorm:"not null;unique"` } type User struct { gorm.Model FullName string `gorm:"not null"` RegionID uint `gorm:"not null"` // 修改为关联地区ID Mobile string `gorm:"unique;not null"` Password string `gorm:"not null"` Region Region // 关联关系 } func init() { // 配置日志格式 logger.SetFormatter(&logrus.JSONFormatter{}) logger.SetOutput(os.Stdout) } func main() { // 初始化数据库 db, err := gorm.Open("sqlite3", "family.db") if err != nil { logger.Fatalf("数据库连接失败: %v", err) } defer db.Close() db.AutoMigrate(&Region{}, &User{}) // 同时迁移Region和User表 // 初始化 Gin 引擎 r := gin.Default() // 配置静态文件服务 r.Static("/assets", "./static/assets") r.LoadHTMLGlob("templates/*") // 配置 Session 中间件 store := cookie.NewStore([]byte("secret")) store.Options(sessions.Options{ MaxAge: 86400 * 7, HttpOnly: true, Secure: false, // 如果是HTTPS需要设为true SameSite: http.SameSiteLaxMode, // 允许跨站携带cookie }) r.Use(sessions.Sessions("mysession", store)) // 登录页面 r.GET("/login", func(c *gin.Context) { c.HTML(http.StatusOK, "login.html", nil) }) // 处理登录请求 r.POST("/login", func(c *gin.Context) { username := c.PostForm("username") // 修改表单字段名 password := c.PostForm("password") var user User // 电话号作为用户名 if err := db.Where("mobile = ?", username).First(&user).Error; err != nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{"error": "用户不存在或密码错误"}) return } // 验证密码 if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil { c.HTML(http.StatusUnauthorized, "login.html", gin.H{"error": "用户不存在或密码错误"}) return } // 保存session(保持原有逻辑) session := sessions.Default(c) session.Set("user", user.ID) if err := session.Save(); err != nil { logger.Errorf("Session保存失败: %v", err) c.HTML(http.StatusInternalServerError, "login.html", gin.H{"error": "登录状态保存失败"}) return } c.Redirect(http.StatusSeeOther, "/") // 改用303状态码 }) // 在登录路由后新增注册路由 // 注册页面 r.GET("/register", func(c *gin.Context) { regions, err := getRegions(db) if err != nil { c.HTML(http.StatusInternalServerError, "register.html", gin.H{"error": "系统错误"}) return } c.HTML(http.StatusOK, "register.html", gin.H{"regions": regions}) }) // 处理注册请求 r.POST("/register", func(c *gin.Context) { // 获取地区参数(修改这部分) regionStr, exists := c.GetPostForm("region") if !exists { regions, err := getRegions(db) if err != nil { c.HTML(http.StatusInternalServerError, "register.html", gin.H{"error": "系统错误"}) return } c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": "请选择所在地区", "regions": regions, "form": gin.H{ "fullname": c.PostForm("fullname"), "mobile": c.PostForm("mobile"), }, }) return } // 转换地区ID为数字 regionID, err := strconv.ParseUint(regionStr, 10, 32) if err != nil { regions, err := getRegions(db) if err != nil { c.HTML(http.StatusInternalServerError, "register.html", gin.H{"error": "系统错误"}) return } c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": "无效的地区参数", "regions": regions, "form": gin.H{ "fullname": c.PostForm("fullname"), "mobile": c.PostForm("mobile"), }, }) return } user := User{ FullName: c.PostForm("fullname"), Mobile: c.PostForm("mobile"), RegionID: uint(regionID), // 使用转换后的ID } // 验证地区是否存在 var region Region if err := db.First(®ion, user.RegionID).Error; err != nil { regions, err := getRegions(db) if err != nil { c.HTML(http.StatusInternalServerError, "register.html", gin.H{"error": "系统错误"}) return } c.HTML(http.StatusBadRequest, "register.html", gin.H{ "error": "请选择有效地区", "regions": regions, "form": gin.H{ "fullname": c.PostForm("fullname"), "mobile": c.PostForm("mobile"), }, }) return } // 验证手机号格式 if len(user.Mobile) != 11 { c.HTML(http.StatusBadRequest, "register.html", gin.H{"error": "手机号格式不正确"}) return } // 密码加密 password := c.PostForm("password") hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { logger.Errorf("密码加密失败: %v", err) c.HTML(http.StatusInternalServerError, "register.html", gin.H{"error": "注册失败"}) return } user.Password = string(hashedPassword) // 创建用户 if err := db.Create(&user).Error; err != nil { logger.Errorf("用户创建失败: %v", err) errorMsg := "注册失败" if strings.Contains(err.Error(), "UNIQUE constraint failed") { errorMsg = "该手机号已注册" } c.HTML(http.StatusBadRequest, "register.html", gin.H{"error": errorMsg}) return } c.Redirect(http.StatusSeeOther, "/login") }) // 权限校验中间件 authMiddleware := func(c *gin.Context) { session := sessions.Default(c) user := session.Get("user") if user == nil { c.Redirect(http.StatusFound, "/login") c.Abort() return } c.Next() } // 文档页面路由 r.Use(authMiddleware) r.GET("/", func(c *gin.Context) { // 记录访问痕迹 logAccess(c) http.ServeFile(c.Writer, c.Request, "./static/index.html") }) // 在权限校验中间件后添加退出路由 r.GET("/logout", func(c *gin.Context) { session := sessions.Default(c) session.Clear() if err := session.Save(); err != nil { logger.Errorf("退出登录失败: %v", err) c.HTML(http.StatusInternalServerError, "error.html", gin.H{"error": "退出登录失败"}) return } c.Redirect(http.StatusSeeOther, "/login") }) // 新增通用静态文件路由(放在其他路由之后) r.NoRoute(func(c *gin.Context) { logAccess(c) filePath := "./static" + c.Request.URL.Path // 检查文件是否存在 if _, err := os.Stat(filePath); err == nil { http.ServeFile(c.Writer, c.Request, filePath) } else { c.AbortWithStatus(http.StatusNotFound) } }) // 启动服务 r.Run(":7070") } // 记录访问痕迹 func logAccess(c *gin.Context) { ip := c.ClientIP() path := c.Request.URL.Path method := c.Request.Method timestamp := time.Now().Format(time.RFC3339) logger.WithFields(logrus.Fields{ "ip": ip, "path": path, "method": method, "timestamp": timestamp, }).Info("Page accessed") } func getRegions(db *gorm.DB) ([]Region, error) { var regions []Region if err := db.Find(®ions).Error; err != nil { logger.Errorf("获取地区数据失败: %v", err) return nil, err } return regions, nil }