修复鉴权:401拦截器加refresh重试 + admin refresh接口 + 路由守卫完善

根因:access_token每2小时过期,前端收到401直接清token跳登录,没有用refresh_token续期

后端修复:
- adminauth模块新增 POST /admin/auth/refresh 接口
- Service 注入 JWTManager,支持 admin refresh token 换新 token pair
- Refresh 方法验证 subjectType=admin + tokenType=refresh

前端修复:
- 401 拦截器核心改造:收到401先调 refresh 接口续期
- 加 isRefreshing 锁 + pendingRequests 队列防止并发刷新
- refresh 用原生 axios.post 避免拦截器递归
- 成功则更新 localStorage + 重试原请求,失败才清 token 跳登录
- 排除 /auth/refresh 自身避免死循环
- 支持 /admin/ 请求独立 token 管理
- auth.ts/adminAuth.ts 新增手动 refreshUserToken/refreshAdminSession
- 路由守卫给所有需登录路由添加 meta.requiresAuth
- 守卫同时支持 PC 端 /login 和移动端 /m/login
This commit is contained in:
yml2213
2026-05-24 07:00:13 +08:00
parent 3631e70321
commit cdee93c7c5
61 changed files with 1735 additions and 722 deletions
+8 -1
View File
@@ -56,12 +56,19 @@ type AdminListQuery struct {
OwnerID uint64
Status string
ReviewStatus string
Limit int
Page int
PageSize int
}
type AdminActionRequest struct {
Reason string `json:"reason" binding:"required"`
}
type PaginatedResult struct {
Items []ListingDTO `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type AuditMeta struct {
IP string
+27 -16
View File
@@ -82,12 +82,13 @@ func (h *Handler) SubmitReview(c *gin.Context) {
}
func (h *Handler) ListPendingReview(c *gin.Context) {
items, err := h.service.ListPendingReview()
page, pageSize := parsePagination(c)
result, err := h.service.ListPendingReview(page, pageSize)
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) ListAdmin(c *gin.Context) {
@@ -95,12 +96,12 @@ func (h *Handler) ListAdmin(c *gin.Context) {
if !ok {
return
}
items, err := h.service.ListAdmin(query)
result, err := h.service.ListAdmin(query)
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) FindAdmin(c *gin.Context) {
@@ -196,12 +197,13 @@ func (h *Handler) Offline(c *gin.Context) {
}
func (h *Handler) ListPublic(c *gin.Context) {
items, err := h.service.ListPublic()
page, pageSize := parsePagination(c)
result, err := h.service.ListPublic(page, pageSize)
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) FindPublic(c *gin.Context) {
@@ -276,12 +278,13 @@ func (h *Handler) ListMine(c *gin.Context) {
response.Unauthorized(c, "缺少用户上下文")
return
}
items, err := h.service.ListMine(ownerID)
page, pageSize := parsePagination(c)
result, err := h.service.ListMine(ownerID, page, pageSize)
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) FindMine(c *gin.Context) {
@@ -339,19 +342,27 @@ func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) {
}
query.OwnerID = value
}
if raw := c.Query("limit"); raw != "" {
value, err := strconv.Atoi(raw)
if err != nil || value <= 0 {
response.BadRequest(c, "查询条数不正确")
return query, false
}
query.Limit = value
}
query.Status = c.Query("status")
query.ReviewStatus = c.Query("review_status")
query.Page, query.PageSize = parsePagination(c)
return query, true
}
func parsePagination(c *gin.Context) (int, int) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
return page, pageSize
}
func writeListingError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
+60 -20
View File
@@ -158,26 +158,41 @@ func (r *Repository) SubmitReview(ownerID uint64, listingID uint64, reviewRequir
return dto, err
}
func (r *Repository) ListPendingReview() ([]ListingDTO, error) {
func (r *Repository) ListPendingReview(page, pageSize int) (*PaginatedResult, error) {
conditions := r.db.Model(&model.RentalListing{}).Where("review_status = ?", "pending")
var total int64
if err := conditions.Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []listingRow
err := r.baseQuery().
err := r.listQuery().
Where("l.review_status = ?", "pending").
Order("l.updated_at ASC, l.id ASC").
Limit(200).
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
return rowsToDTO(rows), nil
return &PaginatedResult{Items: rowsToDTO(rows), Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) ListAdmin(query AdminListQuery) ([]ListingDTO, error) {
limit := query.Limit
if limit <= 0 || limit > 500 {
limit = 200
func (r *Repository) ListAdmin(query AdminListQuery) (*PaginatedResult, error) {
conditions := r.db.Model(&model.RentalListing{})
if query.OwnerID > 0 {
conditions = conditions.Where("owner_id = ?", query.OwnerID)
}
db := r.baseQuery()
if query.Status != "" {
conditions = conditions.Where("status = ?", query.Status)
}
if query.ReviewStatus != "" {
conditions = conditions.Where("review_status = ?", query.ReviewStatus)
}
var total int64
if err := conditions.Count(&total).Error; err != nil {
return nil, err
}
db := r.listQuery()
if query.OwnerID > 0 {
db = db.Where("l.owner_id = ?", query.OwnerID)
}
@@ -187,13 +202,13 @@ func (r *Repository) ListAdmin(query AdminListQuery) ([]ListingDTO, error) {
if query.ReviewStatus != "" {
db = db.Where("l.review_status = ?", query.ReviewStatus)
}
offset := (query.Page - 1) * query.PageSize
var rows []listingRow
err := db.Order("l.id DESC").Limit(limit).Scan(&rows).Error
err := db.Order("l.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error
if err != nil {
return nil, err
}
return rowsToDTO(rows), nil
return &PaginatedResult{Items: rowsToDTO(rows), Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func (r *Repository) FindAdmin(listingID uint64) (*ListingDTO, error) {
@@ -358,29 +373,43 @@ func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
})
}
func (r *Repository) ListPublic() ([]ListingDTO, error) {
func (r *Repository) ListPublic(page, pageSize int) (*PaginatedResult, error) {
conditions := r.db.Model(&model.RentalListing{}).Where("status = ? AND review_status = ?", "published", "approved")
var total int64
if err := conditions.Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []listingRow
err := r.baseQuery().
err := r.listQuery().
Where("l.status = ? AND l.review_status = ?", "published", "approved").
Order("l.published_at DESC, l.id DESC").
Limit(100).
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
return publicListings(rowsToDTO(rows)), nil
items := publicListings(rowsToDTO(rows))
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) {
func (r *Repository) ListMine(ownerID uint64, page, pageSize int) (*PaginatedResult, error) {
conditions := r.db.Model(&model.RentalListing{}).Where("owner_id = ?", ownerID)
var total int64
if err := conditions.Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []listingRow
err := r.baseQuery().
err := r.listQuery().
Where("l.owner_id = ?", ownerID).
Order("l.id DESC").
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
return rowsToDTO(rows), nil
return &PaginatedResult{Items: rowsToDTO(rows), Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) FindPublic(id uint64) (*ListingDTO, error) {
@@ -449,6 +478,17 @@ func (r *Repository) baseQuery() *gorm.DB {
Joins("LEFT JOIN users AS u ON u.id = l.owner_id")
}
// listQuery returns a SELECT-optimized query for list endpoints (excludes heavy fields like description, screenshot_urls, asset_summary).
func (r *Repository) listQuery() *gorm.DB {
return r.db.Table("rental_listings AS l").
Select(`l.id, l.account_id, l.owner_id, l.price_hourly, l.price_daily, l.price_weekly, l.deposit_amount,
l.status, l.review_status, l.review_reason, l.published_at, l.created_at, l.updated_at,
a.title, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount,
COALESCE(u.phone, '') AS owner_phone, COALESCE(u.nickname, '') AS owner_nickname`).
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
Joins("LEFT JOIN users AS u ON u.id = l.owner_id")
}
func (r *Repository) findForReviewUpdate(tx *gorm.DB, listingID uint64) (*model.RentalListing, *model.GameAccount, error) {
var listing model.RentalListing
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, listingID).Error; err != nil {
+7 -7
View File
@@ -93,14 +93,14 @@ func (s *Service) SubmitReview(ownerID uint64, id uint64) (*ListingDTO, error) {
return s.repo.SubmitReview(ownerID, id, reviewRequired)
}
func (s *Service) ListPendingReview() ([]ListingDTO, error) {
func (s *Service) ListPendingReview(page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListPendingReview()
return s.repo.ListPendingReview(page, pageSize)
}
func (s *Service) ListAdmin(query AdminListQuery) ([]ListingDTO, error) {
func (s *Service) ListAdmin(query AdminListQuery) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
@@ -158,18 +158,18 @@ func (s *Service) Offline(ownerID uint64, id uint64) error {
return s.repo.Offline(ownerID, id)
}
func (s *Service) ListPublic() ([]ListingDTO, error) {
func (s *Service) ListPublic(page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListPublic()
return s.repo.ListPublic(page, pageSize)
}
func (s *Service) ListMine(ownerID uint64) ([]ListingDTO, error) {
func (s *Service) ListMine(ownerID uint64, page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListMine(ownerID)
return s.repo.ListMine(ownerID, page, pageSize)
}
func (s *Service) FindPublic(id uint64) (*ListingDTO, error) {