Files
nl-blogs/server/handlers/wx_auth.go
李琦 a9eb20f0e1 1. 小程序端
2. 视频优化
2026-07-30 14:11:52 +08:00

284 lines
6.9 KiB
Go
Raw 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 (
"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),
})
}