From aee3455e9a886a991761316e466ea33686d43920 Mon Sep 17 00:00:00 2001 From: yml Date: Fri, 22 May 2026 16:50:33 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=205=20=E9=98=B6=E6=AE=B5=EF=BC=9A?= =?UTF-8?q?=E7=BA=A0=E7=BA=B7=E3=80=81=E9=80=9A=E7=9F=A5=E4=B8=8E=E5=90=8E?= =?UTF-8?q?=E5=8F=B0-2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + backend/go.mod | 2 +- backend/internal/middleware/auth.go | 31 +++++- backend/internal/model/admin_user.go | 18 ++++ backend/internal/modules/adminauth/dto.go | 25 +++++ backend/internal/modules/adminauth/handler.go | 70 +++++++++++++ .../internal/modules/adminauth/repository.go | 98 +++++++++++++++++++ backend/internal/modules/adminauth/service.go | 34 +++++++ backend/internal/modules/auth/jwt.go | 33 +++++-- backend/internal/modules/auth/service.go | 2 +- backend/internal/modules/dispute/handler.go | 13 ++- .../internal/modules/systemconfig/handler.go | 14 +-- backend/internal/router/router.go | 17 +++- docs/api.md | 3 + docs/business-rules.md | 9 +- docs/project-plan.md | 19 ++-- frontend/src/api/adminAuth.ts | 42 ++++++++ frontend/src/api/client.ts | 4 +- frontend/src/layouts/AppLayout.vue | 3 +- frontend/src/router/index.ts | 11 +++ frontend/src/stores/adminSession.ts | 50 ++++++++++ frontend/src/views/admin/AdminLoginView.vue | 56 +++++++++++ 22 files changed, 519 insertions(+), 36 deletions(-) create mode 100644 backend/internal/model/admin_user.go create mode 100644 backend/internal/modules/adminauth/dto.go create mode 100644 backend/internal/modules/adminauth/handler.go create mode 100644 backend/internal/modules/adminauth/repository.go create mode 100644 backend/internal/modules/adminauth/service.go create mode 100644 frontend/src/api/adminAuth.ts create mode 100644 frontend/src/stores/adminSession.ts create mode 100644 frontend/src/views/admin/AdminLoginView.vue diff --git a/README.md b/README.md index 085f85e..4e58529 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ npm run dev - 站内信已支持订单关键节点自动写入,可通过 `GET /api/notifications` 查看。 - 申诉仲裁已支持订单双方发起申诉、开发态后台处理,后台页面为 `http://localhost:5173/admin/disputes`。 - 系统配置已支持默认配置初始化和后台编辑,页面为 `http://localhost:5173/admin/system-configs`,更新会写入审计日志。 +- 后台已使用独立登录,页面为 `http://localhost:5173/admin/login`;开发态默认管理员为 `admin / admin123456`。 ## 文档 diff --git a/backend/go.mod b/backend/go.mod index ee14c42..c7ed384 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,6 +7,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 github.com/redis/go-redis/v9 v9.17.0 go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.42.0 gorm.io/datatypes v1.2.7 gorm.io/driver/mysql v1.6.0 gorm.io/gorm v1.31.1 @@ -44,7 +45,6 @@ require ( go.uber.org/mock v0.5.0 // indirect go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.42.0 // indirect golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect diff --git a/backend/internal/middleware/auth.go b/backend/internal/middleware/auth.go index 124f847..ef2090f 100644 --- a/backend/internal/middleware/auth.go +++ b/backend/internal/middleware/auth.go @@ -10,8 +10,10 @@ import ( ) const ( - ContextUserID = "user_id" - ContextPhone = "phone" + ContextUserID = "user_id" + ContextPhone = "phone" + ContextAdminID = "admin_id" + ContextUsername = "username" ) func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc { @@ -24,7 +26,7 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc { return } - claims, err := jwtManager.Parse(tokenText, "access") + claims, err := jwtManager.ParseSubject(tokenText, "access", "user") if err != nil { response.Unauthorized(c, "访问令牌无效或已过期") c.Abort() @@ -36,3 +38,26 @@ func Auth(jwtManager *auth.JWTManager) gin.HandlerFunc { c.Next() } } + +func AdminAuth(jwtManager *auth.JWTManager) gin.HandlerFunc { + return func(c *gin.Context) { + header := c.GetHeader("Authorization") + tokenText := strings.TrimSpace(strings.TrimPrefix(header, "Bearer ")) + if tokenText == "" || tokenText == header { + response.Unauthorized(c, "缺少后台访问令牌") + c.Abort() + return + } + + claims, err := jwtManager.ParseSubject(tokenText, "access", "admin") + if err != nil { + response.Unauthorized(c, "后台访问令牌无效或已过期") + c.Abort() + return + } + + c.Set(ContextAdminID, claims.UserID) + c.Set(ContextUsername, claims.Phone) + c.Next() + } +} diff --git a/backend/internal/model/admin_user.go b/backend/internal/model/admin_user.go new file mode 100644 index 0000000..733453b --- /dev/null +++ b/backend/internal/model/admin_user.go @@ -0,0 +1,18 @@ +package model + +import "time" + +type AdminUser struct { + ID uint64 `gorm:"primaryKey" json:"id"` + Username string `gorm:"size:64;not null;uniqueIndex" json:"username"` + PasswordHash string `gorm:"size:255;not null" json:"-"` + Nickname string `gorm:"size:64;not null;default:''" json:"nickname"` + Status string `gorm:"size:32;not null;default:'active'" json:"status"` + LastLoginAt *time.Time `json:"last_login_at"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (AdminUser) TableName() string { + return "admin_users" +} diff --git a/backend/internal/modules/adminauth/dto.go b/backend/internal/modules/adminauth/dto.go new file mode 100644 index 0000000..a03c04f --- /dev/null +++ b/backend/internal/modules/adminauth/dto.go @@ -0,0 +1,25 @@ +package adminauth + +import ( + "time" + + "hfb_sys/backend/internal/modules/auth" +) + +type AdminDTO struct { + ID uint64 `json:"id"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Status string `json:"status"` + LastLoginAt *time.Time `json:"last_login_at"` +} + +type LoginRequest struct { + Username string `json:"username" binding:"required"` + Password string `json:"password" binding:"required"` +} + +type LoginResult struct { + Admin AdminDTO `json:"admin"` + Tokens auth.TokenPair `json:"tokens"` +} diff --git a/backend/internal/modules/adminauth/handler.go b/backend/internal/modules/adminauth/handler.go new file mode 100644 index 0000000..bf047ee --- /dev/null +++ b/backend/internal/modules/adminauth/handler.go @@ -0,0 +1,70 @@ +package adminauth + +import ( + "errors" + "net/http" + "strings" + + "hfb_sys/backend/internal/middleware" + "hfb_sys/backend/pkg/response" + + "github.com/gin-gonic/gin" +) + +type Handler struct { + service *Service +} + +func NewHandler(service *Service) *Handler { + return &Handler{service: service} +} + +func (h *Handler) Login(c *gin.Context) { + var req LoginRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "用户名和密码不能为空") + return + } + result, err := h.service.Login(strings.TrimSpace(req.Username), req.Password) + if err != nil { + writeAdminAuthError(c, err) + return + } + response.OK(c, result) +} + +func (h *Handler) Me(c *gin.Context) { + value, ok := c.Get(middleware.ContextAdminID) + if !ok { + response.Unauthorized(c, "缺少管理员上下文") + return + } + adminID, ok := value.(uint64) + if !ok { + response.Unauthorized(c, "管理员上下文无效") + return + } + admin, err := h.service.Me(adminID) + if err != nil { + writeAdminAuthError(c, err) + return + } + response.OK(c, admin) +} + +func (h *Handler) Logout(c *gin.Context) { + response.OK(c, gin.H{"logged_out": true}) +} + +func writeAdminAuthError(c *gin.Context, err error) { + switch { + case errors.Is(err, ErrDependencyUnavailable): + response.ServiceUnavailable(c, "数据库未连接") + case errors.Is(err, ErrInvalidCredential): + response.BadRequest(c, "用户名或密码错误") + case errors.Is(err, ErrAdminDisabled): + response.Error(c, http.StatusForbidden, "admin_disabled", "管理员已禁用") + default: + response.Error(c, http.StatusInternalServerError, "internal_error", "后台认证服务暂时不可用") + } +} diff --git a/backend/internal/modules/adminauth/repository.go b/backend/internal/modules/adminauth/repository.go new file mode 100644 index 0000000..d62a0ee --- /dev/null +++ b/backend/internal/modules/adminauth/repository.go @@ -0,0 +1,98 @@ +package adminauth + +import ( + "errors" + "time" + + "hfb_sys/backend/internal/model" + "hfb_sys/backend/internal/modules/auth" + + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +const ( + defaultAdminUsername = "admin" + defaultAdminPassword = "admin123456" +) + +type Repository struct { + db *gorm.DB + jwt *auth.JWTManager +} + +func NewRepository(db *gorm.DB, jwt *auth.JWTManager) *Repository { + return &Repository{db: db, jwt: jwt} +} + +func (r *Repository) Login(username string, password string) (LoginResult, error) { + if err := r.ensureDefaultAdmin(); err != nil { + return LoginResult{}, err + } + var admin model.AdminUser + if err := r.db.Where("username = ?", username).First(&admin).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return LoginResult{}, ErrInvalidCredential + } + return LoginResult{}, err + } + if admin.Status != "active" { + return LoginResult{}, ErrAdminDisabled + } + if err := bcrypt.CompareHashAndPassword([]byte(admin.PasswordHash), []byte(password)); err != nil { + return LoginResult{}, ErrInvalidCredential + } + now := time.Now() + admin.LastLoginAt = &now + if err := r.db.Save(&admin).Error; err != nil { + return LoginResult{}, err + } + tokens, err := r.jwt.GenerateSubjectPair(admin.ID, admin.Username, "admin") + if err != nil { + return LoginResult{}, err + } + return LoginResult{Admin: toDTO(admin), Tokens: tokens}, nil +} + +func (r *Repository) FindByID(id uint64) (*AdminDTO, error) { + var admin model.AdminUser + if err := r.db.First(&admin, id).Error; err != nil { + return nil, err + } + if admin.Status != "active" { + return nil, ErrAdminDisabled + } + dto := toDTO(admin) + return &dto, nil +} + +func (r *Repository) ensureDefaultAdmin() error { + var count int64 + if err := r.db.Model(&model.AdminUser{}).Count(&count).Error; err != nil { + return err + } + if count > 0 { + return nil + } + hash, err := bcrypt.GenerateFromPassword([]byte(defaultAdminPassword), bcrypt.DefaultCost) + if err != nil { + return err + } + admin := model.AdminUser{ + Username: defaultAdminUsername, + PasswordHash: string(hash), + Nickname: "超级管理员", + Status: "active", + } + return r.db.Create(&admin).Error +} + +func toDTO(admin model.AdminUser) AdminDTO { + return AdminDTO{ + ID: admin.ID, + Username: admin.Username, + Nickname: admin.Nickname, + Status: admin.Status, + LastLoginAt: admin.LastLoginAt, + } +} diff --git a/backend/internal/modules/adminauth/service.go b/backend/internal/modules/adminauth/service.go new file mode 100644 index 0000000..3018d02 --- /dev/null +++ b/backend/internal/modules/adminauth/service.go @@ -0,0 +1,34 @@ +package adminauth + +import "errors" + +var ( + ErrDependencyUnavailable = errors.New("dependency unavailable") + ErrInvalidCredential = errors.New("invalid credential") + ErrAdminDisabled = errors.New("admin disabled") +) + +type Service struct { + repo *Repository +} + +func NewService(repo *Repository) *Service { + return &Service{repo: repo} +} + +func (s *Service) Login(username string, password string) (LoginResult, error) { + if s.repo == nil { + return LoginResult{}, ErrDependencyUnavailable + } + if username == "" || password == "" { + return LoginResult{}, ErrInvalidCredential + } + return s.repo.Login(username, password) +} + +func (s *Service) Me(adminID uint64) (*AdminDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.FindByID(adminID) +} diff --git a/backend/internal/modules/auth/jwt.go b/backend/internal/modules/auth/jwt.go index ed7d988..0fe20bb 100644 --- a/backend/internal/modules/auth/jwt.go +++ b/backend/internal/modules/auth/jwt.go @@ -22,9 +22,10 @@ type JWTManager struct { } type Claims struct { - UserID uint64 `json:"uid"` - Phone string `json:"phone"` - TokenType string `json:"typ"` + UserID uint64 `json:"uid"` + Phone string `json:"phone"` + TokenType string `json:"typ"` + SubjectType string `json:"sub_type"` jwt.RegisteredClaims } @@ -45,11 +46,15 @@ func NewJWTManager(secret string) *JWTManager { } func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error) { - accessToken, err := m.generate(userID, phone, tokenTypeAccess, m.accessTTL) + return m.GenerateSubjectPair(userID, phone, "user") +} + +func (m *JWTManager) GenerateSubjectPair(userID uint64, subject string, subjectType string) (TokenPair, error) { + accessToken, err := m.generate(userID, subject, subjectType, tokenTypeAccess, m.accessTTL) if err != nil { return TokenPair{}, err } - refreshToken, err := m.generate(userID, phone, tokenTypeRefresh, m.refreshTTL) + refreshToken, err := m.generate(userID, subject, subjectType, tokenTypeRefresh, m.refreshTTL) if err != nil { return TokenPair{}, err } @@ -62,6 +67,10 @@ func (m *JWTManager) GeneratePair(userID uint64, phone string) (TokenPair, error } func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) { + return m.ParseSubject(tokenText, expectedType, "") +} + +func (m *JWTManager) ParseSubject(tokenText, expectedType string, expectedSubjectType string) (*Claims, error) { claims := &Claims{} token, err := jwt.ParseWithClaims(tokenText, claims, func(token *jwt.Token) (any, error) { if token.Method != m.signingMethod { @@ -75,17 +84,21 @@ func (m *JWTManager) Parse(tokenText, expectedType string) (*Claims, error) { if claims.TokenType != expectedType { return nil, ErrInvalidToken } + if expectedSubjectType != "" && claims.SubjectType != expectedSubjectType { + return nil, ErrInvalidToken + } return claims, nil } -func (m *JWTManager) generate(userID uint64, phone string, tokenType string, ttl time.Duration) (string, error) { +func (m *JWTManager) generate(userID uint64, subject string, subjectType string, tokenType string, ttl time.Duration) (string, error) { now := time.Now() claims := Claims{ - UserID: userID, - Phone: phone, - TokenType: tokenType, + UserID: userID, + Phone: subject, + TokenType: tokenType, + SubjectType: subjectType, RegisteredClaims: jwt.RegisteredClaims{ - Subject: phone, + Subject: subject, IssuedAt: jwt.NewNumericDate(now), ExpiresAt: jwt.NewNumericDate(now.Add(ttl)), }, diff --git a/backend/internal/modules/auth/service.go b/backend/internal/modules/auth/service.go index 32fad74..ddfb42e 100644 --- a/backend/internal/modules/auth/service.go +++ b/backend/internal/modules/auth/service.go @@ -109,7 +109,7 @@ func (s *Service) LoginWithSMS(ctx context.Context, phone string, code string) ( } func (s *Service) RefreshToken(refreshToken string) (TokenPair, error) { - claims, err := s.jwt.Parse(refreshToken, tokenTypeRefresh) + claims, err := s.jwt.ParseSubject(refreshToken, tokenTypeRefresh, "user") if err != nil { return TokenPair{}, err } diff --git a/backend/internal/modules/dispute/handler.go b/backend/internal/modules/dispute/handler.go index f76df7c..f6b2564 100644 --- a/backend/internal/modules/dispute/handler.go +++ b/backend/internal/modules/dispute/handler.go @@ -84,9 +84,9 @@ func (h *Handler) AdminList(c *gin.Context) { } func (h *Handler) AdminArbitrate(c *gin.Context) { - adminID, ok := currentUserID(c) + adminID, ok := currentAdminID(c) if !ok { - response.Unauthorized(c, "缺少用户上下文") + response.Unauthorized(c, "缺少管理员上下文") return } id, ok := parseID(c) @@ -106,6 +106,15 @@ func (h *Handler) AdminArbitrate(c *gin.Context) { response.OK(c, item) } +func currentAdminID(c *gin.Context) (uint64, bool) { + value, ok := c.Get(middleware.ContextAdminID) + if !ok { + return 0, false + } + adminID, ok := value.(uint64) + return adminID, ok +} + func currentUserID(c *gin.Context) (uint64, bool) { value, ok := c.Get(middleware.ContextUserID) if !ok { diff --git a/backend/internal/modules/systemconfig/handler.go b/backend/internal/modules/systemconfig/handler.go index 3e96634..2227763 100644 --- a/backend/internal/modules/systemconfig/handler.go +++ b/backend/internal/modules/systemconfig/handler.go @@ -28,9 +28,9 @@ func (h *Handler) List(c *gin.Context) { } func (h *Handler) Update(c *gin.Context) { - userID, ok := currentUserID(c) + adminID, ok := currentAdminID(c) if !ok { - response.Unauthorized(c, "缺少用户上下文") + response.Unauthorized(c, "缺少管理员上下文") return } key := c.Param("key") @@ -39,7 +39,7 @@ func (h *Handler) Update(c *gin.Context) { response.BadRequest(c, "配置值不能为空") return } - item, err := h.service.Update(userID, key, req, AuditMeta{ + item, err := h.service.Update(adminID, key, req, AuditMeta{ IP: c.ClientIP(), UserAgent: c.GetHeader("User-Agent"), }) @@ -50,13 +50,13 @@ func (h *Handler) Update(c *gin.Context) { response.OK(c, item) } -func currentUserID(c *gin.Context) (uint64, bool) { - value, ok := c.Get(middleware.ContextUserID) +func currentAdminID(c *gin.Context) (uint64, bool) { + value, ok := c.Get(middleware.ContextAdminID) if !ok { return 0, false } - userID, ok := value.(uint64) - return userID, ok + adminID, ok := value.(uint64) + return adminID, ok } func writeConfigError(c *gin.Context, err error) { diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 2e781b7..c44932b 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -4,6 +4,7 @@ import ( "hfb_sys/backend/internal/config" "hfb_sys/backend/internal/handler" "hfb_sys/backend/internal/middleware" + "hfb_sys/backend/internal/modules/adminauth" "hfb_sys/backend/internal/modules/auth" "hfb_sys/backend/internal/modules/dispute" "hfb_sys/backend/internal/modules/listing" @@ -37,6 +38,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { } authService := auth.NewService(userRepo, deps.Redis, jwtManager, logger) authHandler := auth.NewHandler(authService) + var adminAuthRepo *adminauth.Repository + if deps.DB != nil { + adminAuthRepo = adminauth.NewRepository(deps.DB, jwtManager) + } + adminAuthService := adminauth.NewService(adminAuthRepo) + adminAuthHandler := adminauth.NewHandler(adminAuthService) userHandler := user.NewHandler(userRepo) var realnameRepo *realname.Repository if deps.DB != nil { @@ -81,6 +88,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { systemConfigService := systemconfig.NewService(systemConfigRepo) systemConfigHandler := systemconfig.NewHandler(systemConfigService) requireAuth := middleware.Auth(jwtManager) + requireAdmin := middleware.AdminAuth(jwtManager) requireRealname := middleware.RequireRealname(userRepo) api := engine.Group("/api") @@ -151,8 +159,15 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { realnameRoutes.GET("/status", realnameHandler.Status) } - adminRoutes := api.Group("/admin", requireAuth) + adminAuthRoutes := api.Group("/admin/auth") { + adminAuthRoutes.POST("/login", adminAuthHandler.Login) + } + + adminRoutes := api.Group("/admin", requireAdmin) + { + adminRoutes.GET("/me", adminAuthHandler.Me) + adminRoutes.POST("/auth/logout", adminAuthHandler.Logout) adminRoutes.GET("/disputes", disputeHandler.AdminList) adminRoutes.POST("/disputes/:id/arbitrate", disputeHandler.AdminArbitrate) adminRoutes.GET("/system-configs", systemConfigHandler.List) diff --git a/docs/api.md b/docs/api.md index e17e371..a7b9ec8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -46,6 +46,9 @@ API 规划以 [项目计划](project-plan.md) 第 10 章为准。 - `GET /api/wallet/ledger` - `GET /api/notifications` - `POST /api/notifications/{id}/read` +- `POST /api/admin/auth/login` +- `POST /api/admin/auth/logout` +- `GET /api/admin/me` - `GET /api/admin/disputes` - `POST /api/admin/disputes/{id}/arbitrate` - `GET /api/admin/system-configs` diff --git a/docs/business-rules.md b/docs/business-rules.md index 82670ab..8a45aa7 100644 --- a/docs/business-rules.md +++ b/docs/business-rules.md @@ -91,4 +91,11 @@ - 系统配置接口为 `/api/admin/system-configs`。 - 首次查询会自动补齐一组默认配置,包括交接超时、归还确认超时、短信限流、最低押金、实名下单开关、平台抽成和提现门槛。 - 更新配置会写入 `audit_logs`,记录操作人、配置项、修改前值和修改后值。 -- 当前后台接口仍是开发态 mock 权限,只要求登录;后续接入后台管理员、角色和权限后再限制可操作配置项。 +- 当前后台接口已使用独立管理员登录和后台 JWT;后续接入角色和权限后再限制可操作配置项。 + +## 开发态后台登录 + +- 后台登录接口为 `/api/admin/auth/login`,前端页面为 `/admin/login`。 +- 后台 JWT 与普通用户 JWT 区分 `admin` 和 `user`,普通用户 Token 不能访问 `/api/admin/*`。 +- 开发态首次后台登录会自动初始化默认管理员:用户名 `admin`,密码 `admin123456`。 +- 默认管理员只用于本地开发;正式部署前必须改为初始化脚本、强密码和管理员密码修改流程。 diff --git a/docs/project-plan.md b/docs/project-plan.md index 499d2b5..c59c8e7 100644 --- a/docs/project-plan.md +++ b/docs/project-plan.md @@ -614,20 +614,23 @@ 后台: -- `GET /admin/dashboard`:后台仪表盘。 -- `GET /admin/users`:用户列表。 -- `POST /admin/users/{id}/freeze`:冻结用户。 -- `POST /admin/users/{id}/unfreeze`:解冻用户。 -- `GET /admin/listings/pending`:待审核商品。 -- `POST /admin/listings/{id}/approve`:审核通过。 -- `POST /admin/listings/{id}/reject`:审核拒绝。 +- `POST /api/admin/auth/login`:后台登录。 +- `POST /api/admin/auth/logout`:后台退出。 +- `GET /api/admin/me`:当前管理员信息。 +- `GET /api/admin/dashboard`:后台仪表盘。 +- `GET /api/admin/users`:用户列表。 +- `POST /api/admin/users/{id}/freeze`:冻结用户。 +- `POST /api/admin/users/{id}/unfreeze`:解冻用户。 +- `GET /api/admin/listings/pending`:待审核商品。 +- `POST /api/admin/listings/{id}/approve`:审核通过。 +- `POST /api/admin/listings/{id}/reject`:审核拒绝。 - `GET /api/admin/orders`:订单列表。 - `GET /api/admin/disputes`:纠纷列表。 - `POST /api/admin/disputes/{id}/arbitrate`:纠纷仲裁。 - `GET /api/admin/wallet/ledger`:资金流水。 - `GET /api/admin/system-configs`:系统配置列表。 - `PUT /api/admin/system-configs/{key}`:更新系统配置。 -- `GET /admin/audit-logs`:审计日志。 +- `GET /api/admin/audit-logs`:审计日志。 ## 11. 安全与风控 diff --git a/frontend/src/api/adminAuth.ts b/frontend/src/api/adminAuth.ts new file mode 100644 index 0000000..f5291c2 --- /dev/null +++ b/frontend/src/api/adminAuth.ts @@ -0,0 +1,42 @@ +import { apiClient } from './client' + +export interface AdminUser { + id: number + username: string + nickname: string + status: string + last_login_at?: string +} + +export interface AdminTokenPair { + access_token: string + refresh_token: string + token_type: string + expires_in: number +} + +export interface AdminLoginData { + admin: AdminUser + tokens: AdminTokenPair +} + +interface ApiResponse { + code: string + message: string + data: T +} + +export async function loginAdmin(username: string, password: string) { + const { data } = await apiClient.post>('/admin/auth/login', { username, password }) + return data.data +} + +export async function fetchAdminMe() { + const { data } = await apiClient.get>('/admin/me') + return data.data +} + +export async function logoutAdmin() { + const { data } = await apiClient.post>('/admin/auth/logout') + return data.data +} diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index c753c5b..dc6fbe5 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -6,7 +6,9 @@ export const apiClient = axios.create({ }) apiClient.interceptors.request.use((config) => { - const token = localStorage.getItem('access_token') + const url = config.url || '' + const tokenKey = url.startsWith('/admin') ? 'admin_access_token' : 'access_token' + const token = localStorage.getItem(tokenKey) if (token) { config.headers.Authorization = `Bearer ${token}` } diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue index aa1bf29..14c2a8b 100644 --- a/frontend/src/layouts/AppLayout.vue +++ b/frontend/src/layouts/AppLayout.vue @@ -1,5 +1,5 @@ + +