This commit is contained in:
Joe
2026-01-16 15:49:34 +08:00
commit 550d3e1f42
380 changed files with 62024 additions and 0 deletions
+401
View File
@@ -0,0 +1,401 @@
package services
import (
"context"
"encoding/json"
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/str"
"github.com/spf13/cast"
"goravel/app/errors"
"goravel/app/http/helpers"
"goravel/app/models"
"goravel/app/utils"
"goravel/app/utils/errorlog"
"goravel/app/utils/logger"
)
type AuthService interface {
// Login 管理员登录
Login(ctx http.Context, username, password string) (*models.Admin, string, error)
// GetAdminInfo 获取管理员完整信息(包括权限和菜单)
GetAdminInfo(ctx http.Context) (*models.Admin, []models.Permission, []models.Menu, error)
// RecordLoginLog 记录登录日志
RecordLoginLog(ctx http.Context, adminID uint, username string, status uint8, message string, request string) error
}
type AuthServiceImpl struct {
adminService AdminService
tokenService TokenService
}
func NewAuthServiceImpl(adminService AdminService, tokenService TokenService) *AuthServiceImpl {
return &AuthServiceImpl{
adminService: adminService,
tokenService: tokenService,
}
}
// Login 管理员登录
//
// 参数:
// - ctx: HTTP 上下文
// - username: 用户名
// - password: 密码
//
// 返回:
// - *models.Admin: 管理员对象
// - string: JWT token
// - error: 错误信息
func (s *AuthServiceImpl) Login(ctx http.Context, username, password string) (*models.Admin, string, error) {
// 验证用户名是否存在
exists, err := facades.Orm().Query().Model(&models.Admin{}).Where("username", username).Exists()
if err != nil {
return nil, "", err
}
if !exists {
return nil, "", errors.ErrUsernameOrPasswordErr
}
// 获取管理员信息
var admin models.Admin
if err := facades.Orm().Query().Where("username", username).First(&admin); err != nil {
return nil, "", err
}
if admin.Status == 0 {
return nil, "", errors.ErrAccountDisabled
}
// 验证密码
if !facades.Hash().Check(password, admin.Password) {
// 记录登录失败日志(注意:这个方法可能不再使用,但为了兼容性保留)
requestData := ""
if allInputs := ctx.Request().All(); len(allInputs) > 0 {
if data, err := json.Marshal(allInputs); err == nil {
requestData = string(data)
}
}
s.RecordLoginLog(ctx, 0, username, 0, "password_error", requestData)
return nil, "", errors.ErrPasswordError
}
// 生成token并存入数据库(类似Laravel Sanctum
// 按配置的过期时间生成token,如果需要永久token,可以在创建token时设置 expiresAt 为 nil
var expiresAt *time.Time
ttl := facades.Config().GetInt("jwt.ttl", 60) // 默认60分钟
if ttl > 0 {
// 如果配置了过期时间,设置过期时间
exp := time.Now().Add(time.Duration(ttl) * time.Minute)
expiresAt = &exp
}
// 如果 ttl 为 0 或负数,expiresAt 为 nil,表示永不过期
// 获取浏览器和操作系统信息
browser, os := helpers.GetBrowserAndOS(ctx)
// 获取真实IP地址
ip := helpers.GetRealIP(ctx)
// sessionID将在CreateToken中自动生成
// 生成token
plainToken, _, err := s.tokenService.CreateToken("admin", admin.ID, "admin-token", expiresAt, browser, ip, os, "")
if err != nil {
return nil, "", err
}
token := plainToken
// 更新最后登录时间(ORM会自动更新UpdatedAt
facades.Orm().Query().Save(&admin)
// 记录登录成功日志
requestData := ""
if allInputs := ctx.Request().All(); len(allInputs) > 0 {
if data, err := json.Marshal(allInputs); err == nil {
requestData = string(data)
}
}
s.RecordLoginLog(ctx, admin.ID, username, 1, "login_success", requestData)
return &admin, token, nil
}
// GetAdminInfo 获取管理员完整信息(包括权限和菜单)
//
// 参数:
// - ctx: HTTP 上下文
//
// 返回:
// - *models.Admin: 管理员对象
// - []models.Permission: 权限列表
// - []models.Menu: 菜单列表
// - error: 错误信息
func (s *AuthServiceImpl) GetAdminInfo(ctx http.Context) (*models.Admin, []models.Permission, []models.Menu, error) {
// 从context中获取admin信息(由JWT中间件设置)
adminValue := ctx.Value("admin")
if adminValue == nil {
logger.ErrorfHTTP(ctx, "GetAdminInfo: admin value is nil in context")
return nil, nil, nil, errors.ErrNotLoggedIn
}
var admin models.Admin
// 尝试值类型
if adminVal, ok := adminValue.(models.Admin); ok {
admin = adminVal
} else if adminPtr, ok := adminValue.(*models.Admin); ok {
// 尝试指针类型
if adminPtr == nil {
logger.ErrorfHTTP(ctx, "GetAdminInfo: admin pointer is nil")
return nil, nil, nil, errors.ErrNotLoggedIn
}
admin = *adminPtr
} else {
logger.ErrorfHTTP(ctx, "GetAdminInfo: admin value type assertion failed, type: %T, value: %+v", adminValue, adminValue)
return nil, nil, nil, errors.ErrNotLoggedIn
}
// facades.Log().Debugf("GetAdminInfo: admin found, ID: %d, Username: %s", admin.ID, admin.Username)
// 重新查询admin并加载关联(避免使用已存在的admin对象,可能导致关联加载问题)
var adminWithRelations models.Admin
if err := facades.Orm().Query().With("Department").With("Roles").Where("id", admin.ID).First(&adminWithRelations); err != nil {
errorlog.RecordHTTP(ctx, "auth", "Failed to load admin with relations", map[string]any{
"error": err.Error(),
"admin_id": admin.ID,
}, "GetAdminInfo: failed to load admin with relations, error: %v", err)
return nil, nil, nil, err
}
admin = adminWithRelations
// 批量加载所有角色的权限和菜单,避免 N+1 查询
if len(admin.Roles) > 0 {
var roleIDs []uint
for _, role := range admin.Roles {
roleIDs = append(roleIDs, role.ID)
}
// 批量加载权限
type RolePermission struct {
RoleID uint `gorm:"column:role_id"`
PermissionID uint `gorm:"column:permission_id"`
}
var rolePermissions []RolePermission
if err := facades.Orm().Query().Table("role_permission").Where("role_id IN ?", roleIDs).Find(&rolePermissions); err == nil {
var permissionIDs []uint
rolePermissionMap := make(map[uint][]uint)
for _, rp := range rolePermissions {
rolePermissionMap[rp.RoleID] = append(rolePermissionMap[rp.RoleID], rp.PermissionID)
permissionIDs = append(permissionIDs, rp.PermissionID)
}
if len(permissionIDs) > 0 {
var permissions []models.Permission
if err := facades.Orm().Query().Where("id IN ?", permissionIDs).Find(&permissions); err == nil {
permissionMap := make(map[uint]models.Permission)
for _, perm := range permissions {
permissionMap[perm.ID] = perm
}
for i := range admin.Roles {
if permIDs, ok := rolePermissionMap[admin.Roles[i].ID]; ok {
for _, permID := range permIDs {
if perm, ok := permissionMap[permID]; ok {
admin.Roles[i].Permissions = append(admin.Roles[i].Permissions, perm)
}
}
}
}
}
}
}
// 批量加载菜单
type RoleMenu struct {
RoleID uint `gorm:"column:role_id"`
MenuID uint `gorm:"column:menu_id"`
}
var roleMenus []RoleMenu
if err := facades.Orm().Query().Table("role_menu").Where("role_id IN ?", roleIDs).Find(&roleMenus); err == nil {
var menuIDs []uint
roleMenuMap := make(map[uint][]uint)
for _, rm := range roleMenus {
roleMenuMap[rm.RoleID] = append(roleMenuMap[rm.RoleID], rm.MenuID)
menuIDs = append(menuIDs, rm.MenuID)
}
if len(menuIDs) > 0 {
var menus []models.Menu
if err := facades.Orm().Query().Where("id IN ?", menuIDs).Find(&menus); err == nil {
menuMap := make(map[uint]models.Menu)
for _, menu := range menus {
menuMap[menu.ID] = menu
}
for i := range admin.Roles {
if mIDs, ok := roleMenuMap[admin.Roles[i].ID]; ok {
for _, menuID := range mIDs {
if menu, ok := menuMap[menuID]; ok {
admin.Roles[i].Menus = append(admin.Roles[i].Menus, menu)
}
}
}
}
}
}
}
}
// 检查是否是超级管理员
const SuperAdminRoleSlug = "super-admin"
isSuperAdmin := false
for _, role := range admin.Roles {
if role.Slug == SuperAdminRoleSlug && role.Status == 1 {
isSuperAdmin = true
break
}
}
// 收集所有角色的权限和菜单(去重)
permissionMap := make(map[uint]models.Permission)
menuMap := make(map[uint]models.Menu)
for _, role := range admin.Roles {
for _, perm := range role.Permissions {
permissionMap[perm.ID] = perm
}
for _, menu := range role.Menus {
menuMap[menu.ID] = menu
}
}
// 如果是超级管理员,返回所有菜单(用于前端显示)
// 但不需要返回所有权限,因为权限检查在中间件中会跳过
if isSuperAdmin {
var allMenus []models.Menu
if err := facades.Orm().Query().Where("status", 1).Order("sort ASC").Find(&allMenus); err == nil {
for _, menu := range allMenus {
menuMap[menu.ID] = menu
}
}
}
// 转换为切片
var permissions []models.Permission
var menus []models.Menu
for _, perm := range permissionMap {
permissions = append(permissions, perm)
}
for _, menu := range menuMap {
menus = append(menus, menu)
}
// 检查是否需要隐藏服务监控菜单
// 只有当配置值不为空且不等于 "0" 时才隐藏("0" 表示不隐藏)
monitorHidden := facades.Config().GetString("admin.monitor_hidden", "")
if monitorHidden != "" && monitorHidden != "0" {
// 检查是否是开发者管理员
developerIDsStr := facades.Config().GetString("admin.developer_ids", "2")
isDeveloperAdmin := s.isDeveloperAdmin(admin.ID, developerIDsStr)
// 如果不是开发者管理员,则过滤掉服务监控菜单
if !isDeveloperAdmin {
var filteredMenus []models.Menu
for _, menu := range menus {
if menu.Slug != "monitor" {
filteredMenus = append(filteredMenus, menu)
}
}
menus = filteredMenus
}
}
// 检查是否需要隐藏开发工具菜单
enableDevTool := facades.Config().GetBool("app.enable_dev_tool")
if !enableDevTool {
var filteredMenus []models.Menu
for _, menu := range menus {
if menu.Slug != "dev" {
filteredMenus = append(filteredMenus, menu)
}
}
menus = filteredMenus
}
return &admin, permissions, menus, nil
}
// RecordLoginLog 记录登录日志
func (s *AuthServiceImpl) RecordLoginLog(ctx http.Context, adminID uint, username string, status uint8, message string, request string) error {
ip := ctx.Request().Ip()
// 先创建登录日志记录(Location 字段先为空,避免阻塞登录流程)
loginLog := models.LoginLog{
AdminID: adminID,
Username: username,
IP: ip,
UserAgent: ctx.Request().Header("User-Agent", ""),
Location: "", // 先为空,异步更新
Status: status,
Message: message,
Request: request,
}
if err := facades.Orm().Query().Create(&loginLog); err != nil {
return err
}
// 异步查询 IP 地理位置信息并更新日志记录
// 这样不会阻塞登录流程
go func() {
// 添加 panic 恢复机制
defer func() {
if r := recover(); r != nil {
facades.Log().Errorf("Recovered from panic in IP location update: %v", r)
}
}()
// 添加上下文超时控制(5秒超时)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
location := utils.GetIPLocation(ip)
if location != "" {
// 更新登录日志的 Location 字段
if _, err := facades.Orm().Query().
Model(&models.LoginLog{}).
Where("id", loginLog.ID).
Update("location", location); err != nil {
facades.Log().Errorf("Failed to update login log location: %v", err)
}
}
// 检查上下文是否超时
select {
case <-ctx.Done():
if ctx.Err() == context.DeadlineExceeded {
facades.Log().Errorf("IP location update timeout for login log ID: %d", loginLog.ID)
}
default:
}
}()
return nil
}
// isDeveloperAdmin 检查是否是开发者管理员
func (s *AuthServiceImpl) isDeveloperAdmin(adminID uint, developerIDsStr string) bool {
if developerIDsStr == "" {
return false
}
// 解析开发者ID列表
parts := str.Of(developerIDsStr).Split(",")
for _, part := range parts {
part = str.Of(part).Trim().String()
if !str.Of(part).IsEmpty() {
if id := cast.ToUint(part); id > 0 && id == adminID {
return true
}
}
}
return false
}