根因: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
297 lines
7.0 KiB
Go
297 lines
7.0 KiB
Go
package listing
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
|
ErrInvalidInput = errors.New("invalid listing input")
|
|
ErrListingLocked = errors.New("listing locked")
|
|
ErrMissingTitle = errors.New("missing listing title")
|
|
ErrMissingServerRegion = errors.New("missing server region")
|
|
ErrInvalidPrice = errors.New("invalid listing price")
|
|
ErrInvalidDeposit = errors.New("invalid listing deposit")
|
|
ErrInvalidHafCoin = errors.New("invalid haf coin amount")
|
|
ErrMissingScreenshot = errors.New("missing screenshot")
|
|
)
|
|
|
|
type Service struct {
|
|
repo *Repository
|
|
config ConfigReader
|
|
}
|
|
|
|
type ConfigReader interface {
|
|
FindValue(key string) (string, error)
|
|
}
|
|
|
|
const (
|
|
reviewRequiredConfigKey = "listing.review_required"
|
|
publishOptionsConfigKey = "listing.publish_options"
|
|
defaultFireLevelMin = 38
|
|
)
|
|
|
|
type FireLevelTooLowError struct {
|
|
Min int
|
|
}
|
|
|
|
func (e FireLevelTooLowError) Error() string {
|
|
return "fire level too low"
|
|
}
|
|
|
|
func NewService(repo *Repository, config ConfigReader) *Service {
|
|
return &Service{repo: repo, config: config}
|
|
}
|
|
|
|
func (s *Service) Create(ownerID uint64, req CreateRequest) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
rules, err := s.publishRules()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateRequest(req, rules); err != nil {
|
|
return nil, err
|
|
}
|
|
reviewRequired, err := s.reviewRequired()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.Create(ownerID, req, reviewRequired)
|
|
}
|
|
|
|
func (s *Service) Update(ownerID uint64, id uint64, req UpdateRequest) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
rules, err := s.publishRules()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateRequest(req, rules); err != nil {
|
|
return nil, err
|
|
}
|
|
reviewRequired, err := s.reviewRequired()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.Update(ownerID, id, req, reviewRequired)
|
|
}
|
|
|
|
func (s *Service) SubmitReview(ownerID uint64, id uint64) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
reviewRequired, err := s.reviewRequired()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.repo.SubmitReview(ownerID, id, reviewRequired)
|
|
}
|
|
|
|
func (s *Service) ListPendingReview(page, pageSize int) (*PaginatedResult, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.ListPendingReview(page, pageSize)
|
|
}
|
|
|
|
func (s *Service) ListAdmin(query AdminListQuery) (*PaginatedResult, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.ListAdmin(query)
|
|
}
|
|
|
|
func (s *Service) FindAdmin(id uint64) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.FindAdmin(id)
|
|
}
|
|
|
|
func (s *Service) AdminOffline(adminID uint64, id uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
if req.Reason == "" {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
return s.repo.AdminOffline(adminID, id, req, meta)
|
|
}
|
|
|
|
func (s *Service) AdminMarkAbnormal(adminID uint64, id uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
if req.Reason == "" {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
return s.repo.AdminMarkAbnormal(adminID, id, req, meta)
|
|
}
|
|
|
|
func (s *Service) Approve(id uint64) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.Approve(id)
|
|
}
|
|
|
|
func (s *Service) Reject(id uint64, req ReviewRequest) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
if req.Reason == "" {
|
|
return nil, ErrInvalidInput
|
|
}
|
|
return s.repo.Reject(id, req)
|
|
}
|
|
|
|
func (s *Service) Offline(ownerID uint64, id uint64) error {
|
|
if s.repo == nil {
|
|
return ErrDependencyUnavailable
|
|
}
|
|
return s.repo.Offline(ownerID, id)
|
|
}
|
|
|
|
func (s *Service) ListPublic(page, pageSize int) (*PaginatedResult, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.ListPublic(page, pageSize)
|
|
}
|
|
|
|
func (s *Service) ListMine(ownerID uint64, page, pageSize int) (*PaginatedResult, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.ListMine(ownerID, page, pageSize)
|
|
}
|
|
|
|
func (s *Service) FindPublic(id uint64) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.FindPublic(id)
|
|
}
|
|
|
|
func (s *Service) FindPublicCoverKey(id uint64) (string, error) {
|
|
if s.repo == nil {
|
|
return "", ErrDependencyUnavailable
|
|
}
|
|
return s.repo.FindPublicCoverKey(id)
|
|
}
|
|
|
|
func (s *Service) FindPublicScreenshotKey(id uint64, index int) (string, error) {
|
|
if s.repo == nil {
|
|
return "", ErrDependencyUnavailable
|
|
}
|
|
return s.repo.FindPublicScreenshotKey(id, index)
|
|
}
|
|
|
|
func (s *Service) FindMine(ownerID uint64, id uint64) (*ListingDTO, error) {
|
|
if s.repo == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
return s.repo.FindMine(ownerID, id)
|
|
}
|
|
|
|
type publishRules struct {
|
|
FireLevelMin int
|
|
}
|
|
|
|
func validateRequest(req CreateRequest, rules publishRules) error {
|
|
if strings.TrimSpace(req.Title) == "" {
|
|
return ErrMissingTitle
|
|
}
|
|
if strings.TrimSpace(req.ServerRegion) == "" {
|
|
return ErrMissingServerRegion
|
|
}
|
|
if req.PriceHourly <= 0 {
|
|
return ErrInvalidPrice
|
|
}
|
|
if req.DepositAmount < 0 {
|
|
return ErrInvalidDeposit
|
|
}
|
|
if req.HafCoinAmount < 0 {
|
|
return ErrInvalidHafCoin
|
|
}
|
|
if !hasScreenshotURL(req.ScreenshotURLS) {
|
|
return ErrMissingScreenshot
|
|
}
|
|
if fireLevel, ok := readFireLevel(req.AssetSummary); ok && fireLevel < rules.FireLevelMin {
|
|
return FireLevelTooLowError{Min: rules.FireLevelMin}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readFireLevel(summary map[string]any) (int, bool) {
|
|
if summary == nil {
|
|
return 0, false
|
|
}
|
|
value, ok := summary["fire_level"]
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
switch current := value.(type) {
|
|
case float64:
|
|
return int(current), true
|
|
case int:
|
|
return current, true
|
|
case string:
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(current))
|
|
return parsed, err == nil
|
|
default:
|
|
return 0, false
|
|
}
|
|
}
|
|
|
|
func hasScreenshotURL(urls []string) bool {
|
|
for _, url := range urls {
|
|
if strings.TrimSpace(url) != "" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (s *Service) reviewRequired() (bool, error) {
|
|
if s.config == nil {
|
|
return false, nil
|
|
}
|
|
value, err := s.config.FindValue(reviewRequiredConfigKey)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
required, err := strconv.ParseBool(strings.TrimSpace(value))
|
|
if err != nil {
|
|
return false, nil
|
|
}
|
|
return required, nil
|
|
}
|
|
|
|
func (s *Service) publishRules() (publishRules, error) {
|
|
rules := publishRules{FireLevelMin: defaultFireLevelMin}
|
|
if s.config == nil {
|
|
return rules, nil
|
|
}
|
|
value, err := s.config.FindValue(publishOptionsConfigKey)
|
|
if err != nil {
|
|
return rules, err
|
|
}
|
|
var raw struct {
|
|
FireLevelMin int `json:"fire_level_min"`
|
|
}
|
|
if err := json.Unmarshal([]byte(value), &raw); err != nil {
|
|
return rules, nil
|
|
}
|
|
if raw.FireLevelMin > 0 {
|
|
rules.FireLevelMin = raw.FireLevelMin
|
|
}
|
|
return rules, nil
|
|
}
|