package handlers import ( "crypto/rand" "fmt" "math/big" "strings" "time" "github.com/gin-gonic/gin" "github.com/niangaodev/art-code/config" "github.com/niangaodev/art-code/middleware" "github.com/niangaodev/art-code/models" "github.com/niangaodev/art-code/repositories" "github.com/niangaodev/art-code/utils" "gorm.io/gorm" ) const guestCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" func randomGuestSuffix(minLen, maxLen int) string { nBig, _ := rand.Int(rand.Reader, big.NewInt(int64(maxLen-minLen+1))) n := int(nBig.Int64()) + minLen b := make([]byte, n) for i := 0; i < n; i++ { idx, _ := rand.Int(rand.Reader, big.NewInt(int64(len(guestCharset)))) b[i] = guestCharset[idx.Int64()] } return string(b) } func uniqueGuestUsername() (string, error) { for i := 0; i < 8; i++ { name := "游客_" + randomGuestSuffix(6, 10) exist, err := repositories.GetUserByUsername(name) if err != nil { return "", err } if exist == nil { return name, nil } } return "", fmt.Errorf("failed to allocate guest username") } // WxLogin 微信小程序静默登录 / 自动建游客账号 func WxLogin(c *gin.Context) { var req models.WxLoginRequest if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Code) == "" { utils.Error(c, 400, "Invalid request") return } session, err := utils.Code2Session(req.Code) if err != nil { utils.Error(c, 400, err.Error()) return } user, err := repositories.GetUserByWxOpenID(session.OpenID) if err != nil { utils.ServerError(c, err) return } if user == nil { username, err := uniqueGuestUsername() if err != nil { utils.ServerError(c, err) return } hash, err := utils.HashPassword(randomGuestSuffix(16, 24)) if err != nil { utils.ServerError(c, err) return } openidPrefix := session.OpenID if len(openidPrefix) > 12 { openidPrefix = openidPrefix[:12] } user = &models.User{ Username: username, Email: fmt.Sprintf("wx_%s@guest.local", openidPrefix), Avatar: "", PasswordHash: hash, WxOpenID: session.OpenID, Role: "viewer", IsActive: 1, } if role, rerr := repositories.GetRoleByName("viewer"); rerr == nil && role != nil { user.RoleID = role.ID } if err := repositories.CreateUser(user); err != nil { utils.ServerError(c, err) return } // reload with role name user, err = repositories.GetUserByWxOpenID(session.OpenID) if err != nil || user == nil { utils.ServerError(c, fmt.Errorf("created user but reload failed")) return } } if user.IsActive == 0 { utils.Error(c, 403, "Account disabled") return } tokenString, expireUnix, err := middleware.GenerateToken(user.ID, user.Username, user.Role) if err != nil { utils.ServerError(c, err) return } go func() { ip := c.ClientIP() location := utils.GetRegion(ip) logEntry := &models.UserAccessLog{ UserID: user.ID, UserIP: ip, UserLocation: location, } _ = repositories.CreateUserAccessLog(logEntry) }() utils.Success(c, gin.H{ "token": tokenString, "expire": expireUnix, "user": repositories.BuildUserResponse(user), }) } // isGuestUser 判断是否为可合并的游客账号(viewer 或 游客_ 前缀) func isGuestUser(u *models.User) bool { if u == nil { return false } role := strings.ToLower(strings.TrimSpace(u.Role)) if strings.HasPrefix(u.Username, "游客_") { return true } return role == "viewer" || role == "guest" } // isStaffUser 判断是否为可绑定的员工账号 func isStaffUser(u *models.User) bool { if u == nil { return false } role := strings.ToLower(strings.TrimSpace(u.Role)) return role == "admin" || role == "editor" || strings.Contains(role, "admin") || strings.Contains(role, "editor") } // MergeAccountRequest 游客合并到管理员账号 type MergeAccountRequest struct { Username string `json:"username" binding:"required"` Password string `json:"password" binding:"required"` } // MergeAccount 游客校验管理员密码后,将微信 openid 迁到管理员,并逻辑删除游客 func MergeAccount(c *gin.Context) { var req MergeAccountRequest if err := c.ShouldBindJSON(&req); err != nil { utils.Error(c, 400, "Invalid request") return } guestIDVal, ok := c.Get("userID") if !ok { utils.Error(c, 401, "Unauthorized") return } guestID, ok := guestIDVal.(uint) if !ok { utils.Error(c, 401, "Unauthorized") return } guest, err := repositories.GetUserByID(guestID) if err != nil { utils.ServerError(c, err) return } if guest == nil { utils.Error(c, 401, "Unauthorized") return } if !isGuestUser(guest) { utils.Error(c, 400, "当前账号不是游客,无法合并") return } openid := strings.TrimSpace(guest.WxOpenID) if openid == "" { utils.Error(c, 400, "当前游客未绑定微信,无法合并") return } target, err := repositories.GetUserByUsername(strings.TrimSpace(req.Username)) if err != nil { utils.ServerError(c, err) return } if target == nil || !utils.CheckPasswordHash(req.Password, target.PasswordHash) { utils.Error(c, 401, "Invalid username or password") return } if target.IsActive == 0 { utils.Error(c, 403, "Account disabled") return } if !isStaffUser(target) { utils.Error(c, 403, "目标账号不是管理员或编辑者") return } if target.ID == guest.ID { utils.Error(c, 400, "不能合并到当前账号") return } existingOpenID := strings.TrimSpace(target.WxOpenID) if existingOpenID != "" && existingOpenID != openid { utils.Error(c, 409, "该管理员已绑定其他微信账号") return } // 事务:清空游客 openid → 软删游客 → 绑定到管理员 err = config.DB.Transaction(func(tx *gorm.DB) error { now := time.Now().Unix() if err := tx.Model(&models.User{}).Where("id = ?", guest.ID). Updates(map[string]interface{}{ "wx_openid": nil, "updated_at": now, }).Error; err != nil { return err } if err := tx.Model(&models.User{}).Where("id = ?", guest.ID). Update("deleted_at", now).Error; err != nil { return err } if existingOpenID == openid { return nil } return tx.Model(&models.User{}). Where("id = ? AND deleted_at = ?", target.ID, 0). Updates(map[string]interface{}{ "wx_openid": openid, "updated_at": now, }).Error }) if err != nil { utils.ServerError(c, err) return } admin, err := repositories.GetUserByID(target.ID) if err != nil || admin == nil { utils.ServerError(c, fmt.Errorf("merged but reload admin failed")) return } tokenString, expireUnix, err := middleware.GenerateToken(admin.ID, admin.Username, admin.Role) if err != nil { utils.ServerError(c, err) return } go func() { ip := c.ClientIP() location := utils.GetRegion(ip) logEntry := &models.UserAccessLog{ UserID: admin.ID, UserIP: ip, UserLocation: location, } _ = repositories.CreateUserAccessLog(logEntry) }() utils.Success(c, gin.H{ "token": tokenString, "expire": expireUnix, "user": repositories.BuildUserResponse(admin), }) }