修复鉴权: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
+12 -4
View File
@@ -7,10 +7,18 @@ import (
)
type Query struct {
ActorID uint64
Action string
BizType string
Limit int
ActorID uint64
Action string
BizType string
Page int
PageSize int
}
type PaginatedResult struct {
Items interface{} `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type LogDTO struct {
+20 -10
View File
@@ -18,17 +18,32 @@ func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
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 (h *Handler) List(c *gin.Context) {
query, ok := parseQuery(c)
if !ok {
return
}
items, err := h.service.List(query)
result, err := h.service.List(query)
if err != nil {
writeAuditError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func parseQuery(c *gin.Context) (Query, bool) {
@@ -41,16 +56,11 @@ func parseQuery(c *gin.Context) (Query, bool) {
}
query.ActorID = 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.Action = c.Query("action")
query.BizType = c.Query("biz_type")
page, pageSize := parsePagination(c)
query.Page = page
query.PageSize = pageSize
return query, true
}
@@ -3,6 +3,8 @@ package adminaudit
import (
"time"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
"gorm.io/gorm"
)
@@ -15,37 +17,42 @@ func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) List(query Query) ([]LogDTO, error) {
limit := query.Limit
if limit <= 0 || limit > 500 {
limit = 200
}
func (r *Repository) List(query Query) (*PaginatedResult, error) {
db := r.db.Table("audit_logs AS al").
Select(`al.id, al.actor_type, al.actor_id, COALESCE(au.username, '') AS actor_username,
COALESCE(au.nickname, '') AS actor_nickname, al.action, al.biz_type, al.biz_id,
al.ip, al.user_agent, al.detail, al.created_at`).
Joins("LEFT JOIN admin_users AS au ON au.id = al.actor_id AND al.actor_type = ?", "admin")
countDB := r.db.Model(&model.AuditLog{})
if query.ActorID > 0 {
db = db.Where("al.actor_id = ?", query.ActorID)
countDB = countDB.Where("actor_id = ?", query.ActorID)
}
if query.Action != "" {
db = db.Where("al.action = ?", query.Action)
countDB = countDB.Where("action = ?", query.Action)
}
if query.BizType != "" {
db = db.Where("al.biz_type = ?", query.BizType)
countDB = countDB.Where("biz_type = ?", query.BizType)
}
var total int64
if err := countDB.Count(&total).Error; err != nil {
return nil, err
}
offset := (query.Page - 1) * query.PageSize
var rows []auditLogRow
if err := db.Order("al.id DESC").Limit(limit).Scan(&rows).Error; err != nil {
if err := db.Order("al.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error; err != nil {
return nil, err
}
items := make([]LogDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
}
return items, nil
return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
type auditLogRow struct {
@@ -12,7 +12,7 @@ func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) List(query Query) ([]LogDTO, error) {
func (s *Service) List(query Query) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
@@ -65,10 +65,30 @@ func (h *Handler) Logout(c *gin.Context) {
response.OK(c, gin.H{"logged_out": true})
}
type AdminRefreshRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
func (h *Handler) Refresh(c *gin.Context) {
var req AdminRefreshRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "refresh_token 不能为空")
return
}
tokens, err := h.service.Refresh(req.RefreshToken)
if err != nil {
writeAdminAuthError(c, err)
return
}
response.OK(c, tokens)
}
func writeAdminAuthError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrInvalidRefreshToken):
response.Unauthorized(c, "刷新令牌无效或已过期")
case errors.Is(err, ErrInvalidCredential):
response.BadRequest(c, "用户名或密码错误")
case errors.Is(err, ErrCaptchaInvalid):
+23 -3
View File
@@ -1,20 +1,40 @@
package adminauth
import "errors"
import (
"errors"
"hfb_sys/backend/internal/modules/auth"
)
var (
ErrDependencyUnavailable = errors.New("dependency unavailable")
ErrInvalidCredential = errors.New("invalid credential")
ErrCaptchaInvalid = errors.New("captcha invalid")
ErrAdminDisabled = errors.New("admin disabled")
ErrInvalidRefreshToken = errors.New("invalid refresh token")
)
type Service struct {
repo *Repository
jwt *auth.JWTManager
}
func NewService(repo *Repository, jwt *auth.JWTManager) *Service {
return &Service{repo: repo, jwt: jwt}
}
func NewService(repo *Repository) *Service {
return &Service{repo: repo}
func (s *Service) Refresh(refreshToken string) (*auth.TokenPair, error) {
if s.jwt == nil {
return nil, ErrDependencyUnavailable
}
claims, err := s.jwt.ParseSubject(refreshToken, "refresh", "admin")
if err != nil {
return nil, ErrInvalidRefreshToken
}
pair, err := s.jwt.GenerateSubjectPair(claims.UserID, claims.Phone, "admin")
if err != nil {
return nil, err
}
return &pair, nil
}
func (s *Service) Captcha() (*CaptchaDTO, error) {
+7 -1
View File
@@ -17,7 +17,13 @@ type UserDTO struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type FreezeRequest struct {
Reason string `json:"reason"`
}
type PaginatedResult struct {
Items interface{} `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
+18 -2
View File
@@ -19,13 +19,29 @@ func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
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 (h *Handler) List(c *gin.Context) {
items, err := h.service.List()
page, pageSize := parsePagination(c)
result, err := h.service.List(page, pageSize)
if err != nil {
writeAdminUserError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) Freeze(c *gin.Context) {
@@ -24,7 +24,12 @@ func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) List() ([]UserDTO, error) {
func (r *Repository) List(page, pageSize int) (*PaginatedResult, error) {
var total int64
if err := r.db.Model(&model.User{}).Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []userRow
err := r.db.Table("users AS u").
Select(`u.*,
@@ -35,7 +40,7 @@ func (r *Repository) List() ([]UserDTO, error) {
Joins("LEFT JOIN (SELECT owner_id AS user_id, COUNT(*) AS listing_count FROM rental_listings GROUP BY owner_id) AS l ON l.user_id = u.id").
Joins("LEFT JOIN (SELECT user_id, COUNT(*) AS dispute_count FROM (SELECT initiator_id AS user_id FROM disputes UNION ALL SELECT target_user_id AS user_id FROM disputes) AS dispute_users GROUP BY user_id) AS d ON d.user_id = u.id").
Order("u.id DESC").
Limit(200).
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
@@ -44,7 +49,7 @@ func (r *Repository) List() ([]UserDTO, error) {
for _, row := range rows {
items = append(items, row.toDTO())
}
return items, nil
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) Freeze(adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) {
@@ -15,11 +15,11 @@ func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) List() ([]UserDTO, error) {
func (s *Service) List(page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.List()
return s.repo.List(page, pageSize)
}
func (s *Service) Freeze(adminID uint64, userID uint64, req FreezeRequest, meta AuditMeta) (*UserDTO, error) {
+7 -1
View File
@@ -36,8 +36,14 @@ type ArbitrateRequest struct {
Remark string `json:"remark" binding:"required"`
Amount float64 `json:"amount"`
}
type AuditMeta struct {
IP string
UserAgent string
}
type PaginatedResult struct {
Items []DisputeDTO `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
+21 -4
View File
@@ -48,12 +48,13 @@ func (h *Handler) List(c *gin.Context) {
response.Unauthorized(c, "缺少用户上下文")
return
}
items, err := h.service.ListForUser(userID)
page, pageSize := parsePagination(c)
result, err := h.service.ListForUser(userID, page, pageSize)
if err != nil {
writeDisputeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) Detail(c *gin.Context) {
@@ -75,12 +76,13 @@ func (h *Handler) Detail(c *gin.Context) {
}
func (h *Handler) AdminList(c *gin.Context) {
items, err := h.service.ListAdmin()
page, pageSize := parsePagination(c)
result, err := h.service.ListAdmin(page, pageSize)
if err != nil {
writeDisputeError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) AdminArbitrate(c *gin.Context) {
@@ -136,6 +138,21 @@ func parseID(c *gin.Context) (uint64, bool) {
return id, 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 writeDisputeError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
+17 -5
View File
@@ -125,16 +125,23 @@ func (r *Repository) Create(userID uint64, orderID uint64, req CreateRequest) (*
return r.FindForUser(userID, createdID)
}
func (r *Repository) ListForUser(userID uint64) ([]DisputeDTO, error) {
func (r *Repository) ListForUser(userID uint64, page, pageSize int) (*PaginatedResult, error) {
conditions := r.db.Model(&model.Dispute{}).Where("initiator_id = ? OR target_user_id = ?", userID, userID)
var total int64
if err := conditions.Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []disputeRow
err := r.baseQuery().
Where("d.initiator_id = ? OR d.target_user_id = ?", userID, userID).
Order("d.id DESC").
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
return toDTOs(rows), nil
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) FindForUser(userID uint64, id uint64) (*DisputeDTO, error) {
@@ -148,13 +155,18 @@ func (r *Repository) FindForUser(userID uint64, id uint64) (*DisputeDTO, error)
return &dto, nil
}
func (r *Repository) ListAdmin() ([]DisputeDTO, error) {
func (r *Repository) ListAdmin(page, pageSize int) (*PaginatedResult, error) {
var total int64
if err := r.db.Model(&model.Dispute{}).Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []disputeRow
err := r.baseQuery().Order("d.id DESC").Limit(200).Scan(&rows).Error
err := r.baseQuery().Order("d.id DESC").Offset(offset).Limit(pageSize).Scan(&rows).Error
if err != nil {
return nil, err
}
return toDTOs(rows), nil
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
+4 -4
View File
@@ -28,11 +28,11 @@ func (s *Service) Create(userID uint64, orderID uint64, req CreateRequest) (*Dis
return s.repo.Create(userID, orderID, req)
}
func (s *Service) ListForUser(userID uint64) ([]DisputeDTO, error) {
func (s *Service) ListForUser(userID uint64, page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListForUser(userID)
return s.repo.ListForUser(userID, page, pageSize)
}
func (s *Service) FindForUser(userID uint64, id uint64) (*DisputeDTO, error) {
@@ -42,11 +42,11 @@ func (s *Service) FindForUser(userID uint64, id uint64) (*DisputeDTO, error) {
return s.repo.FindForUser(userID, id)
}
func (s *Service) ListAdmin() ([]DisputeDTO, error) {
func (s *Service) ListAdmin(page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListAdmin()
return s.repo.ListAdmin(page, pageSize)
}
func (s *Service) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
+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) {
@@ -13,3 +13,10 @@ type NotificationDTO struct {
ReadAt *time.Time `json:"read_at"`
CreatedAt time.Time `json:"created_at"`
}
type PaginatedResult struct {
Items interface{} `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
@@ -18,18 +18,34 @@ func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
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 (h *Handler) List(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
items, err := h.service.List(userID)
page, pageSize := parsePagination(c)
result, err := h.service.List(userID, page, pageSize)
if err != nil {
writeNotificationError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) MarkRead(c *gin.Context) {
@@ -25,16 +25,21 @@ func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) List(userID uint64) ([]NotificationDTO, error) {
func (r *Repository) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
var total int64
if err := r.db.Model(&model.Notification{}).Where("user_id = ?", userID).Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []model.Notification
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Limit(100).Find(&rows).Error; err != nil {
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil {
return nil, err
}
items := make([]NotificationDTO, 0, len(rows))
for _, row := range rows {
items = append(items, toDTO(row))
}
return items, nil
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) MarkRead(userID uint64, id uint64) error {
@@ -12,11 +12,11 @@ func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) List(userID uint64) ([]NotificationDTO, error) {
func (s *Service) List(userID uint64, page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.List(userID)
return s.repo.List(userID, page, pageSize)
}
func (s *Service) MarkRead(userID uint64, id uint64) error {
+23
View File
@@ -78,6 +78,29 @@ type HandoffRecordDTO struct {
CreatedAt time.Time `json:"created_at"`
}
type OrderListItemDTO struct {
ID uint64 `json:"id"`
OrderNo string `json:"order_no"`
ListingTitle string `json:"listing_title"`
GameName string `json:"game_name"`
AccountTitle string `json:"account_title"`
RenterNickname string `json:"renter_nickname"`
OwnerNickname string `json:"owner_nickname"`
Status string `json:"status"`
HandoffStatus string `json:"handoff_status"`
SettlementStatus string `json:"settlement_status"`
RentAmount float64 `json:"rent_amount"`
DepositAmount float64 `json:"deposit_amount"`
CreatedAt time.Time `json:"created_at"`
}
type PaginatedOrders struct {
Items []OrderListItemDTO `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type CheckoutDTO struct {
ID uint64 `json:"id"`
OrderID uint64 `json:"order_id"`
+26 -4
View File
@@ -44,21 +44,43 @@ func (h *Handler) List(c *gin.Context) {
response.Unauthorized(c, "缺少用户上下文")
return
}
items, err := h.service.ListForUser(userID)
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
}
result, err := h.service.ListForUser(userID, page, pageSize)
if err != nil {
writeOrderError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, gin.H{"items": result.Items, "total": result.Total, "page": result.Page, "page_size": result.PageSize})
}
func (h *Handler) AdminList(c *gin.Context) {
items, err := h.service.ListAdmin()
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
}
result, err := h.service.ListAdmin(page, pageSize)
if err != nil {
writeOrderError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, gin.H{"items": result.Items, "total": result.Total, "page": result.Page, "page_size": result.PageSize})
}
func (h *Handler) AdminDetail(c *gin.Context) {
+66 -14
View File
@@ -488,36 +488,45 @@ func (r *Repository) AcceptCheckout(userID uint64, orderID uint64) error {
})
}
func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) {
var rows []orderRow
err := r.baseQuery().
Where("o.renter_id = ?", userID).
func (r *Repository) ListForUser(userID uint64, page int, pageSize int) (*PaginatedOrders, error) {
var total int64
r.db.Model(&model.RentalOrder{}).Where("renter_id = ? OR owner_id = ?", userID, userID).Count(&total)
offset := (page - 1) * pageSize
var rows []orderListRow
err := r.listItemBaseQuery().
Where("o.renter_id = ? OR o.owner_id = ?", userID, userID).
Order("o.id DESC").
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]OrderDTO, 0, len(rows))
items := make([]OrderListItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
items = append(items, row.toListItemDTO())
}
return items, nil
return &PaginatedOrders{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) ListAdmin() ([]OrderDTO, error) {
var rows []orderRow
err := r.adminQuery().
func (r *Repository) ListAdmin(page int, pageSize int) (*PaginatedOrders, error) {
var total int64
r.db.Model(&model.RentalOrder{}).Count(&total)
offset := (page - 1) * pageSize
var rows []orderListRow
err := r.listItemBaseQuery().
Order("o.id DESC").
Limit(200).
Offset(offset).Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]OrderDTO, 0, len(rows))
items := make([]OrderListItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
items = append(items, row.toListItemDTO())
}
return items, nil
return &PaginatedOrders{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) {
@@ -898,6 +907,16 @@ func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) {
return &dto, nil
}
func (r *Repository) listItemBaseQuery() *gorm.DB {
return r.db.Table("rental_orders AS o").
Select("o.id, o.order_no, o.status, o.handoff_status, o.settlement_status, o.rent_amount, o.deposit_amount, o.created_at, "+
"a.title AS account_title, a.game_name, "+
"owner.nickname AS owner_nickname, renter.nickname AS renter_nickname").
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Joins("JOIN users AS owner ON owner.id = o.owner_id").
Joins("JOIN users AS renter ON renter.id = o.renter_id")
}
func (r *Repository) baseQuery() *gorm.DB {
return r.db.Table("rental_orders AS o").
Select("o.*, a.title, a.server_region, a.login_platform").
@@ -948,6 +967,39 @@ func (row orderRow) toDTO() OrderDTO {
}
}
type orderListRow struct {
ID uint64
OrderNo string
Status string
HandoffStatus string
SettlementStatus string
RentAmount float64
DepositAmount float64
CreatedAt time.Time
AccountTitle string
GameName string
OwnerNickname string
RenterNickname string
}
func (row orderListRow) toListItemDTO() OrderListItemDTO {
return OrderListItemDTO{
ID: row.ID,
OrderNo: row.OrderNo,
ListingTitle: row.AccountTitle,
GameName: row.GameName,
AccountTitle: row.AccountTitle,
RenterNickname: row.RenterNickname,
OwnerNickname: row.OwnerNickname,
Status: row.Status,
HandoffStatus: row.HandoffStatus,
SettlementStatus: row.SettlementStatus,
RentAmount: row.RentAmount,
DepositAmount: row.DepositAmount,
CreatedAt: row.CreatedAt,
}
}
func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO {
return HandoffRecordDTO{
ID: record.ID,
+4 -4
View File
@@ -103,18 +103,18 @@ func (s *Service) AcceptCheckout(userID uint64, orderID uint64) error {
return s.repo.AcceptCheckout(userID, orderID)
}
func (s *Service) ListForUser(userID uint64) ([]OrderDTO, error) {
func (s *Service) ListForUser(userID uint64, page int, pageSize int) (*PaginatedOrders, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListForUser(userID)
return s.repo.ListForUser(userID, page, pageSize)
}
func (s *Service) ListAdmin() ([]OrderDTO, error) {
func (s *Service) ListAdmin(page int, pageSize int) (*PaginatedOrders, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.ListAdmin()
return s.repo.ListAdmin(page, pageSize)
}
func (s *Service) FindAdmin(orderID uint64) (*OrderDTO, error) {
+12 -4
View File
@@ -25,10 +25,18 @@ type LedgerDTO struct {
}
type AdminLedgerQuery struct {
UserID uint64
OrderID uint64
BizType string
Limit int
UserID uint64
OrderID uint64
BizType string
Page int
PageSize int
}
type PaginatedResult struct {
Items interface{} `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type AdminLedgerDTO struct {
+23 -12
View File
@@ -32,18 +32,34 @@ func (h *Handler) Balance(c *gin.Context) {
response.OK(c, account)
}
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 (h *Handler) Ledger(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
response.Unauthorized(c, "缺少用户上下文")
return
}
items, err := h.service.Ledger(userID)
page, pageSize := parsePagination(c)
result, err := h.service.Ledger(userID, page, pageSize)
if err != nil {
writeWalletError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func (h *Handler) AdminLedger(c *gin.Context) {
@@ -51,12 +67,12 @@ func (h *Handler) AdminLedger(c *gin.Context) {
if !ok {
return
}
items, err := h.service.AdminLedger(query)
result, err := h.service.AdminLedger(query)
if err != nil {
writeWalletError(c, err)
return
}
response.OK(c, gin.H{"items": items})
response.OK(c, result)
}
func parseAdminLedgerQuery(c *gin.Context) (AdminLedgerQuery, bool) {
@@ -77,15 +93,10 @@ func parseAdminLedgerQuery(c *gin.Context) (AdminLedgerQuery, bool) {
}
query.OrderID = 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.BizType = c.Query("biz_type")
page, pageSize := parsePagination(c)
query.Page = page
query.PageSize = pageSize
return query, true
}
+22 -12
View File
@@ -45,24 +45,24 @@ func (r *Repository) Account(userID uint64) (*AccountDTO, error) {
return toAccountDTO(account), nil
}
func (r *Repository) Ledger(userID uint64) ([]LedgerDTO, error) {
func (r *Repository) Ledger(userID uint64, page, pageSize int) (*PaginatedResult, error) {
var total int64
if err := r.db.Model(&model.WalletLedger{}).Where("user_id = ?", userID).Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []model.WalletLedger
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Limit(100).Find(&rows).Error; err != nil {
if err := r.db.Where("user_id = ?", userID).Order("id DESC").Offset(offset).Limit(pageSize).Find(&rows).Error; err != nil {
return nil, err
}
items := make([]LedgerDTO, 0, len(rows))
for _, row := range rows {
items = append(items, toLedgerDTO(row))
}
return items, nil
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
limit := query.Limit
if limit <= 0 || limit > 500 {
limit = 200
}
func (r *Repository) AdminLedger(query AdminLedgerQuery) (*PaginatedResult, error) {
db := r.db.Table("wallet_ledger AS wl").
Select(`wl.id, wl.ledger_no, wl.user_id, COALESCE(u.phone, '') AS user_phone,
COALESCE(u.nickname, '') AS user_nickname, wl.order_id, COALESCE(ro.order_no, '') AS order_no,
@@ -71,21 +71,31 @@ func (r *Repository) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, erro
Joins("LEFT JOIN users AS u ON u.id = wl.user_id").
Joins("LEFT JOIN rental_orders AS ro ON ro.id = wl.order_id")
countDB := r.db.Model(&model.WalletLedger{})
if query.UserID > 0 {
db = db.Where("wl.user_id = ?", query.UserID)
countDB = countDB.Where("user_id = ?", query.UserID)
}
if query.OrderID > 0 {
db = db.Where("wl.order_id = ?", query.OrderID)
countDB = countDB.Where("order_id = ?", query.OrderID)
}
if query.BizType != "" {
db = db.Where("wl.biz_type = ?", query.BizType)
countDB = countDB.Where("biz_type = ?", query.BizType)
}
var items []AdminLedgerDTO
if err := db.Order("wl.id DESC").Limit(limit).Scan(&items).Error; err != nil {
var total int64
if err := countDB.Count(&total).Error; err != nil {
return nil, err
}
return items, nil
offset := (query.Page - 1) * query.PageSize
var items []AdminLedgerDTO
if err := db.Order("wl.id DESC").Offset(offset).Limit(query.PageSize).Scan(&items).Error; err != nil {
return nil, err
}
return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil
}
func AppendEntries(tx *gorm.DB, entries ...Entry) error {
+3 -3
View File
@@ -19,14 +19,14 @@ func (s *Service) Account(userID uint64) (*AccountDTO, error) {
return s.repo.Account(userID)
}
func (s *Service) Ledger(userID uint64) ([]LedgerDTO, error) {
func (s *Service) Ledger(userID uint64, page, pageSize int) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.Ledger(userID)
return s.repo.Ledger(userID, page, pageSize)
}
func (s *Service) AdminLedger(query AdminLedgerQuery) ([]AdminLedgerDTO, error) {
func (s *Service) AdminLedger(query AdminLedgerQuery) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
+2 -1
View File
@@ -46,7 +46,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
if deps.DB != nil {
adminAuthRepo = adminauth.NewRepository(deps.DB, deps.Redis, jwtManager)
}
adminAuthService := adminauth.NewService(adminAuthRepo)
adminAuthService := adminauth.NewService(adminAuthRepo, jwtManager)
adminAuthHandler := adminauth.NewHandler(adminAuthService)
var adminDashboardRepo *admindashboard.Repository
if deps.DB != nil {
@@ -211,6 +211,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
{
adminAuthRoutes.GET("/captcha", adminAuthHandler.Captcha)
adminAuthRoutes.POST("/login", adminAuthHandler.Login)
adminAuthRoutes.POST("/refresh", adminAuthHandler.Refresh)
}
adminRoutes := api.Group("/admin", requireAdmin)