优化发布等各种状态
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: "下单是否必须完成实名认证"},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
const listingStatusMap: Record<string, string> = {
|
||||
draft: '草稿',
|
||||
published: '已上架',
|
||||
rented: '租用中',
|
||||
offline: '已下架',
|
||||
abnormal: '异常',
|
||||
}
|
||||
|
||||
const listingReviewStatusMap: Record<string, string> = {
|
||||
none: '未提交',
|
||||
pending: '待审核',
|
||||
approved: '已通过',
|
||||
rejected: '已拒绝',
|
||||
}
|
||||
|
||||
const orderStatusMap: Record<string, string> = {
|
||||
pending_confirm: '待确认',
|
||||
pending_handoff: '待交接',
|
||||
renting: '使用中',
|
||||
overdue: '已逾期',
|
||||
pending_return_confirm: '待归还确认',
|
||||
completed: '已完成',
|
||||
cancelled: '已取消',
|
||||
closed: '已关闭',
|
||||
disputing: '申诉中',
|
||||
abnormal: '异常',
|
||||
}
|
||||
|
||||
const handoffStatusMap: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
unsettled: '未结算',
|
||||
pending: '待结算',
|
||||
frozen: '冻结中',
|
||||
settled: '已结算',
|
||||
refunded: '已退款',
|
||||
cancelled: '已取消',
|
||||
closed: '已关闭',
|
||||
arbitrated: '已仲裁',
|
||||
}
|
||||
|
||||
const realnameStatusMap: Record<string, string> = {
|
||||
unverified: '未认证',
|
||||
pending: '认证中',
|
||||
verified: '已认证',
|
||||
rejected: '认证失败',
|
||||
}
|
||||
|
||||
const userStatusMap: Record<string, string> = {
|
||||
active: '正常',
|
||||
frozen: '已冻结',
|
||||
disabled: '已禁用',
|
||||
}
|
||||
|
||||
const riskStatusMap: Record<string, string> = {
|
||||
normal: '正常',
|
||||
watch: '观察',
|
||||
restricted: '受限',
|
||||
blocked: '已拦截',
|
||||
}
|
||||
|
||||
const disputeStatusMap: Record<string, string> = {
|
||||
open: '待处理',
|
||||
processing: '处理中',
|
||||
resolved: '已处理',
|
||||
closed: '已关闭',
|
||||
}
|
||||
|
||||
const walletStatusMap: Record<string, string> = {
|
||||
active: '正常',
|
||||
frozen: '已冻结',
|
||||
disabled: '已禁用',
|
||||
}
|
||||
|
||||
const ledgerDirectionMap: Record<string, string> = {
|
||||
in: '收入',
|
||||
out: '支出',
|
||||
freeze: '冻结',
|
||||
unfreeze: '解冻',
|
||||
}
|
||||
|
||||
const balanceTypeMap: Record<string, string> = {
|
||||
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 || '-'
|
||||
}
|
||||
@@ -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) {
|
||||
<div v-if="order" class="detail-grid">
|
||||
<div class="metric-card">
|
||||
<span>状态</span>
|
||||
<strong>{{ order.status }}</strong>
|
||||
<strong>{{ orderStatusLabel(order.status) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>交接</span>
|
||||
<strong>{{ order.handoff_status }}</strong>
|
||||
<strong>{{ handoffStatusLabel(order.handoff_status) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>订单金额</span>
|
||||
|
||||
@@ -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<Order[]>([])
|
||||
@@ -31,7 +32,9 @@ async function loadOrders() {
|
||||
<el-table-column prop="title" label="账号" min-width="180" />
|
||||
<el-table-column prop="rent_amount" label="订单金额" width="100" />
|
||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="140" />
|
||||
<el-table-column label="状态" width="140">
|
||||
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{ row }">
|
||||
<RouterLink :to="`/orders/${row.id}`">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { getRealnameStatus, startRealname, type RealnameStatus } from '@/api/realname'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { realnameStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const session = useSessionStore()
|
||||
@@ -59,7 +60,7 @@ function readError(error: unknown, fallback: string) {
|
||||
|
||||
<div v-if="status" class="status-panel">
|
||||
<span>当前状态</span>
|
||||
<strong>{{ status.status }}</strong>
|
||||
<strong>{{ realnameStatusLabel(status.status) }}</strong>
|
||||
<p v-if="status.masked_name">姓名:{{ status.masked_name }}</p>
|
||||
<p v-if="status.masked_id_no">证件号:{{ status.masked_id_no }}</p>
|
||||
<p v-if="status.verified_at">通过时间:{{ formatDateTime(status.verified_at) }}</p>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchWalletBalance, fetchWalletLedger, type WalletAccount, type WalletLedger } from '@/api/wallet'
|
||||
import { balanceTypeLabel, ledgerDirectionLabel, walletStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -41,16 +42,20 @@ async function loadWallet() {
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>状态</span>
|
||||
<strong>{{ account.status }}</strong>
|
||||
<strong>{{ walletStatusLabel(account.status) }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table class="table-panel" :data="ledger">
|
||||
<el-table-column prop="ledger_no" label="流水号" min-width="220" />
|
||||
<el-table-column prop="biz_type" label="业务" width="140" />
|
||||
<el-table-column prop="direction" label="方向" width="80" />
|
||||
<el-table-column label="方向" width="80">
|
||||
<template #default="{ row }">{{ ledgerDirectionLabel(row.direction) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="金额" width="100" />
|
||||
<el-table-column prop="balance_type" label="余额类型" width="110" />
|
||||
<el-table-column label="余额类型" width="110">
|
||||
<template #default="{ row }">{{ balanceTypeLabel(row.balance_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance_after" label="变化后余额" width="130" />
|
||||
<el-table-column prop="remark" label="备注" min-width="220" />
|
||||
<el-table-column label="时间" width="180">
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchAdminDashboard, type AdminDashboard } from '@/api/adminDashboard'
|
||||
import { disputeStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -118,7 +119,9 @@ function money(value?: number) {
|
||||
<el-table v-if="dashboard" class="table-panel" :data="dashboard.recent_orders">
|
||||
<el-table-column prop="order_no" label="最近订单" min-width="210" />
|
||||
<el-table-column prop="title" label="账号" min-width="170" />
|
||||
<el-table-column prop="status" label="状态" width="140" />
|
||||
<el-table-column label="状态" width="140">
|
||||
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rent_amount" label="订单金额" width="100" />
|
||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
@@ -130,7 +133,9 @@ function money(value?: number) {
|
||||
<el-table-column prop="order_no" label="最近申诉" min-width="210" />
|
||||
<el-table-column prop="title" label="账号" min-width="170" />
|
||||
<el-table-column prop="type" label="类型" width="160" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column label="状态" width="120">
|
||||
<template #default="{ row }">{{ disputeStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { onMounted, ref } from 'vue'
|
||||
|
||||
import { arbitrateDispute, fetchAdminDisputes, type Dispute } from '@/api/disputes'
|
||||
import { fetchAdminFileBlob } from '@/api/files'
|
||||
import { disputeStatusLabel } from '@/utils/statusLabels'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
@@ -105,7 +106,9 @@ function readError(error: unknown, fallback: string) {
|
||||
<el-table-column prop="order_no" label="订单号" min-width="210" />
|
||||
<el-table-column prop="title" label="账号" min-width="160" />
|
||||
<el-table-column prop="type" label="类型" width="150" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">{{ disputeStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="说明" min-width="220" show-overflow-tooltip />
|
||||
<el-table-column prop="arbitration_result" label="结果" width="150" />
|
||||
<el-table-column label="证据" width="100">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import { fetchAdminFileBlob } from '@/api/files'
|
||||
import { adminMarkListingAbnormal, adminOfflineListing, fetchAdminListing, type Listing } from '@/api/listings'
|
||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -107,11 +108,11 @@ function readError(error: unknown, fallback: string) {
|
||||
<div v-if="listing" class="metric-grid dashboard-metrics">
|
||||
<div class="metric-card">
|
||||
<span>商品状态</span>
|
||||
<strong>{{ listing.status }}</strong>
|
||||
<strong>{{ listingStatusLabel(listing.status) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>审核状态</span>
|
||||
<strong>{{ listing.review_status }}</strong>
|
||||
<strong>{{ listingReviewStatusLabel(listing.review_status) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>价格</span>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminListings, type Listing } from '@/api/listings'
|
||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -149,12 +150,12 @@ function reviewType(status: string) {
|
||||
</el-table-column>
|
||||
<el-table-column label="商品状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)">{{ row.status }}</el-tag>
|
||||
<el-tag :type="statusType(row.status)">{{ listingStatusLabel(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="reviewType(row.review_status)">{{ row.review_status }}</el-tag>
|
||||
<el-tag :type="reviewType(row.review_status)">{{ listingReviewStatusLabel(row.review_status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上架时间" min-width="180">
|
||||
|
||||
@@ -4,6 +4,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
import { adminCloseOrder, adminMarkOrderAbnormal, fetchAdminHandoffRecords, fetchAdminOrder, type HandoffRecord, type Order } from '@/api/orders'
|
||||
import { handoffStatusLabel, orderStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -84,11 +85,11 @@ function readError(error: unknown, fallback: string) {
|
||||
<div v-if="order" class="metric-grid dashboard-metrics">
|
||||
<div class="metric-card">
|
||||
<span>订单状态</span>
|
||||
<strong>{{ order.status }}</strong>
|
||||
<strong>{{ orderStatusLabel(order.status) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>交接状态</span>
|
||||
<strong>{{ order.handoff_status }}</strong>
|
||||
<strong>{{ handoffStatusLabel(order.handoff_status) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span>订单金额</span>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchAdminOrders, type Order } from '@/api/orders'
|
||||
import { handoffStatusLabel, orderStatusLabel, settlementStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -54,9 +55,15 @@ async function loadOrders() {
|
||||
<el-table-column prop="title" label="账号" min-width="170" />
|
||||
<el-table-column prop="renter_phone" label="租客" min-width="130" />
|
||||
<el-table-column prop="owner_phone" label="号主" min-width="130" />
|
||||
<el-table-column prop="status" label="状态" width="140" />
|
||||
<el-table-column prop="handoff_status" label="交接" width="180" />
|
||||
<el-table-column prop="settlement_status" label="结算" width="120" />
|
||||
<el-table-column label="状态" width="140">
|
||||
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="交接" width="180">
|
||||
<template #default="{ row }">{{ handoffStatusLabel(row.handoff_status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结算" width="120">
|
||||
<template #default="{ row }">{{ settlementStatusLabel(row.settlement_status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rent_amount" label="订单金额" width="100" />
|
||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
||||
<el-table-column label="创建时间" min-width="180">
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchAdminUsers, freezeAdminUser, unfreezeAdminUser, type AdminUserItem } from '@/api/adminUsers'
|
||||
import { realnameStatusLabel, riskStatusLabel, userStatusLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -79,10 +80,16 @@ function readError(error: unknown, fallback: string) {
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="phone" label="手机号" min-width="130" />
|
||||
<el-table-column prop="nickname" label="昵称" min-width="130" />
|
||||
<el-table-column prop="realname_status" label="实名" width="110" />
|
||||
<el-table-column prop="risk_status" label="风险" width="110" />
|
||||
<el-table-column label="实名" width="110">
|
||||
<template #default="{ row }">{{ realnameStatusLabel(row.realname_status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="风险" width="110">
|
||||
<template #default="{ row }">{{ riskStatusLabel(row.risk_status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="credit_score" label="信用分" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">{{ userStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="order_count" label="订单" width="90" />
|
||||
<el-table-column prop="listing_count" label="发布" width="90" />
|
||||
<el-table-column prop="dispute_count" label="申诉" width="90" />
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Search } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import { fetchAdminWalletLedger, type AdminWalletLedger } from '@/api/adminWallet'
|
||||
import { balanceTypeLabel, ledgerDirectionLabel } from '@/utils/statusLabels'
|
||||
import { formatDateTime } from '@/utils/time'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -48,11 +49,13 @@ function money(value: number) {
|
||||
}
|
||||
|
||||
function directionType(direction: string) {
|
||||
return direction === 'in' ? 'success' : 'danger'
|
||||
if (direction === 'in') return 'success'
|
||||
if (direction === 'out') return 'danger'
|
||||
return 'warning'
|
||||
}
|
||||
|
||||
function directionLabel(direction: string) {
|
||||
return direction === 'in' ? '入账' : '出账'
|
||||
return ledgerDirectionLabel(direction)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -132,7 +135,9 @@ function directionLabel(direction: string) {
|
||||
<el-table-column label="金额" width="110">
|
||||
<template #default="{ row }">{{ money(Number(row.amount)) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance_type" label="余额类型" width="110" />
|
||||
<el-table-column label="余额类型" width="110">
|
||||
<template #default="{ row }">{{ balanceTypeLabel(row.balance_type) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变化后余额" width="130">
|
||||
<template #default="{ row }">{{ money(Number(row.balance_after)) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
startRealname,
|
||||
type RealnameStatus,
|
||||
} from "@/api/realname";
|
||||
import { realnameStatusLabel } from "@/utils/statusLabels";
|
||||
import { useSessionStore } from "@/stores/session";
|
||||
import { formatDateTime } from "@/utils/time";
|
||||
|
||||
@@ -106,7 +107,7 @@ async function handleSubmit() {
|
||||
size="medium"
|
||||
round
|
||||
>
|
||||
{{ status.status === "verified" ? "已认证" : status.status }}
|
||||
{{ realnameStatusLabel(status.status) }}
|
||||
</van-tag>
|
||||
</div>
|
||||
<p v-if="status.masked_name" class="status-detail">
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
.mobile-publish {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 100dvh;
|
||||
background: #f4f6f8;
|
||||
padding-bottom: 56px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@@ -528,18 +531,22 @@
|
||||
bottom: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
background: #fff;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
flex: 1;
|
||||
flex: 0 0 20%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
height: 50px;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
text-decoration: none;
|
||||
|
||||
@@ -356,7 +356,7 @@ async function handleSubmit() {
|
||||
const dailyPrice = calculatedFinalPrice.value;
|
||||
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
|
||||
|
||||
await createListing({
|
||||
const listing = await createListing({
|
||||
title,
|
||||
description: form.remark,
|
||||
server_region: form.server_region,
|
||||
@@ -371,7 +371,13 @@ async function handleSubmit() {
|
||||
deposit_amount: Number(form.deposit_amount),
|
||||
});
|
||||
localStorage.removeItem(draftKey);
|
||||
showToast({ message: "发布成功", icon: "passed" });
|
||||
showToast({
|
||||
message:
|
||||
listing.status === "published" && listing.review_status === "approved"
|
||||
? "发布成功,已上架"
|
||||
: "发布成功,等待后台审核",
|
||||
icon: "passed",
|
||||
});
|
||||
await router.push("/m/profile");
|
||||
} catch (error) {
|
||||
showToast({
|
||||
@@ -396,9 +402,15 @@ function validateForm() {
|
||||
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
|
||||
return "请填写押金";
|
||||
}
|
||||
if (!Number.isFinite(Number(form.deposit_amount))) {
|
||||
return "押金格式不正确";
|
||||
}
|
||||
if (!calculatedFinalPrice.value) {
|
||||
return "请完善币数、保险、体力和负重后再发布";
|
||||
}
|
||||
if (!Number.isFinite(calculatedFinalPrice.value)) {
|
||||
return "发布价格计算异常,请检查填写内容";
|
||||
}
|
||||
for (const item of screenshotSlots.value) {
|
||||
if (isScreenshotRequired(item) && !screenshotFiles[item.key]) {
|
||||
return `请上传${item.label}`;
|
||||
@@ -705,7 +717,7 @@ function readError(error: unknown, fallback: string) {
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
min="0"
|
||||
:placeholder="item.placeholder || '0'"
|
||||
placeholder="0"
|
||||
class="quantity-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { Refresh, Search } from "@element-plus/icons-vue";
|
||||
|
||||
import { fetchListings, type Listing } from "@/api/listings";
|
||||
import { listingStatusLabel } from "@/utils/statusLabels";
|
||||
|
||||
const loading = ref(false);
|
||||
const listings = ref<Listing[]>([]);
|
||||
@@ -192,7 +193,7 @@ function listingPrice(item: Listing) {
|
||||
<div class="resource-main">
|
||||
<div class="resource-title-line">
|
||||
<strong>{{ item.title }}</strong>
|
||||
<em>{{ item.status === "published" ? "可租" : item.status }}</em>
|
||||
<em>{{ item.status === "published" ? "可租" : listingStatusLabel(item.status) }}</em>
|
||||
</div>
|
||||
<p>
|
||||
{{
|
||||
|
||||
@@ -27,14 +27,18 @@ async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
const price = Number(form.price_daily || 0)
|
||||
await createListing({
|
||||
const listing = await createListing({
|
||||
...form,
|
||||
price_hourly: Math.max(Math.round((price / 24) * 100) / 100, 0.01),
|
||||
price_daily: price,
|
||||
price_weekly: Math.round(price * 7 * 100) / 100,
|
||||
screenshot_urls: screenshotUrls.value,
|
||||
})
|
||||
ElMessage.success('发布已创建')
|
||||
ElMessage.success(
|
||||
listing.status === 'published' && listing.review_status === 'approved'
|
||||
? '发布成功,已上架'
|
||||
: '发布成功,等待后台审核',
|
||||
)
|
||||
await router.push('/seller/listings')
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '发布失败,请确认已登录并完成实名认证'))
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ElMessage } from 'element-plus'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchSellerListings, offlineListing, submitListingReview, type Listing } from '@/api/listings'
|
||||
import { listingReviewStatusLabel, listingStatusLabel } from '@/utils/statusLabels'
|
||||
|
||||
const loading = ref(false)
|
||||
const listings = ref<Listing[]>([])
|
||||
@@ -19,8 +20,12 @@ async function loadListings() {
|
||||
}
|
||||
|
||||
async function submitReview(id: number) {
|
||||
await submitListingReview(id)
|
||||
ElMessage.success('已提交审核,等待后台处理')
|
||||
const listing = await submitListingReview(id)
|
||||
ElMessage.success(
|
||||
listing.status === 'published' && listing.review_status === 'approved'
|
||||
? '已上架'
|
||||
: '已提交审核,等待后台处理',
|
||||
)
|
||||
await loadListings()
|
||||
}
|
||||
|
||||
@@ -56,12 +61,20 @@ function listingPrice(row: Listing) {
|
||||
<template #default="{ row }">{{ listingPrice(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deposit_amount" label="押金" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="110" />
|
||||
<el-table-column prop="review_status" label="审核" width="110" />
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">{{ listingStatusLabel(row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="审核" width="110">
|
||||
<template #default="{ row }">{{ listingReviewStatusLabel(row.review_status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="review_reason" label="审核原因" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="190">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" :disabled="row.review_status === 'pending' || row.status === 'rented'" @click="submitReview(row.id)">
|
||||
<el-button
|
||||
size="small"
|
||||
:disabled="row.review_status === 'pending' || row.status === 'rented' || row.status === 'published'"
|
||||
@click="submitReview(row.id)"
|
||||
>
|
||||
提审
|
||||
</el-button>
|
||||
<el-button size="small" type="danger" :disabled="row.status === 'rented'" @click="offline(row.id)">下架</el-button>
|
||||
|
||||
Reference in New Issue
Block a user