diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index cbb26a8..1b67d9c 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -39,10 +39,10 @@ type CreateRequest struct { HafCoinAmount int64 `json:"haf_coin_amount"` AssetSummary map[string]any `json:"asset_summary"` ScreenshotURLS []string `json:"screenshot_urls"` - PriceHourly float64 `json:"price_hourly" binding:"required"` + PriceHourly float64 `json:"price_hourly"` PriceDaily float64 `json:"price_daily"` PriceWeekly float64 `json:"price_weekly"` - DepositAmount float64 `json:"deposit_amount" binding:"required"` + DepositAmount float64 `json:"deposit_amount"` } type UpdateRequest = CreateRequest diff --git a/backend/internal/modules/listing/handler.go b/backend/internal/modules/listing/handler.go index b73ff92..6afb94b 100644 --- a/backend/internal/modules/listing/handler.go +++ b/backend/internal/modules/listing/handler.go @@ -301,6 +301,18 @@ func writeListingError(c *gin.Context, err error) { switch { case errors.Is(err, ErrDependencyUnavailable): response.ServiceUnavailable(c, "数据库未连接") + case errors.Is(err, ErrMissingTitle): + response.BadRequest(c, "发布标题不能为空") + case errors.Is(err, ErrMissingServerRegion): + response.BadRequest(c, "请选择区服") + case errors.Is(err, ErrInvalidPrice): + response.BadRequest(c, "发布价格不正确") + case errors.Is(err, ErrInvalidDeposit): + response.BadRequest(c, "押金不能小于 0") + case errors.Is(err, ErrInvalidHafCoin): + response.BadRequest(c, "哈夫币数量不正确") + case errors.Is(err, ErrMissingScreenshot): + response.BadRequest(c, "请至少上传一张账号截图") case errors.Is(err, ErrInvalidInput): response.BadRequest(c, "发布信息不符合规则") case errors.Is(err, ErrListingLocked): diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go index c6ebd1e..2c933dc 100644 --- a/backend/internal/modules/listing/repository.go +++ b/backend/internal/modules/listing/repository.go @@ -22,7 +22,15 @@ func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } -func (r *Repository) Create(ownerID uint64, req CreateRequest) (*ListingDTO, error) { +func initialPublishState(reviewRequired bool) (string, string, *time.Time) { + if reviewRequired { + return "draft", "pending", nil + } + now := time.Now() + return "published", "approved", &now +} + +func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bool) (*ListingDTO, error) { var dto *ListingDTO err := r.db.Transaction(func(tx *gorm.DB) error { screenshots, err := marshalScreenshots(req.ScreenshotURLS) @@ -33,6 +41,7 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest) (*ListingDTO, err if err != nil { return err } + listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) account := model.GameAccount{ OwnerID: ownerID, GameName: "delta_force", @@ -44,7 +53,7 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest) (*ListingDTO, err HafCoinAmount: req.HafCoinAmount, AssetSummary: assetSummary, ScreenshotURLS: screenshots, - Status: "draft", + Status: listingStatus, } if err := tx.Create(&account).Error; err != nil { return err @@ -56,8 +65,9 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest) (*ListingDTO, err PriceDaily: req.PriceDaily, PriceWeekly: req.PriceWeekly, DepositAmount: req.DepositAmount, - Status: "draft", - ReviewStatus: "none", + Status: listingStatus, + ReviewStatus: reviewStatus, + PublishedAt: publishedAt, } if err := tx.Create(&listing).Error; err != nil { return err @@ -68,7 +78,7 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest) (*ListingDTO, err return dto, err } -func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest) (*ListingDTO, error) { +func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest, reviewRequired bool) (*ListingDTO, error) { var dto *ListingDTO err := r.db.Transaction(func(tx *gorm.DB) error { listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID) @@ -95,18 +105,20 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest) return err } account.ScreenshotURLS = screenshots - if err := tx.Save(account).Error; err != nil { - return err - } listing.PriceHourly = req.PriceHourly listing.PriceDaily = req.PriceDaily listing.PriceWeekly = req.PriceWeekly listing.DepositAmount = req.DepositAmount - listing.Status = "draft" - listing.ReviewStatus = "none" + listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) + listing.Status = listingStatus + listing.ReviewStatus = reviewStatus listing.ReviewReason = "" - listing.PublishedAt = nil + listing.PublishedAt = publishedAt + account.Status = listingStatus + if err := tx.Save(account).Error; err != nil { + return err + } if err := tx.Save(listing).Error; err != nil { return err } @@ -116,7 +128,7 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest) return dto, err } -func (r *Repository) SubmitReview(ownerID uint64, listingID uint64) (*ListingDTO, error) { +func (r *Repository) SubmitReview(ownerID uint64, listingID uint64, reviewRequired bool) (*ListingDTO, error) { var dto *ListingDTO err := r.db.Transaction(func(tx *gorm.DB) error { listing, account, err := r.findOwnedForUpdate(tx, ownerID, listingID) @@ -126,11 +138,12 @@ func (r *Repository) SubmitReview(ownerID uint64, listingID uint64) (*ListingDTO if listing.Status == "rented" { return ErrListingLocked } - listing.Status = "draft" - listing.ReviewStatus = "pending" + listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired) + listing.Status = listingStatus + listing.ReviewStatus = reviewStatus listing.ReviewReason = "" - listing.PublishedAt = nil - account.Status = "draft" + listing.PublishedAt = publishedAt + account.Status = listingStatus if err := tx.Save(account).Error; err != nil { return err } diff --git a/backend/internal/modules/listing/service.go b/backend/internal/modules/listing/service.go index 12f7c07..f2e39f0 100644 --- a/backend/internal/modules/listing/service.go +++ b/backend/internal/modules/listing/service.go @@ -2,6 +2,7 @@ package listing import ( "errors" + "strconv" "strings" ) @@ -9,14 +10,27 @@ 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 + repo *Repository + config ConfigReader } -func NewService(repo *Repository) *Service { - return &Service{repo: repo} +type ConfigReader interface { + FindValue(key string) (string, error) +} + +const reviewRequiredConfigKey = "listing.review_required" + +func NewService(repo *Repository, config ConfigReader) *Service { + return &Service{repo: repo, config: config} } func (s *Service) Create(ownerID uint64, req CreateRequest) (*ListingDTO, error) { @@ -26,7 +40,11 @@ func (s *Service) Create(ownerID uint64, req CreateRequest) (*ListingDTO, error) if err := validateRequest(req); err != nil { return nil, err } - return s.repo.Create(ownerID, req) + 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) { @@ -36,14 +54,22 @@ func (s *Service) Update(ownerID uint64, id uint64, req UpdateRequest) (*Listing if err := validateRequest(req); err != nil { return nil, err } - return s.repo.Update(ownerID, id, req) + 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 } - return s.repo.SubmitReview(ownerID, id) + reviewRequired, err := s.reviewRequired() + if err != nil { + return nil, err + } + return s.repo.SubmitReview(ownerID, id, reviewRequired) } func (s *Service) ListPendingReview() ([]ListingDTO, error) { @@ -140,17 +166,23 @@ func (s *Service) FindMine(ownerID uint64, id uint64) (*ListingDTO, error) { } func validateRequest(req CreateRequest) error { - if req.Title == "" || req.ServerRegion == "" { - return ErrInvalidInput + if strings.TrimSpace(req.Title) == "" { + return ErrMissingTitle } - if req.PriceHourly <= 0 || req.DepositAmount < 0 { - return ErrInvalidInput + if strings.TrimSpace(req.ServerRegion) == "" { + return ErrMissingServerRegion + } + if req.PriceHourly <= 0 { + return ErrInvalidPrice + } + if req.DepositAmount < 0 { + return ErrInvalidDeposit } if req.HafCoinAmount < 0 { - return ErrInvalidInput + return ErrInvalidHafCoin } if !hasScreenshotURL(req.ScreenshotURLS) { - return ErrInvalidInput + return ErrMissingScreenshot } return nil } @@ -163,3 +195,18 @@ func hasScreenshotURL(urls []string) bool { } 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 +} diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 4c7fa27..09dad5b 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -33,6 +33,7 @@ var defaultConfigs = []defaultConfig{ {Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"}, {Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后归还宽限分钟数"}, {Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"}, + {Key: "listing.review_required", Value: "false", Description: "发布账号是否需要后台人工审核"}, {Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"}, {Key: "risk.sms_limit_per_ip_hour", Value: "20", Description: "单 IP 每小时短信验证码次数"}, {Key: "realname.required_for_order", Value: "false", Description: "下单是否必须完成实名认证"}, diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 860e8d6..2a89a0c 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -77,8 +77,6 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { if deps.DB != nil { listingRepo = listing.NewRepository(deps.DB) } - listingService := listing.NewService(listingRepo) - listingHandler := listing.NewHandler(listingService) var orderRepo *order.Repository if deps.DB != nil { orderRepo = order.NewRepository(deps.DB) @@ -109,6 +107,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { } systemConfigService := systemconfig.NewService(systemConfigRepo) systemConfigHandler := systemconfig.NewHandler(systemConfigService) + listingService := listing.NewService(listingRepo, systemConfigRepo) + listingHandler := listing.NewHandler(listingService) var fileStorage *filemodule.Storage if cfg.Storage.Endpoint != "" && cfg.Storage.Bucket != "" { var err error diff --git a/frontend/src/utils/statusLabels.ts b/frontend/src/utils/statusLabels.ts new file mode 100644 index 0000000..099a43e --- /dev/null +++ b/frontend/src/utils/statusLabels.ts @@ -0,0 +1,147 @@ +const listingStatusMap: Record = { + draft: '草稿', + published: '已上架', + rented: '租用中', + offline: '已下架', + abnormal: '异常', +} + +const listingReviewStatusMap: Record = { + none: '未提交', + pending: '待审核', + approved: '已通过', + rejected: '已拒绝', +} + +const orderStatusMap: Record = { + pending_confirm: '待确认', + pending_handoff: '待交接', + renting: '使用中', + overdue: '已逾期', + pending_return_confirm: '待归还确认', + completed: '已完成', + cancelled: '已取消', + closed: '已关闭', + disputing: '申诉中', + abnormal: '异常', +} + +const handoffStatusMap: Record = { + pending_owner: '待号主交接', + pending_renter_confirm: '待租客确认', + received: '已确认收号', + pending_owner_return_confirm: '待号主确认归还', + returned: '已归还', + cancelled: '已取消', + owner_timeout: '号主交接超时', + renter_confirm_timeout: '租客确认超时', + return_overdue: '归还逾期', + owner_return_confirm_timeout: '号主确认归还超时', + admin_closed: '客服关闭', + admin_abnormal: '客服标记异常', + arbitrated: '已仲裁', +} + +const settlementStatusMap: Record = { + unsettled: '未结算', + pending: '待结算', + frozen: '冻结中', + settled: '已结算', + refunded: '已退款', + cancelled: '已取消', + closed: '已关闭', + arbitrated: '已仲裁', +} + +const realnameStatusMap: Record = { + unverified: '未认证', + pending: '认证中', + verified: '已认证', + rejected: '认证失败', +} + +const userStatusMap: Record = { + active: '正常', + frozen: '已冻结', + disabled: '已禁用', +} + +const riskStatusMap: Record = { + normal: '正常', + watch: '观察', + restricted: '受限', + blocked: '已拦截', +} + +const disputeStatusMap: Record = { + open: '待处理', + processing: '处理中', + resolved: '已处理', + closed: '已关闭', +} + +const walletStatusMap: Record = { + active: '正常', + frozen: '已冻结', + disabled: '已禁用', +} + +const ledgerDirectionMap: Record = { + in: '收入', + out: '支出', + freeze: '冻结', + unfreeze: '解冻', +} + +const balanceTypeMap: Record = { + available: '可用余额', + frozen: '冻结余额', +} + +export function listingStatusLabel(status: string) { + return listingStatusMap[status] || status || '-' +} + +export function listingReviewStatusLabel(status: string) { + return listingReviewStatusMap[status] || status || '-' +} + +export function orderStatusLabel(status: string) { + return orderStatusMap[status] || status || '-' +} + +export function handoffStatusLabel(status: string) { + return handoffStatusMap[status] || status || '-' +} + +export function settlementStatusLabel(status: string) { + return settlementStatusMap[status] || status || '-' +} + +export function realnameStatusLabel(status: string) { + return realnameStatusMap[status] || status || '-' +} + +export function userStatusLabel(status: string) { + return userStatusMap[status] || status || '-' +} + +export function riskStatusLabel(status: string) { + return riskStatusMap[status] || status || '-' +} + +export function disputeStatusLabel(status: string) { + return disputeStatusMap[status] || status || '-' +} + +export function walletStatusLabel(status: string) { + return walletStatusMap[status] || status || '-' +} + +export function ledgerDirectionLabel(direction: string) { + return ledgerDirectionMap[direction] || direction || '-' +} + +export function balanceTypeLabel(type: string) { + return balanceTypeMap[type] || type || '-' +} diff --git a/frontend/src/views/account/OrderDetailView.vue b/frontend/src/views/account/OrderDetailView.vue index 2ee0523..a2f7aa1 100644 --- a/frontend/src/views/account/OrderDetailView.vue +++ b/frontend/src/views/account/OrderDetailView.vue @@ -17,6 +17,7 @@ import { type Order, } from '@/api/orders' import { useSessionStore } from '@/stores/session' +import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels' import { formatDateTime } from '@/utils/time' const route = useRoute() @@ -190,11 +191,11 @@ function readError(error: unknown, fallback: string) {
状态 - {{ order.status }} + {{ orderStatusLabel(order.status) }}
交接 - {{ order.handoff_status }} + {{ handoffStatusLabel(order.handoff_status) }}
订单金额 diff --git a/frontend/src/views/account/OrdersView.vue b/frontend/src/views/account/OrdersView.vue index 1164659..5278081 100644 --- a/frontend/src/views/account/OrdersView.vue +++ b/frontend/src/views/account/OrdersView.vue @@ -2,6 +2,7 @@ import { onMounted, ref } from 'vue' import { fetchOrders, type Order } from '@/api/orders' +import { orderStatusLabel } from '@/utils/statusLabels' const loading = ref(false) const orders = ref([]) @@ -31,7 +32,9 @@ async function loadOrders() { - + + +