Browse Source

feat: login by phone/username, register requires phone, seed uses username

master
dark 1 month ago
parent
commit
d2cb7fa8c8
  1. 12
      backend/base.api
  2. 25
      backend/internal/logic/auth/loginlogic.go
  3. 2
      backend/internal/logic/auth/refreshtokenlogic.go
  4. 32
      backend/internal/logic/auth/registerlogic.go
  5. 304
      backend/internal/logic/auth/ssologic.go
  6. 157
      backend/internal/svc/servicecontext.go
  7. 13
      backend/internal/types/types.go

12
backend/base.api

@ -29,13 +29,13 @@ type (
// 注册请求 // 注册请求
RegisterRequest { RegisterRequest {
Username string `json:"username" validate:"required,min=3,max=32"` // 用户名 Username string `json:"username" validate:"required,min=3,max=32"` // 用户名
Email string `json:"email" validate:"required,email"` // 邮箱
Password string `json:"password" validate:"required,min=6,max=32"` // 密码 Password string `json:"password" validate:"required,min=6,max=32"` // 密码
Phone string `json:"phone,optional"` // 手机号 Phone string `json:"phone" validate:"required"` // 手机号(必填)
Email string `json:"email,optional"` // 邮箱(可选)
} }
// 登录请求 // 登录请求
LoginRequest { LoginRequest {
Email string `json:"email" validate:"required,email"` // 邮箱 Account string `json:"account" validate:"required"` // 手机号或用户名
Password string `json:"password" validate:"required,min=6,max=32"` // 密码 Password string `json:"password" validate:"required,min=6,max=32"` // 密码
} }
// 刷新Token请求 // 刷新Token请求
@ -71,7 +71,7 @@ service base-api {
@server ( @server (
prefix: /api/v1 prefix: /api/v1
group: user group: user
middleware: Cors,Log,Auth middleware: Cors,Log,Auth,Authz
) )
service base-api { service base-api {
// ========== 用户管理接口 ========== // ========== 用户管理接口 ==========
@ -104,7 +104,7 @@ service base-api {
@server ( @server (
prefix: /api/v1 prefix: /api/v1
group: profile group: profile
middleware: Cors,Log,Auth middleware: Cors,Log,Auth,Authz
) )
service base-api { service base-api {
// ========== 个人中心接口 ========== // ========== 个人中心接口 ==========
@ -127,7 +127,7 @@ service base-api {
@server ( @server (
prefix: /api/v1 prefix: /api/v1
group: dashboard group: dashboard
middleware: Cors,Log,Auth middleware: Cors,Log,Auth,Authz
) )
service base-api { service base-api {
// ========== 仪表盘接口 ========== // ========== 仪表盘接口 ==========

25
backend/internal/logic/auth/loginlogic.go

@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/md5" "crypto/md5"
"fmt" "fmt"
"regexp"
"github.com/youruser/base/internal/svc" "github.com/youruser/base/internal/svc"
"github.com/youruser/base/internal/types" "github.com/youruser/base/internal/types"
@ -13,13 +14,14 @@ import (
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
var phoneRegex = regexp.MustCompile(`^\d{11}$`)
type LoginLogic struct { type LoginLogic struct {
logx.Logger logx.Logger
ctx context.Context ctx context.Context
svcCtx *svc.ServiceContext svcCtx *svc.ServiceContext
} }
// 用户登录
func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic {
return &LoginLogic{ return &LoginLogic{
Logger: logx.WithContext(ctx), Logger: logx.WithContext(ctx),
@ -29,8 +31,13 @@ func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic
} }
func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) { func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse, err error) {
// 查询用户 var user *model.User
user, err := model.FindOneByEmail(l.ctx, l.svcCtx.DB, req.Email) if phoneRegex.MatchString(req.Account) {
user, err = model.FindOneByPhone(l.ctx, l.svcCtx.DB, req.Account)
} else {
user, err = model.FindOneByUsername(l.ctx, l.svcCtx.DB, req.Account)
}
if err != nil { if err != nil {
if err == model.ErrNotFound { if err == model.ErrNotFound {
return &types.LoginResponse{ return &types.LoginResponse{
@ -42,7 +49,14 @@ func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse,
return nil, fmt.Errorf("查询用户失败: %v", err) return nil, fmt.Errorf("查询用户失败: %v", err)
} }
// 加密输入的密码并与数据库密码对比 if user.UserType == "casdoor" {
return &types.LoginResponse{
Code: 400,
Message: "该账号已绑定 SSO,请使用 SSO 方式登录",
Success: false,
}, nil
}
inputPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.Password))) inputPassword := fmt.Sprintf("%x", md5.Sum([]byte(req.Password)))
if user.Password != inputPassword { if user.Password != inputPassword {
return &types.LoginResponse{ return &types.LoginResponse{
@ -52,8 +66,7 @@ func (l *LoginLogic) Login(req *types.LoginRequest) (resp *types.LoginResponse,
}, nil }, nil
} }
// 生成 Token token, err := jwt.GenerateToken(user.Id, user.Username, user.Role)
token, err := jwt.GenerateToken(user.Id, user.Username, user.Email)
if err != nil { if err != nil {
return nil, fmt.Errorf("生成Token失败: %v", err) return nil, fmt.Errorf("生成Token失败: %v", err)
} }

2
backend/internal/logic/auth/refreshtokenlogic.go

@ -52,7 +52,7 @@ func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenRequest) (resp *
} }
// 生成新 Token // 生成新 Token
newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Email) newToken, err := jwt.GenerateToken(user.Id, user.Username, user.Role)
if err != nil { if err != nil {
return nil, fmt.Errorf("生成Token失败: %v", err) return nil, fmt.Errorf("生成Token失败: %v", err)
} }

32
backend/internal/logic/auth/registerlogic.go

@ -18,7 +18,6 @@ type RegisterLogic struct {
svcCtx *svc.ServiceContext svcCtx *svc.ServiceContext
} }
// 用户注册
func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic {
return &RegisterLogic{ return &RegisterLogic{
Logger: logx.WithContext(ctx), Logger: logx.WithContext(ctx),
@ -28,50 +27,55 @@ func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Register
} }
func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.UserInfo, err error) { func (l *RegisterLogic) Register(req *types.RegisterRequest) (resp *types.UserInfo, err error) {
// 检查邮箱是否已存在 _, err = model.FindOneByUsername(l.ctx, l.svcCtx.DB, req.Username)
_, err = model.FindOneByEmail(l.ctx, l.svcCtx.DB, req.Email)
if err == nil { if err == nil {
return nil, fmt.Errorf("邮箱已被注册") return nil, fmt.Errorf("用户名已被注册")
} }
if err != model.ErrNotFound { if err != model.ErrNotFound {
return nil, fmt.Errorf("检查邮箱失败: %v", err) return nil, fmt.Errorf("检查用户名失败: %v", err)
}
_, err = model.FindOneByPhone(l.ctx, l.svcCtx.DB, req.Phone)
if err == nil {
return nil, fmt.Errorf("手机号已被注册")
}
if err != model.ErrNotFound {
return nil, fmt.Errorf("检查手机号失败: %v", err)
} }
// 创建用户模型
user := &model.User{ user := &model.User{
Username: req.Username, Username: req.Username,
Email: req.Email, Email: req.Email,
Password: fmt.Sprintf("%x", md5.Sum([]byte(req.Password))), // 密码加密 Password: fmt.Sprintf("%x", md5.Sum([]byte(req.Password))),
Phone: req.Phone, Phone: req.Phone,
Status: 1, // 默认正常状态 Role: model.RoleUser,
Source: model.SourceRegister,
Status: 1,
} }
// 插入数据库
id, err := model.Insert(l.ctx, l.svcCtx.DB, user) id, err := model.Insert(l.ctx, l.svcCtx.DB, user)
if err != nil { if err != nil {
return nil, fmt.Errorf("创建用户失败: %v", err) return nil, fmt.Errorf("创建用户失败: %v", err)
} }
// 查询创建的用户
user, err = model.FindOne(l.ctx, l.svcCtx.DB, id) user, err = model.FindOne(l.ctx, l.svcCtx.DB, id)
if err != nil { if err != nil {
return nil, fmt.Errorf("查询用户失败: %v", err) return nil, fmt.Errorf("查询用户失败: %v", err)
} }
// 返回用户信息(不返回密码)
resp = &types.UserInfo{ resp = &types.UserInfo{
Id: user.Id, Id: user.Id,
Username: user.Username, Username: user.Username,
Email: user.Email, Email: user.Email,
Phone: user.Phone, Phone: user.Phone,
Role: user.Role,
Source: user.Source,
Remark: user.Remark,
Status: int(user.Status), Status: int(user.Status),
CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"), CreatedAt: user.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"), UpdatedAt: user.UpdatedAt.Format("2006-01-02 15:04:05"),
} }
// 返回 Token 在响应头中(通过中间件处理)
// 临时方案:将 token 放入响应 Data 中
l.Infof("注册成功,userId=%d", user.Id) l.Infof("注册成功,userId=%d", user.Id)
return resp, nil return resp, nil
} }

304
backend/internal/logic/auth/ssologic.go

@ -0,0 +1,304 @@
package auth
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/youruser/base/internal/svc"
"github.com/youruser/base/internal/util/jwt"
"github.com/youruser/base/model"
"github.com/zeromicro/go-zero/core/logx"
)
// casdoorHttpClient 用于与 Casdoor 通信的 HTTP 客户端(带超时)
var casdoorHttpClient = &http.Client{Timeout: 10 * time.Second}
type SSOLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewSSOLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SSOLogic {
return &SSOLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
// GetLoginUrl 生成 Casdoor SSO 登录链接
func (l *SSOLogic) GetLoginUrl() (map[string]string, error) {
c := l.svcCtx.Config.Casdoor
state, err := generateState()
if err != nil {
return nil, fmt.Errorf("生成 state 失败: %v", err)
}
loginUrl := fmt.Sprintf("%s/login/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=read&state=%s",
c.Endpoint,
url.QueryEscape(c.ClientId),
url.QueryEscape(c.RedirectUrl),
url.QueryEscape(state),
)
return map[string]string{
"login_url": loginUrl,
}, nil
}
// casdoorTokenResponse Casdoor token 响应
type casdoorTokenResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
Scope string `json:"scope"`
}
// casdoorUserInfo Casdoor 用户信息
type casdoorUserInfo struct {
Sub string `json:"sub"`
Name string `json:"name"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
Phone string `json:"phone"`
Avatar string `json:"avatar"`
}
// HandleCallback 处理 SSO 回调
func (l *SSOLogic) HandleCallback(code, state string) (string, error) {
if code == "" {
return "", fmt.Errorf("缺少授权码")
}
c := l.svcCtx.Config.Casdoor
// 1. 用 code 换取 access_token
accessToken, err := l.exchangeToken(code)
if err != nil {
l.Errorf("SSO token 交换失败: %v", err)
return "", fmt.Errorf("token 交换失败: %v", err)
}
// 2. 获取用户信息
userInfo, err := l.getUserInfo(accessToken)
if err != nil {
l.Errorf("SSO 获取用户信息失败: %v", err)
return "", fmt.Errorf("获取用户信息失败: %v", err)
}
// 3. 查找或创建本地用户
casdoorId := userInfo.Sub
if casdoorId == "" {
casdoorId = userInfo.Name
}
// 从 Casdoor 信息中提取用户名和邮箱
username := userInfo.PreferredUsername
if username == "" {
username = userInfo.Name
}
email := userInfo.Email
if email == "" {
email = username + "@sso.local"
}
localUser, err := model.FindOneByCasdoorId(l.ctx, l.svcCtx.DB, casdoorId)
if err != nil {
if err == model.ErrNotFound {
// 用户不存在,尝试通过邮箱关联已有本地用户
existingUser, findErr := model.FindOneByEmail(l.ctx, l.svcCtx.DB, email)
if findErr == nil {
existingUser.CasdoorId = casdoorId
existingUser.UserType = "casdoor"
if updateErr := model.Update(l.ctx, l.svcCtx.DB, existingUser); updateErr != nil {
return "", fmt.Errorf("关联用户失败: %v", updateErr)
}
localUser = existingUser
l.Infof("SSO 关联已有用户: userId=%d, casdoorId=%s", existingUser.Id, casdoorId)
} else {
// 创建新用户
newUser := &model.User{
Username: username,
Email: email,
Password: "SSO_NO_PASSWORD", // SSO 用户不使用密码登录
Phone: userInfo.Phone,
CasdoorId: casdoorId,
UserType: "casdoor",
Role: model.RoleUser,
Source: model.SourceCasdoor,
Status: 1,
}
_, insertErr := model.Insert(l.ctx, l.svcCtx.DB, newUser)
if insertErr != nil {
l.Errorf("SSO 创建用户失败: %v", insertErr)
return "", fmt.Errorf("创建用户失败: %v", insertErr)
}
localUser = newUser
l.Infof("SSO 新用户创建成功: username=%s, casdoorId=%s", username, casdoorId)
}
} else {
return "", fmt.Errorf("查询用户失败: %v", err)
}
} else {
// 已有用户,同步更新 Casdoor 端的最新信息
updated := false
if username != "" && localUser.Username != username {
localUser.Username = username
updated = true
}
if email != "" && localUser.Email != email {
localUser.Email = email
updated = true
}
if userInfo.Phone != "" && localUser.Phone != userInfo.Phone {
localUser.Phone = userInfo.Phone
updated = true
}
if updated {
if updateErr := model.Update(l.ctx, l.svcCtx.DB, localUser); updateErr != nil {
l.Errorf("SSO 同步用户信息失败: %v", updateErr)
}
}
}
// 4. 生成本地 JWT Token
token, err := jwt.GenerateToken(localUser.Id, localUser.Username, localUser.Role)
if err != nil {
return "", fmt.Errorf("生成 Token 失败: %v", err)
}
l.Infof("SSO 登录成功: userId=%d, username=%s", localUser.Id, localUser.Username)
// 5. 构建前端回调 URL
redirectUrl := fmt.Sprintf("%s/sso/callback?token=%s",
c.FrontendUrl,
url.QueryEscape(token),
)
return redirectUrl, nil
}
// exchangeToken 用授权码换取 access_token
func (l *SSOLogic) exchangeToken(code string) (string, error) {
c := l.svcCtx.Config.Casdoor
tokenUrl := fmt.Sprintf("%s/api/login/oauth/access_token", c.Endpoint)
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", c.ClientId)
data.Set("client_secret", c.ClientSecret)
data.Set("code", code)
data.Set("redirect_uri", c.RedirectUrl)
req, err := http.NewRequestWithContext(l.ctx, http.MethodPost, tokenUrl,
strings.NewReader(data.Encode()))
if err != nil {
return "", fmt.Errorf("创建请求失败: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := casdoorHttpClient.Do(req)
if err != nil {
return "", fmt.Errorf("请求 token 失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("token 请求返回 %d: %s", resp.StatusCode, string(body))
}
var tokenResp casdoorTokenResponse
if err := json.Unmarshal(body, &tokenResp); err != nil {
return "", fmt.Errorf("解析 token 响应失败: %v", err)
}
if tokenResp.AccessToken == "" {
return "", fmt.Errorf("未获取到 access_token, 响应: %s", string(body))
}
return tokenResp.AccessToken, nil
}
// getUserInfo 从 access_token JWT 中解析用户信息
// Casdoor 的 access_token 本身是一个 JWT,包含完整的用户 claims
func (l *SSOLogic) getUserInfo(accessToken string) (*casdoorUserInfo, error) {
// 解析 JWT payload(不验证签名,因为 token 刚从 Casdoor 获取)
parts := strings.Split(accessToken, ".")
if len(parts) != 3 {
return nil, fmt.Errorf("access_token 不是有效的 JWT 格式")
}
// Base64 解码 payload
payload := parts[1]
// 补齐 base64 padding
switch len(payload) % 4 {
case 2:
payload += "=="
case 3:
payload += "="
}
decoded, err := base64.URLEncoding.DecodeString(payload)
if err != nil {
return nil, fmt.Errorf("解码 JWT payload 失败: %v", err)
}
// 解析 JWT claims
var claims map[string]interface{}
if err := json.Unmarshal(decoded, &claims); err != nil {
return nil, fmt.Errorf("解析 JWT claims 失败: %v", err)
}
// 从 claims 中提取用户信息(Casdoor JWT 字段名)
userInfo := &casdoorUserInfo{
Sub: getStringClaim(claims, "sub"),
}
// Casdoor JWT 中用户名可能在 name 或 preferred_username 字段
userInfo.Name = getStringClaim(claims, "name")
userInfo.PreferredUsername = getStringClaim(claims, "preferred_username")
userInfo.Email = getStringClaim(claims, "email")
userInfo.Phone = getStringClaim(claims, "phone")
userInfo.Avatar = getStringClaim(claims, "avatar")
return userInfo, nil
}
// getStringClaim 从 claims map 中安全获取字符串值
func getStringClaim(claims map[string]interface{}, key string) string {
if v, ok := claims[key]; ok {
if s, ok := v.(string); ok {
return s
}
}
return ""
}
// generateState 生成随机 state 参数(CSRF 防护)
func generateState() (string, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}

157
backend/internal/svc/servicecontext.go

@ -4,6 +4,15 @@
package svc package svc
import ( import (
"context"
"crypto/md5"
"fmt"
"log"
"github.com/casbin/casbin/v2"
casbinmodel "github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/youruser/base/internal/config" "github.com/youruser/base/internal/config"
"github.com/youruser/base/internal/middleware" "github.com/youruser/base/internal/middleware"
"github.com/youruser/base/model" "github.com/youruser/base/model"
@ -14,13 +23,33 @@ import (
"github.com/zeromicro/go-zero/rest" "github.com/zeromicro/go-zero/rest"
) )
const casbinModelText = `
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act
`
type ServiceContext struct { type ServiceContext struct {
Config config.Config Config config.Config
Cors rest.Middleware Cors rest.Middleware
Log rest.Middleware Log rest.Middleware
Auth rest.Middleware Auth rest.Middleware
Authz rest.Middleware
// 数据库连接 // 数据库连接
DB *gorm.DB DB *gorm.DB
// Casbin enforcer
Enforcer *casbin.Enforcer
} }
func NewServiceContext(c config.Config) *ServiceContext { func NewServiceContext(c config.Config) *ServiceContext {
@ -37,12 +66,23 @@ func NewServiceContext(c config.Config) *ServiceContext {
panic("Failed to migrate database: " + err.Error()) panic("Failed to migrate database: " + err.Error())
} }
// 初始化 Casbin
enforcer := initCasbin(db)
// 种子超级管理员
seedSuperAdmin(db)
// 种子 Casbin 策略
seedCasbinPolicies(enforcer)
return &ServiceContext{ return &ServiceContext{
Config: c, Config: c,
Cors: middleware.NewCorsMiddleware().Handle, Cors: middleware.NewCorsMiddleware().Handle,
Log: middleware.NewLogMiddleware().Handle, Log: middleware.NewLogMiddleware().Handle,
Auth: middleware.NewAuthMiddleware().Handle, Auth: middleware.NewAuthMiddleware().Handle,
DB: db, Authz: middleware.NewAuthzMiddleware(enforcer).Handle,
DB: db,
Enforcer: enforcer,
} }
} }
@ -57,3 +97,110 @@ func (s *ServiceContext) Close() error {
} }
return nil return nil
} }
// initCasbin 初始化 Casbin enforcer
func initCasbin(db *gorm.DB) *casbin.Enforcer {
// 使用 GORM adapter(自动创建 casbin_rule 表)
adapter, err := gormadapter.NewAdapterByDB(db)
if err != nil {
panic("Failed to create Casbin adapter: " + err.Error())
}
// 从字符串加载 model
m, err := casbinmodel.NewModelFromString(casbinModelText)
if err != nil {
panic("Failed to create Casbin model: " + err.Error())
}
enforcer, err := casbin.NewEnforcer(m, adapter)
if err != nil {
panic("Failed to create Casbin enforcer: " + err.Error())
}
// 加载策略
if err := enforcer.LoadPolicy(); err != nil {
panic("Failed to load Casbin policy: " + err.Error())
}
log.Println("[Casbin] Enforcer initialized successfully")
return enforcer
}
// seedSuperAdmin 首次启动创建超级管理员
func seedSuperAdmin(db *gorm.DB) {
ctx := context.Background()
existing, err := model.FindOneByUsername(ctx, db, "admin")
if err == nil {
if existing.Role != model.RoleSuperAdmin {
existing.Role = model.RoleSuperAdmin
existing.Source = model.SourceSystem
model.Update(ctx, db, existing)
log.Println("[Seed] Updated admin to super_admin role")
}
return
}
password := fmt.Sprintf("%x", md5.Sum([]byte("admin123")))
admin := &model.User{
Username: "admin",
Phone: "13800000000",
Email: "",
Password: password,
Role: model.RoleSuperAdmin,
Source: model.SourceSystem,
Remark: "系统自动创建的超级管理员",
Status: 1,
}
_, err = model.Insert(ctx, db, admin)
if err != nil {
log.Printf("[Seed] Failed to create super admin: %v", err)
return
}
log.Println("[Seed] Super admin created: admin / admin123")
}
// seedCasbinPolicies 种子 Casbin 策略(幂等)
func seedCasbinPolicies(enforcer *casbin.Enforcer) {
// 角色层级: super_admin > admin > user > guest
roleHierarchy := [][]string{
{"super_admin", "admin"},
{"admin", "user"},
{"user", "guest"},
}
for _, g := range roleHierarchy {
if has, _ := enforcer.HasGroupingPolicy(g[0], g[1]); !has {
enforcer.AddGroupingPolicy(g[0], g[1])
}
}
// 默认策略
policies := [][]string{
// guest: 仪表盘只读
{"guest", "/api/v1/dashboard/*", "GET"},
// user: 个人中心
{"user", "/api/v1/profile/*", "GET"},
{"user", "/api/v1/profile/*", "PUT"},
{"user", "/api/v1/profile/*", "POST"},
// admin: 用户管理(增查改)
{"admin", "/api/v1/users", "GET"},
{"admin", "/api/v1/user", "POST"},
{"admin", "/api/v1/user/:id", "GET"},
{"admin", "/api/v1/user/:id", "PUT"},
// super_admin: 用户删除
{"super_admin", "/api/v1/user/:id", "DELETE"},
}
for _, p := range policies {
if has, _ := enforcer.HasPolicy(p[0], p[1], p[2]); !has {
enforcer.AddPolicy(p[0], p[1], p[2])
}
}
enforcer.SavePolicy()
log.Println("[Casbin] Policies seeded successfully")
}

13
backend/internal/types/types.go

@ -21,6 +21,8 @@ type CreateUserRequest struct {
Email string `json:"email" validate:"required,email"` // 邮箱 Email string `json:"email" validate:"required,email"` // 邮箱
Password string `json:"password" validate:"required,min=6,max=32"` // 密码 Password string `json:"password" validate:"required,min=6,max=32"` // 密码
Phone string `json:"phone,optional"` // 手机号 Phone string `json:"phone,optional"` // 手机号
Role string `json:"role,optional"` // 角色
Remark string `json:"remark,optional"` // 备注
} }
type DashboardStatsResponse struct { type DashboardStatsResponse struct {
@ -52,7 +54,7 @@ type GetUserRequest struct {
} }
type LoginRequest struct { type LoginRequest struct {
Email string `json:"email" validate:"required,email"` // 邮箱 Account string `json:"account" validate:"required"` // 手机号或用户名
Password string `json:"password" validate:"required,min=6,max=32"` // 密码 Password string `json:"password" validate:"required,min=6,max=32"` // 密码
} }
@ -77,9 +79,9 @@ type RefreshTokenRequest struct {
type RegisterRequest struct { type RegisterRequest struct {
Username string `json:"username" validate:"required,min=3,max=32"` // 用户名 Username string `json:"username" validate:"required,min=3,max=32"` // 用户名
Email string `json:"email" validate:"required,email"` // 邮箱
Password string `json:"password" validate:"required,min=6,max=32"` // 密码 Password string `json:"password" validate:"required,min=6,max=32"` // 密码
Phone string `json:"phone,optional"` // 手机号 Phone string `json:"phone" validate:"required"` // 手机号(必填)
Email string `json:"email,optional"` // 邮箱(可选)
} }
type Response struct { type Response struct {
@ -102,6 +104,8 @@ type UpdateUserRequest struct {
Email string `json:"email,optional"` // 邮箱 Email string `json:"email,optional"` // 邮箱
Phone string `json:"phone,optional"` // 手机号 Phone string `json:"phone,optional"` // 手机号
Status int `json:"status,optional"` // 状态 Status int `json:"status,optional"` // 状态
Role string `json:"role,optional"` // 角色
Remark string `json:"remark,optional"` // 备注
} }
type UserInfo struct { type UserInfo struct {
@ -109,6 +113,9 @@ type UserInfo struct {
Username string `json:"username"` // 用户名 Username string `json:"username"` // 用户名
Email string `json:"email"` // 邮箱 Email string `json:"email"` // 邮箱
Phone string `json:"phone"` // 手机号 Phone string `json:"phone"` // 手机号
Role string `json:"role"` // 角色
Source string `json:"source"` // 来源
Remark string `json:"remark"` // 备注
Status int `json:"status"` // 状态 1-正常 2-禁用 Status int `json:"status"` // 状态 1-正常 2-禁用
CreatedAt string `json:"createdAt"` // 创建时间 CreatedAt string `json:"createdAt"` // 创建时间
UpdatedAt string `json:"updatedAt"` // 更新时间 UpdatedAt string `json:"updatedAt"` // 更新时间

Loading…
Cancel
Save