diff --git a/backend/go-service/app/auth/dao/role_dao.go b/backend/go-service/app/auth/dao/role_dao.go new file mode 100644 index 0000000..842724d --- /dev/null +++ b/backend/go-service/app/auth/dao/role_dao.go @@ -0,0 +1,119 @@ +package dao + +import ( + "context" + + "github.com/echochat/backend/app/auth/model" + "github.com/echochat/backend/pkg/logs" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// RoleDAO 角色数据访问对象 +type RoleDAO struct { + db *gorm.DB +} + +// NewRoleDAO 创建 RoleDAO 实例 +func NewRoleDAO(db *gorm.DB) *RoleDAO { + return &RoleDAO{db: db} +} + +// FindByCode 按角色代码查询角色 +// 利用 code 唯一索引,精确匹配 +func (d *RoleDAO) FindByCode(ctx context.Context, code string) (*model.Role, error) { + funcName := "dao.role_dao.FindByCode" + logs.Debug(ctx, funcName, "按代码查询角色", zap.String("code", code)) + + var role model.Role + err := d.db.WithContext(ctx).Where("code = ?", code).First(&role).Error + if err != nil { + return nil, err + } + return &role, nil +} + +// AssignRole 为用户分配角色 +// 插入 user_id + role_id 关联记录 +func (d *RoleDAO) AssignRole(ctx context.Context, userID int64, roleID int) error { + funcName := "dao.role_dao.AssignRole" + logs.Info(ctx, funcName, "分配角色", + zap.Int64("user_id", userID), + zap.Int("role_id", roleID), + ) + + var err error + defer func() { + if err != nil { + logs.Error(ctx, funcName, "分配角色失败", zap.Error(err)) + } + }() + + userRole := model.UserRole{ + UserID: userID, + RoleID: roleID, + } + err = d.db.WithContext(ctx).Create(&userRole).Error + return err +} + +// GetUserRoles 获取用户的所有角色列表 +// 通过 JOIN auth_roles 表获取角色完整信息 +func (d *RoleDAO) GetUserRoles(ctx context.Context, userID int64) ([]model.Role, error) { + funcName := "dao.role_dao.GetUserRoles" + logs.Debug(ctx, funcName, "获取用户角色", zap.Int64("user_id", userID)) + + var roles []model.Role + err := d.db.WithContext(ctx). + Joins("JOIN auth_user_roles ON auth_user_roles.role_id = auth_roles.id"). + Where("auth_user_roles.user_id = ?", userID). + Find(&roles).Error + if err != nil { + return nil, err + } + return roles, nil +} + +// GetUserRoleCodes 获取用户的角色代码列表(便于权限判断) +func (d *RoleDAO) GetUserRoleCodes(ctx context.Context, userID int64) ([]string, error) { + roles, err := d.GetUserRoles(ctx, userID) + if err != nil { + return nil, err + } + + codes := make([]string, 0, len(roles)) + for _, r := range roles { + codes = append(codes, r.Code) + } + return codes, nil +} + +// HasRole 检查用户是否拥有指定角色 +func (d *RoleDAO) HasRole(ctx context.Context, userID int64, roleCode string) (bool, error) { + funcName := "dao.role_dao.HasRole" + logs.Debug(ctx, funcName, "检查用户角色", + zap.Int64("user_id", userID), + zap.String("role_code", roleCode), + ) + + var count int64 + err := d.db.WithContext(ctx). + Model(&model.UserRole{}). + Joins("JOIN auth_roles ON auth_roles.id = auth_user_roles.role_id"). + Where("auth_user_roles.user_id = ? AND auth_roles.code = ?", userID, roleCode). + Count(&count).Error + if err != nil { + return false, err + } + return count > 0, nil +} + +// RemoveUserRoles 移除用户的所有角色(用于重新分配) +func (d *RoleDAO) RemoveUserRoles(ctx context.Context, userID int64) error { + funcName := "dao.role_dao.RemoveUserRoles" + logs.Info(ctx, funcName, "移除用户所有角色", zap.Int64("user_id", userID)) + + return d.db.WithContext(ctx). + Where("user_id = ?", userID). + Delete(&model.UserRole{}).Error +} diff --git a/backend/go-service/app/auth/dao/user_dao.go b/backend/go-service/app/auth/dao/user_dao.go new file mode 100644 index 0000000..34f15bc --- /dev/null +++ b/backend/go-service/app/auth/dao/user_dao.go @@ -0,0 +1,131 @@ +// Package dao 提供 auth 模块的数据库访问操作 +package dao + +import ( + "context" + + "github.com/echochat/backend/app/auth/model" + "github.com/echochat/backend/pkg/logs" + "go.uber.org/zap" + "gorm.io/gorm" +) + +// UserDAO 用户数据访问对象 +type UserDAO struct { + db *gorm.DB +} + +// NewUserDAO 创建 UserDAO 实例 +func NewUserDAO(db *gorm.DB) *UserDAO { + return &UserDAO{db: db} +} + +// Create 创建用户 +// 插入一条新用户记录,username 和 email 有唯一索引 +func (d *UserDAO) Create(ctx context.Context, user *model.User) error { + funcName := "dao.user_dao.Create" + logs.Info(ctx, funcName, "创建用户", + zap.String("username", user.Username), + zap.String("email", logs.MaskEmail(user.Email)), + ) + + var err error + defer func() { + if err != nil { + logs.Error(ctx, funcName, "创建用户失败", zap.Error(err)) + } else { + logs.Info(ctx, funcName, "创建用户成功", zap.Int64("user_id", user.ID)) + } + }() + + err = d.db.WithContext(ctx).Create(user).Error + return err +} + +// FindByEmail 按邮箱查询用户 +// 精确匹配邮箱地址,利用 email 唯一索引 +func (d *UserDAO) FindByEmail(ctx context.Context, email string) (*model.User, error) { + funcName := "dao.user_dao.FindByEmail" + logs.Debug(ctx, funcName, "按邮箱查询用户", zap.String("email", logs.MaskEmail(email))) + + var user model.User + err := d.db.WithContext(ctx).Where("email = ?", email).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// FindByUsername 按用户名查询用户 +// 精确匹配用户名,利用 username 唯一索引 +func (d *UserDAO) FindByUsername(ctx context.Context, username string) (*model.User, error) { + funcName := "dao.user_dao.FindByUsername" + logs.Debug(ctx, funcName, "按用户名查询用户", zap.String("username", username)) + + var user model.User + err := d.db.WithContext(ctx).Where("username = ?", username).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// FindByID 按 ID 查询用户 +func (d *UserDAO) FindByID(ctx context.Context, id int64) (*model.User, error) { + funcName := "dao.user_dao.FindByID" + logs.Debug(ctx, funcName, "按ID查询用户", zap.Int64("id", id)) + + var user model.User + err := d.db.WithContext(ctx).First(&user, id).Error + if err != nil { + return nil, err + } + return &user, nil +} + +// Update 更新用户信息 +// 仅更新非零值字段 +func (d *UserDAO) Update(ctx context.Context, user *model.User) error { + funcName := "dao.user_dao.Update" + logs.Info(ctx, funcName, "更新用户信息", zap.Int64("user_id", user.ID)) + + var err error + defer func() { + if err != nil { + logs.Error(ctx, funcName, "更新用户失败", zap.Int64("user_id", user.ID), zap.Error(err)) + } + }() + + err = d.db.WithContext(ctx).Save(user).Error + return err +} + +// UpdateLastLogin 更新最后登录信息 +func (d *UserDAO) UpdateLastLogin(ctx context.Context, userID int64, ip string) error { + funcName := "dao.user_dao.UpdateLastLogin" + logs.Debug(ctx, funcName, "更新登录信息", zap.Int64("user_id", userID)) + + err := d.db.WithContext(ctx). + Model(&model.User{}). + Where("id = ?", userID). + Updates(map[string]interface{}{ + "last_login_at": gorm.Expr("NOW()"), + "last_login_ip": ip, + }).Error + return err +} + +// FindByAccount 按用户名或邮箱查找用户(登录时使用) +func (d *UserDAO) FindByAccount(ctx context.Context, account string) (*model.User, error) { + funcName := "dao.user_dao.FindByAccount" + logs.Debug(ctx, funcName, "按账号查询用户", zap.String("account", account)) + + var user model.User + err := d.db.WithContext(ctx). + Where("username = ? OR email = ?", account, account). + First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} diff --git a/backend/go-service/app/auth/model/role.go b/backend/go-service/app/auth/model/role.go new file mode 100644 index 0000000..8bbf132 --- /dev/null +++ b/backend/go-service/app/auth/model/role.go @@ -0,0 +1,29 @@ +package model + +import "time" + +// Role 角色表模型,对应 auth_roles 表 +type Role struct { + ID int `json:"id" gorm:"primaryKey;autoIncrement"` + Code string `json:"code" gorm:"uniqueIndex;size:50;not null"` // 角色代码: user/admin/super_admin + Name string `json:"name" gorm:"size:50;not null"` // 角色显示名称 + Description string `json:"description" gorm:"size:200;default:''"` + CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` +} + +// TableName 指定数据库表名 +func (Role) TableName() string { + return "auth_roles" +} + +// UserRole 用户角色关联表模型,对应 auth_user_roles 表 +type UserRole struct { + UserID int64 `json:"user_id" gorm:"primaryKey"` + RoleID int `json:"role_id" gorm:"primaryKey"` + CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` +} + +// TableName 指定数据库表名 +func (UserRole) TableName() string { + return "auth_user_roles" +} diff --git a/backend/go-service/app/auth/model/user.go b/backend/go-service/app/auth/model/user.go new file mode 100644 index 0000000..6e18af2 --- /dev/null +++ b/backend/go-service/app/auth/model/user.go @@ -0,0 +1,26 @@ +// Package model 定义 auth 模块的数据库模型 +package model + +import "time" + +// User 用户主表模型,对应 auth_users 表 +type User struct { + ID int64 `json:"id" gorm:"primaryKey;autoIncrement"` + Username string `json:"username" gorm:"uniqueIndex;size:50;not null"` + Email string `json:"email" gorm:"uniqueIndex;size:100;not null"` + PasswordHash string `json:"-" gorm:"column:password_hash;size:255;not null"` // JSON 序列化时隐藏密码 + Nickname string `json:"nickname" gorm:"size:50;not null;default:''"` + Avatar string `json:"avatar" gorm:"size:500;not null;default:''"` + Gender int `json:"gender" gorm:"not null;default:0"` // 0=未知, 1=男, 2=女 + Phone *string `json:"phone" gorm:"size:20"` // 可选字段 + Status int `json:"status" gorm:"not null;default:1"` // 1=正常, 2=禁用, 3=注销 + LastLoginAt *time.Time `json:"last_login_at"` + LastLoginIP *string `json:"last_login_ip" gorm:"column:last_login_ip;size:50"` + CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime"` +} + +// TableName 指定数据库表名 +func (User) TableName() string { + return "auth_users" +} diff --git a/backend/go-service/app/auth/provider.go b/backend/go-service/app/auth/provider.go new file mode 100644 index 0000000..df8a90c --- /dev/null +++ b/backend/go-service/app/auth/provider.go @@ -0,0 +1,13 @@ +// Package auth 提供用户认证与授权模块 +package auth + +import ( + "github.com/echochat/backend/app/auth/dao" + "github.com/google/wire" +) + +// AuthSet Auth 模块依赖注入 Provider Set +var AuthSet = wire.NewSet( + dao.NewUserDAO, + dao.NewRoleDAO, +) diff --git a/backend/go-service/app/constants/role_code.go b/backend/go-service/app/constants/role_code.go new file mode 100644 index 0000000..c6da2f0 --- /dev/null +++ b/backend/go-service/app/constants/role_code.go @@ -0,0 +1,15 @@ +package constants + +// 角色代码 +const ( + RoleUser = "user" // 普通用户 + RoleAdmin = "admin" // 管理员 + RoleSuperAdmin = "super_admin" // 超级管理员 +) + +// RoleNameMap 角色代码到中文名的映射 +var RoleNameMap = map[string]string{ + RoleUser: "普通用户", + RoleAdmin: "管理员", + RoleSuperAdmin: "超级管理员", +} diff --git a/backend/go-service/app/constants/user_status.go b/backend/go-service/app/constants/user_status.go new file mode 100644 index 0000000..0bb5358 --- /dev/null +++ b/backend/go-service/app/constants/user_status.go @@ -0,0 +1,23 @@ +// Package constants 定义系统全局常量 +package constants + +// 用户账号状态 +const ( + UserStatusActive = 1 // 正常 + UserStatusDisabled = 2 // 禁用(管理员封禁) + UserStatusDeleted = 3 // 注销(用户主动注销) +) + +// UserStatusMap 用户状态中文映射 +var UserStatusMap = map[int]string{ + UserStatusActive: "正常", + UserStatusDisabled: "禁用", + UserStatusDeleted: "注销", +} + +// 用户性别 +const ( + GenderUnknown = 0 // 未知 + GenderMale = 1 // 男 + GenderFemale = 2 // 女 +)