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