优化发布等各种状态

This commit is contained in:
yml2213
2026-05-23 15:52:44 +08:00
parent f2492d5e40
commit b101415e9d
25 changed files with 368 additions and 70 deletions
+2 -2
View File
@@ -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):
+29 -16
View File
@@ -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
}
+59 -12
View File
@@ -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: "下单是否必须完成实名认证"},
+2 -2
View File
@@ -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
+147
View File
@@ -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>
+4 -1
View File
@@ -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}`">
+2 -1
View File
@@ -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>
+8 -3
View File
@@ -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>
+10 -3
View File
@@ -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">
+10 -3
View File
@@ -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>
+2 -1
View File
@@ -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>