删掉租期相关错误东西

This commit is contained in:
yml2213
2026-05-23 14:44:32 +08:00
parent e6cc9f1255
commit a6a1afb879
41 changed files with 127 additions and 312 deletions
+2 -2
View File
@@ -254,7 +254,7 @@ func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresh
UserID: order.RenterID, UserID: order.RenterID,
Type: "timeout", Type: "timeout",
Title: "订单已逾期未归还", Title: "订单已逾期未归还",
Content: "租期已超时,请尽快提交归还说明,避免进入申诉处理。", Content: "订单已超过预计截止时间,请尽快提交归还说明,避免进入申诉处理。",
BizType: "order", BizType: "order",
BizID: &orderID, BizID: &orderID,
}, },
@@ -262,7 +262,7 @@ func (j *Job) handleReturnOverdue(ctx context.Context, now time.Time, cfg thresh
UserID: order.OwnerID, UserID: order.OwnerID,
Type: "timeout", Type: "timeout",
Title: "租客逾期未归还", Title: "租客逾期未归还",
Content: "租客未在租期结束后及时归还,你可以发起申诉或等待客服处理。", Content: "租客未在预计截止后及时归还,你可以发起申诉或等待客服处理。",
BizType: "order", BizType: "order",
BizID: &orderID, BizID: &orderID,
}, },
-2
View File
@@ -36,8 +36,6 @@ type RentalListing struct {
PriceDaily float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_daily"` PriceDaily float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_daily"`
PriceWeekly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_weekly"` PriceWeekly float64 `gorm:"type:decimal(12,2);not null;default:0" json:"price_weekly"`
DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"` DepositAmount float64 `gorm:"type:decimal(12,2);not null;default:0" json:"deposit_amount"`
MinRentHours int `gorm:"not null;default:1" json:"min_rent_hours"`
MaxRentHours int `gorm:"not null;default:168" json:"max_rent_hours"`
Status string `gorm:"size:32;not null;default:'draft'" json:"status"` Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"` ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"` ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"`
@@ -320,7 +320,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
addRenterRefund(req.Amount, "仲裁部分退款") addRenterRefund(req.Amount, "仲裁部分退款")
addOwnerIncome(total-req.Amount, "仲裁剩余金额结算给号主") addOwnerIncome(total-req.Amount, "仲裁剩余金额结算给号主")
case "release_deposit": case "release_deposit":
addOwnerIncome(order.RentAmount, "仲裁确认租金结算给号主") addOwnerIncome(order.RentAmount, "仲裁确认订单金额结算给号主")
addRenterRefund(order.DepositAmount, "仲裁释放押金给租客") addRenterRefund(order.DepositAmount, "仲裁释放押金给租客")
case "deduct_deposit", "compensate_owner": case "deduct_deposit", "compensate_owner":
deductAmount := req.Amount deductAmount := req.Amount
@@ -331,7 +331,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
return settlement, ErrInvalidDispute return settlement, ErrInvalidDispute
} }
settlement.DepositDeductAmount = deductAmount settlement.DepositDeductAmount = deductAmount
addOwnerIncome(order.RentAmount+deductAmount, "仲裁租金及押金赔付结算给号主") addOwnerIncome(order.RentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客") addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客")
case "order_close": case "order_close":
// Only release frozen funds. No available-balance settlement happens in development mode. // Only release frozen funds. No available-balance settlement happens in development mode.
-4
View File
@@ -22,8 +22,6 @@ type ListingDTO struct {
PriceDaily float64 `json:"price_daily"` PriceDaily float64 `json:"price_daily"`
PriceWeekly float64 `json:"price_weekly"` PriceWeekly float64 `json:"price_weekly"`
DepositAmount float64 `json:"deposit_amount"` DepositAmount float64 `json:"deposit_amount"`
MinRentHours int `json:"min_rent_hours"`
MaxRentHours int `json:"max_rent_hours"`
Status string `json:"status"` Status string `json:"status"`
ReviewStatus string `json:"review_status"` ReviewStatus string `json:"review_status"`
ReviewReason string `json:"review_reason"` ReviewReason string `json:"review_reason"`
@@ -45,8 +43,6 @@ type CreateRequest struct {
PriceDaily float64 `json:"price_daily"` PriceDaily float64 `json:"price_daily"`
PriceWeekly float64 `json:"price_weekly"` PriceWeekly float64 `json:"price_weekly"`
DepositAmount float64 `json:"deposit_amount" binding:"required"` DepositAmount float64 `json:"deposit_amount" binding:"required"`
MinRentHours int `json:"min_rent_hours" binding:"required"`
MaxRentHours int `json:"max_rent_hours" binding:"required"`
} }
type UpdateRequest = CreateRequest type UpdateRequest = CreateRequest
@@ -56,8 +56,6 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest) (*ListingDTO, err
PriceDaily: req.PriceDaily, PriceDaily: req.PriceDaily,
PriceWeekly: req.PriceWeekly, PriceWeekly: req.PriceWeekly,
DepositAmount: req.DepositAmount, DepositAmount: req.DepositAmount,
MinRentHours: req.MinRentHours,
MaxRentHours: req.MaxRentHours,
Status: "draft", Status: "draft",
ReviewStatus: "none", ReviewStatus: "none",
} }
@@ -105,8 +103,6 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest)
listing.PriceDaily = req.PriceDaily listing.PriceDaily = req.PriceDaily
listing.PriceWeekly = req.PriceWeekly listing.PriceWeekly = req.PriceWeekly
listing.DepositAmount = req.DepositAmount listing.DepositAmount = req.DepositAmount
listing.MinRentHours = req.MinRentHours
listing.MaxRentHours = req.MaxRentHours
listing.Status = "draft" listing.Status = "draft"
listing.ReviewStatus = "none" listing.ReviewStatus = "none"
listing.ReviewReason = "" listing.ReviewReason = ""
@@ -470,8 +466,6 @@ func (row listingRow) toDTO() ListingDTO {
PriceDaily: row.PriceDaily, PriceDaily: row.PriceDaily,
PriceWeekly: row.PriceWeekly, PriceWeekly: row.PriceWeekly,
DepositAmount: row.DepositAmount, DepositAmount: row.DepositAmount,
MinRentHours: row.MinRentHours,
MaxRentHours: row.MaxRentHours,
Status: row.Status, Status: row.Status,
ReviewStatus: row.ReviewStatus, ReviewStatus: row.ReviewStatus,
ReviewReason: row.ReviewReason, ReviewReason: row.ReviewReason,
@@ -502,8 +496,6 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
PriceDaily: listing.PriceDaily, PriceDaily: listing.PriceDaily,
PriceWeekly: listing.PriceWeekly, PriceWeekly: listing.PriceWeekly,
DepositAmount: listing.DepositAmount, DepositAmount: listing.DepositAmount,
MinRentHours: listing.MinRentHours,
MaxRentHours: listing.MaxRentHours,
Status: listing.Status, Status: listing.Status,
ReviewStatus: listing.ReviewStatus, ReviewStatus: listing.ReviewStatus,
ReviewReason: listing.ReviewReason, ReviewReason: listing.ReviewReason,
@@ -146,9 +146,6 @@ func validateRequest(req CreateRequest) error {
if req.PriceHourly <= 0 || req.DepositAmount < 0 { if req.PriceHourly <= 0 || req.DepositAmount < 0 {
return ErrInvalidInput return ErrInvalidInput
} }
if req.MinRentHours <= 0 || req.MaxRentHours < req.MinRentHours {
return ErrInvalidInput
}
if req.HafCoinAmount < 0 { if req.HafCoinAmount < 0 {
return ErrInvalidInput return ErrInvalidInput
} }
+1 -1
View File
@@ -1,3 +1,3 @@
# Order Module # Order Module
订单状态机、账号交接、租期归还、超时处理。 订单状态机、账号交接、归还、超时处理。
-2
View File
@@ -20,7 +20,6 @@ type OrderDTO struct {
LoginPlatform string `json:"login_platform"` LoginPlatform string `json:"login_platform"`
RentStartAt *time.Time `json:"rent_start_at"` RentStartAt *time.Time `json:"rent_start_at"`
RentEndAt *time.Time `json:"rent_end_at"` RentEndAt *time.Time `json:"rent_end_at"`
RentHours int `json:"rent_hours"`
RentAmount float64 `json:"rent_amount"` RentAmount float64 `json:"rent_amount"`
DepositAmount float64 `json:"deposit_amount"` DepositAmount float64 `json:"deposit_amount"`
PlatformFee float64 `json:"platform_fee"` PlatformFee float64 `json:"platform_fee"`
@@ -34,7 +33,6 @@ type OrderDTO struct {
type CreateRequest struct { type CreateRequest struct {
ListingID uint64 `json:"listing_id" binding:"required"` ListingID uint64 `json:"listing_id" binding:"required"`
RentHours int `json:"rent_hours" binding:"required"`
} }
type SubmitHandoffRequest struct { type SubmitHandoffRequest struct {
+1 -1
View File
@@ -282,7 +282,7 @@ func writeOrderError(c *gin.Context, err error) {
case errors.Is(err, ErrDependencyUnavailable): case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接") response.ServiceUnavailable(c, "数据库未连接")
case errors.Is(err, ErrInvalidRentHours): case errors.Is(err, ErrInvalidRentHours):
response.BadRequest(c, "租期不符合规则") response.BadRequest(c, "订单信息不符合规则")
case errors.Is(err, ErrListingUnavailable): case errors.Is(err, ErrListingUnavailable):
response.Error(c, http.StatusConflict, "listing_unavailable", "该账号暂不可租") response.Error(c, http.StatusConflict, "listing_unavailable", "该账号暂不可租")
case errors.Is(err, ErrCannotRentOwnListing): case errors.Is(err, ErrCannotRentOwnListing):
+8 -11
View File
@@ -38,9 +38,6 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
if listing.OwnerID == renterID { if listing.OwnerID == renterID {
return ErrCannotRentOwnListing return ErrCannotRentOwnListing
} }
if req.RentHours < listing.MinRentHours || req.RentHours > listing.MaxRentHours {
return ErrInvalidRentHours
}
var account model.GameAccount var account model.GameAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil { if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil {
@@ -55,7 +52,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
return err return err
} }
now := time.Now() now := time.Now()
rentEnd := now.Add(time.Duration(req.RentHours) * time.Hour) rentHours := internalOrderHours
rentEnd := now.Add(time.Duration(rentHours) * time.Hour)
order := model.RentalOrder{ order := model.RentalOrder{
OrderNo: orderNo, OrderNo: orderNo,
ListingID: listing.ID, ListingID: listing.ID,
@@ -64,8 +62,8 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
RenterID: renterID, RenterID: renterID,
RentStartAt: &now, RentStartAt: &now,
RentEndAt: &rentEnd, RentEndAt: &rentEnd,
RentHours: req.RentHours, RentHours: rentHours,
RentAmount: listing.PriceHourly * float64(req.RentHours), RentAmount: listing.PriceHourly * float64(rentHours),
DepositAmount: listing.DepositAmount, DepositAmount: listing.DepositAmount,
PlatformFee: 0, PlatformFee: 0,
AccountSnapshot: snapshot, AccountSnapshot: snapshot,
@@ -86,7 +84,7 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro
BalanceType: "frozen", BalanceType: "frozen",
BizType: "order_lock", BizType: "order_lock",
BizNo: order.OrderNo, BizNo: order.OrderNo,
Remark: "开发态模拟冻结租金和押金", Remark: "开发态模拟冻结订单金额和押金",
}, },
); err != nil { ); err != nil {
return err return err
@@ -272,7 +270,7 @@ func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error {
UserID: order.OwnerID, UserID: order.OwnerID,
Type: "handoff", Type: "handoff",
Title: "租客已确认收号", Title: "租客已确认收号",
Content: "订单已进入租赁中。", Content: "订单已进入使用中。",
BizType: "order", BizType: "order",
BizID: &orderID, BizID: &orderID,
}); err != nil { }); err != nil {
@@ -399,7 +397,7 @@ func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
BalanceType: "available", BalanceType: "available",
BizType: "owner_income", BizType: "owner_income",
BizNo: order.OrderNo, BizNo: order.OrderNo,
Remark: "订单完成模拟结算租金", Remark: "订单完成模拟结算订单金额",
}, },
wallet.Entry{ wallet.Entry{
UserID: order.RenterID, UserID: order.RenterID,
@@ -427,7 +425,7 @@ func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error {
UserID: order.OwnerID, UserID: order.OwnerID,
Type: "settlement", Type: "settlement",
Title: "订单已完成", Title: "订单已完成",
Content: "订单已完成,模拟租金已入账。", Content: "订单已完成,模拟订单金额已入账。",
BizType: "order", BizType: "order",
BizID: &orderID, BizID: &orderID,
}, },
@@ -727,7 +725,6 @@ func (row orderRow) toDTO() OrderDTO {
LoginPlatform: row.LoginPlatform, LoginPlatform: row.LoginPlatform,
RentStartAt: row.RentStartAt, RentStartAt: row.RentStartAt,
RentEndAt: row.RentEndAt, RentEndAt: row.RentEndAt,
RentHours: row.RentHours,
RentAmount: row.RentAmount, RentAmount: row.RentAmount,
DepositAmount: row.DepositAmount, DepositAmount: row.DepositAmount,
PlatformFee: row.PlatformFee, PlatformFee: row.PlatformFee,
+3 -1
View File
@@ -15,6 +15,8 @@ var (
ErrPermissionDenied = errors.New("permission denied") ErrPermissionDenied = errors.New("permission denied")
) )
const internalOrderHours = 24
type Service struct { type Service struct {
repo *Repository repo *Repository
} }
@@ -27,7 +29,7 @@ func (s *Service) Create(userID uint64, req CreateRequest) (*OrderDTO, error) {
if s.repo == nil { if s.repo == nil {
return nil, ErrDependencyUnavailable return nil, ErrDependencyUnavailable
} }
if req.ListingID == 0 || req.RentHours <= 0 { if req.ListingID == 0 {
return nil, ErrInvalidRentHours return nil, ErrInvalidRentHours
} }
return s.repo.Create(userID, req) return s.repo.Create(userID, req)
@@ -83,7 +83,6 @@ type PublishRatioConfig struct {
InsuranceBaseRatios []PublishInsuranceBaseRatio `json:"insurance_base_ratios"` InsuranceBaseRatios []PublishInsuranceBaseRatio `json:"insurance_base_ratios"`
ConfigItems []PublishRatioConfigItem `json:"config_items"` ConfigItems []PublishRatioConfigItem `json:"config_items"`
CoinCorrections []PublishCoinCorrection `json:"coin_corrections"` CoinCorrections []PublishCoinCorrection `json:"coin_corrections"`
RentRules []PublishRentRule `json:"rent_rules"`
} }
type PublishInsuranceBaseRatio struct { type PublishInsuranceBaseRatio struct {
@@ -103,8 +102,3 @@ type PublishCoinCorrection struct {
ThresholdM float64 `json:"threshold_m"` ThresholdM float64 `json:"threshold_m"`
Correction float64 `json:"correction"` Correction float64 `json:"correction"`
} }
type PublishRentRule struct {
ThresholdM float64 `json:"threshold_m"`
Days int `json:"days"`
}
@@ -106,11 +106,6 @@ func DefaultPublishOptions() PublishOptionsDTO {
{ThresholdM: 380, Correction: 4}, {ThresholdM: 380, Correction: 4},
{ThresholdM: 530, Correction: 5}, {ThresholdM: 530, Correction: 5},
}, },
RentRules: []PublishRentRule{
{ThresholdM: 300, Days: 7},
{ThresholdM: 100, Days: 3},
{ThresholdM: 0, Days: 1},
},
}, },
} }
} }
@@ -31,7 +31,7 @@ var defaultConfigs = []defaultConfig{
{Key: "handoff.owner_submit_timeout_minutes", Value: "30", Description: "号主待交接超时分钟数"}, {Key: "handoff.owner_submit_timeout_minutes", Value: "30", Description: "号主待交接超时分钟数"},
{Key: "handoff.renter_confirm_timeout_minutes", Value: "30", Description: "租客待确认收号超时分钟数"}, {Key: "handoff.renter_confirm_timeout_minutes", Value: "30", Description: "租客待确认收号超时分钟数"},
{Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"}, {Key: "handoff.owner_return_confirm_timeout_minutes", Value: "120", Description: "号主待确认归还超时分钟数"},
{Key: "order.return_overdue_grace_minutes", Value: "10", Description: "租期到期后归还宽限分钟数"}, {Key: "order.return_overdue_grace_minutes", Value: "10", Description: "预计截止后归还宽限分钟数"},
{Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"}, {Key: "deposit.min_amount", Value: "50", Description: "发布租号最低押金"},
{Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"}, {Key: "risk.sms_limit_per_phone_hour", Value: "5", Description: "单手机号每小时短信验证码次数"},
{Key: "risk.sms_limit_per_ip_hour", Value: "20", Description: "单 IP 每小时短信验证码次数"}, {Key: "risk.sms_limit_per_ip_hour", Value: "20", Description: "单 IP 每小时短信验证码次数"},
@@ -211,7 +211,4 @@ func normalizePublishOptions(options *PublishOptionsDTO) {
if len(options.RatioConfig.CoinCorrections) == 0 { if len(options.RatioConfig.CoinCorrections) == 0 {
options.RatioConfig.CoinCorrections = defaults.RatioConfig.CoinCorrections options.RatioConfig.CoinCorrections = defaults.RatioConfig.CoinCorrections
} }
if len(options.RatioConfig.RentRules) == 0 {
options.RatioConfig.RentRules = defaults.RatioConfig.RentRules
}
} }
+1 -1
View File
@@ -1,3 +1,3 @@
# Wallet Module # Wallet Module
押金冻结、租金结算、不可变资金流水。 押金冻结、订单金额结算、不可变资金流水。
-2
View File
@@ -57,8 +57,6 @@ CREATE TABLE rental_listings (
price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00, price_daily DECIMAL(12,2) NOT NULL DEFAULT 0.00,
price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00, price_weekly DECIMAL(12,2) NOT NULL DEFAULT 0.00,
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00, deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
min_rent_hours INT NOT NULL DEFAULT 1,
max_rent_hours INT NOT NULL DEFAULT 168,
status VARCHAR(32) NOT NULL DEFAULT 'draft', status VARCHAR(32) NOT NULL DEFAULT 'draft',
review_status VARCHAR(32) NOT NULL DEFAULT 'none', review_status VARCHAR(32) NOT NULL DEFAULT 'none',
review_reason VARCHAR(255) NOT NULL DEFAULT '', review_reason VARCHAR(255) NOT NULL DEFAULT '',
+10 -10
View File
@@ -214,7 +214,7 @@ VALUES
( (
@owner_c, '三角洲行动', '微信', '扫码', @owner_c, '三角洲行动', '微信', '扫码',
'纯币180M/6格/6体/5负/80发AWM', '纯币180M/6格/6体/5负/80发AWM',
'扫码交接,资源适中,适合三日租', '扫码交接,资源适中,适合短期使用',
'黑鹰', 180000000, '黑鹰', 180000000,
JSON_OBJECT( JSON_OBJECT(
'face_owner', '', 'face_owner', '',
@@ -361,29 +361,29 @@ VALUES
); );
INSERT INTO rental_listings INSERT INTO rental_listings
(account_id, owner_id, price_hourly, price_daily, price_weekly, deposit_amount, min_rent_hours, max_rent_hours, status, review_status, published_at, created_at, updated_at) (account_id, owner_id, price_hourly, price_daily, price_weekly, deposit_amount, status, review_status, published_at, created_at, updated_at)
SELECT id, owner_id, 29.76, 714.29, 5000.00, 120.00, 24, 168, 'published', 'approved', NOW() - INTERVAL 80 MINUTE, created_at, updated_at SELECT id, owner_id, 29.76, 714.29, 5000.00, 120.00, 'published', 'approved', NOW() - INTERVAL 80 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币300M/6格/6体/6负/4六甲/6六头' FROM game_accounts WHERE title = '纯币300M/6格/6体/6负/4六甲/6六头'
UNION ALL UNION ALL
SELECT id, owner_id, 33.33, 800.00, 2400.00, 60.00, 24, 72, 'published', 'approved', NOW() - INTERVAL 70 MINUTE, created_at, updated_at SELECT id, owner_id, 33.33, 800.00, 2400.00, 60.00, 'published', 'approved', NOW() - INTERVAL 70 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币120M/4格/5体/5负/2六头' FROM game_accounts WHERE title = '纯币120M/4格/5体/5负/2六头'
UNION ALL UNION ALL
SELECT id, owner_id, 62.50, 1500.00, 1500.00, 80.00, 8, 24, 'published', 'approved', NOW() - INTERVAL 60 MINUTE, created_at, updated_at SELECT id, owner_id, 62.50, 1500.00, 1500.00, 80.00, 'published', 'approved', NOW() - INTERVAL 60 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币60M/2格/4体/4负/1体验卡' FROM game_accounts WHERE title = '纯币60M/2格/4体/4负/1体验卡'
UNION ALL UNION ALL
SELECT id, owner_id, 42.52, 1020.41, 7142.86, 180.00, 24, 168, 'published', 'approved', NOW() - INTERVAL 50 MINUTE, created_at, updated_at SELECT id, owner_id, 42.52, 1020.41, 7142.86, 180.00, 'published', 'approved', NOW() - INTERVAL 50 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币500M/9格/7体/7负/10六头' FROM game_accounts WHERE title = '纯币500M/9格/7体/7负/10六头'
UNION ALL UNION ALL
SELECT id, owner_id, 50.00, 1200.00, 3600.00, 100.00, 24, 72, 'published', 'approved', NOW() - INTERVAL 40 MINUTE, created_at, updated_at SELECT id, owner_id, 50.00, 1200.00, 3600.00, 100.00, 'published', 'approved', NOW() - INTERVAL 40 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币180M/6格/6体/5负/80发AWM' FROM game_accounts WHERE title = '纯币180M/6格/6体/5负/80发AWM'
UNION ALL UNION ALL
SELECT id, owner_id, 31.25, 750.00, 750.00, 50.00, 8, 24, 'published', 'approved', NOW() - INTERVAL 30 MINUTE, created_at, updated_at SELECT id, owner_id, 31.25, 750.00, 750.00, 50.00, 'published', 'approved', NOW() - INTERVAL 30 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币90M/4格/5体/4负/3五级套' FROM game_accounts WHERE title = '纯币90M/4格/5体/4负/3五级套'
UNION ALL UNION ALL
SELECT id, owner_id, 25.79, 619.05, 4333.33, 120.00, 24, 168, 'published', 'approved', NOW() - INTERVAL 20 MINUTE, created_at, updated_at SELECT id, owner_id, 25.79, 619.05, 4333.33, 120.00, 'published', 'approved', NOW() - INTERVAL 20 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币260M/6格/6体/6负/皮肤多' FROM game_accounts WHERE title = '纯币260M/6格/6体/6负/皮肤多'
UNION ALL UNION ALL
SELECT id, owner_id, 35.71, 857.14, 6000.00, 160.00, 24, 168, 'published', 'approved', NOW() - INTERVAL 10 MINUTE, created_at, updated_at SELECT id, owner_id, 35.71, 857.14, 6000.00, 160.00, 'published', 'approved', NOW() - INTERVAL 10 MINUTE, created_at, updated_at
FROM game_accounts WHERE title = '纯币360M/9格/7体/6负/赠送六级弹'; FROM game_accounts WHERE title = '纯币360M/9格/7体/6负/赠送六级弹';
INSERT INTO notifications (user_id, type, title, content, biz_type, biz_id, created_at) INSERT INTO notifications (user_id, type, title, content, biz_type, biz_id, created_at)
-20
View File
@@ -52,16 +52,10 @@ export interface PublishCoinCorrection {
correction: number correction: number
} }
export interface PublishRentRule {
threshold_m: number
days: number
}
export interface PublishRatioConfig { export interface PublishRatioConfig {
insurance_base_ratios: PublishInsuranceBaseRatio[] insurance_base_ratios: PublishInsuranceBaseRatio[]
config_items: PublishRatioConfigItem[] config_items: PublishRatioConfigItem[]
coin_corrections: PublishCoinCorrection[] coin_corrections: PublishCoinCorrection[]
rent_rules: PublishRentRule[]
} }
export interface ListingPublishOptions { export interface ListingPublishOptions {
@@ -109,7 +103,6 @@ export const emptyListingPublishOptions: ListingPublishOptions = {
insurance_base_ratios: [], insurance_base_ratios: [],
config_items: [], config_items: [],
coin_corrections: [], coin_corrections: [],
rent_rules: [],
}, },
} }
@@ -208,7 +201,6 @@ function normalizeRatioConfig(value?: unknown): PublishRatioConfig {
coin_corrections: normalizeCoinCorrections( coin_corrections: normalizeCoinCorrections(
Array.isArray(row.coin_corrections) ? row.coin_corrections : [], Array.isArray(row.coin_corrections) ? row.coin_corrections : [],
), ),
rent_rules: normalizeRentRules(Array.isArray(row.rent_rules) ? row.rent_rules : []),
} }
} }
@@ -251,18 +243,6 @@ function normalizeCoinCorrections(values: unknown[]): PublishCoinCorrection[] {
.filter((item) => item.threshold_m >= 0 && item.correction > 0) .filter((item) => item.threshold_m >= 0 && item.correction > 0)
} }
function normalizeRentRules(values: unknown[]): PublishRentRule[] {
return values
.map((item) => {
const row = isRecord(item) ? item : {}
return {
threshold_m: readNumber(row.threshold_m),
days: Math.trunc(readNumber(row.days)),
}
})
.filter((item) => item.threshold_m >= 0 && item.days > 0)
}
function readNumber(value: unknown) { function readNumber(value: unknown) {
const parsed = Number(value) const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0 return Number.isFinite(parsed) ? parsed : 0
-4
View File
@@ -20,8 +20,6 @@ export interface Listing {
price_daily: number price_daily: number
price_weekly: number price_weekly: number
deposit_amount: number deposit_amount: number
min_rent_hours: number
max_rent_hours: number
status: string status: string
review_status: string review_status: string
review_reason: string review_reason: string
@@ -43,8 +41,6 @@ export interface ListingPayload {
price_daily: number price_daily: number
price_weekly: number price_weekly: number
deposit_amount: number deposit_amount: number
min_rent_hours: number
max_rent_hours: number
} }
interface ApiResponse<T> { interface ApiResponse<T> {
+1 -3
View File
@@ -14,7 +14,6 @@ export interface Order {
login_platform: string login_platform: string
rent_start_at?: string rent_start_at?: string
rent_end_at?: string rent_end_at?: string
rent_hours: number
rent_amount: number rent_amount: number
deposit_amount: number deposit_amount: number
platform_fee: number platform_fee: number
@@ -44,10 +43,9 @@ interface ApiResponse<T> {
data: T data: T
} }
export async function createOrder(listingId: number, rentHours: number) { export async function createOrder(listingId: number) {
const { data } = await apiClient.post<ApiResponse<Order>>('/orders', { const { data } = await apiClient.post<ApiResponse<Order>>('/orders', {
listing_id: listingId, listing_id: listingId,
rent_hours: rentHours,
}) })
return data.data return data.data
} }
@@ -3,7 +3,7 @@
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Create Order</p> <p class="eyebrow">Create Order</p>
<h1>创建订单</h1> <h1>创建订单</h1>
<p>确认租期租金押金和账号资产快照</p> <p>确认价格押金和账号资产快照</p>
</div> </div>
</section> </section>
</template> </template>
@@ -91,7 +91,7 @@ async function handleConfirmReceive() {
confirming.value = true confirming.value = true
try { try {
await confirmReceive(order.value.id) await confirmReceive(order.value.id)
ElMessage.success('已确认收号,订单进入租赁中') ElMessage.success('已确认收号,订单进入使用中')
await loadOrder() await loadOrder()
} catch (error) { } catch (error) {
ElMessage.error(readError(error, '确认收号失败')) ElMessage.error(readError(error, '确认收号失败'))
@@ -197,7 +197,7 @@ function readError(error: unknown, fallback: string) {
<strong>{{ order.handoff_status }}</strong> <strong>{{ order.handoff_status }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>租金</span> <span>订单金额</span>
<strong>¥{{ order.rent_amount }}</strong> <strong>¥{{ order.rent_amount }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
@@ -207,9 +207,8 @@ function readError(error: unknown, fallback: string) {
</div> </div>
<div v-if="order" class="order-panel"> <div v-if="order" class="order-panel">
<p>租期{{ order.rent_hours }} 小时</p>
<p>开始{{ formatDateTime(order.rent_start_at, '未开始') }}</p> <p>开始{{ formatDateTime(order.rent_start_at, '未开始') }}</p>
<p>结束{{ formatDateTime(order.rent_end_at, '未设置') }}</p> <p>预计截止{{ formatDateTime(order.rent_end_at, '未设置') }}</p>
<el-button v-if="order.status === 'pending_handoff'" type="danger" :loading="cancelling" @click="handleCancel"> <el-button v-if="order.status === 'pending_handoff'" type="danger" :loading="cancelling" @click="handleCancel">
取消订单并释放账号 取消订单并释放账号
</el-button> </el-button>
@@ -236,7 +235,7 @@ function readError(error: unknown, fallback: string) {
class="order-panel" class="order-panel"
> >
<h2>确认收号</h2> <h2>确认收号</h2>
<p>确认账号可以正常登录后订单会进入租赁中并重新计算租期结束时间</p> <p>确认账号可以正常登录后订单会进入使用中并重新计算预计截止时间</p>
<el-button type="primary" :loading="confirming" @click="handleConfirmReceive">确认已收到账号</el-button> <el-button type="primary" :loading="confirming" @click="handleConfirmReceive">确认已收到账号</el-button>
</div> </div>
+2 -3
View File
@@ -23,14 +23,13 @@ async function loadOrders() {
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Orders</p> <p class="eyebrow">Orders</p>
<h1>我的订单</h1> <h1>我的订单</h1>
<p>跟踪待交接租赁待归还申诉中和已完成订单</p> <p>跟踪待交接使用待归还申诉中和已完成订单</p>
</div> </div>
<el-table v-loading="loading" class="table-panel" :data="orders"> <el-table v-loading="loading" class="table-panel" :data="orders">
<el-table-column prop="order_no" label="订单号" min-width="210" /> <el-table-column prop="order_no" label="订单号" min-width="210" />
<el-table-column prop="title" label="账号" min-width="180" /> <el-table-column prop="title" label="账号" min-width="180" />
<el-table-column prop="rent_hours" label="租期" width="90" /> <el-table-column prop="rent_amount" label="订单金额" width="100" />
<el-table-column prop="rent_amount" label="租金" width="100" />
<el-table-column prop="deposit_amount" label="押金" width="100" /> <el-table-column prop="deposit_amount" label="押金" width="100" />
<el-table-column prop="status" label="状态" width="140" /> <el-table-column prop="status" label="状态" width="140" />
<el-table-column label="操作" width="100"> <el-table-column label="操作" width="100">
@@ -56,7 +56,7 @@ function money(value?: number) {
<strong>{{ dashboard.metrics.total_orders }}</strong> <strong>{{ dashboard.metrics.total_orders }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>租赁</span> <span>使用</span>
<strong>{{ dashboard.metrics.renting_orders }}</strong> <strong>{{ dashboard.metrics.renting_orders }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
@@ -119,7 +119,7 @@ function money(value?: number) {
<el-table-column prop="order_no" label="最近订单" min-width="210" /> <el-table-column prop="order_no" label="最近订单" min-width="210" />
<el-table-column prop="title" label="账号" min-width="170" /> <el-table-column prop="title" label="账号" min-width="170" />
<el-table-column prop="status" label="状态" width="140" /> <el-table-column prop="status" label="状态" width="140" />
<el-table-column prop="rent_amount" label="租金" width="100" /> <el-table-column prop="rent_amount" label="订单金额" width="100" />
<el-table-column prop="deposit_amount" label="押金" width="100" /> <el-table-column prop="deposit_amount" label="押金" width="100" />
<el-table-column label="创建时间" min-width="180"> <el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template> <template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
@@ -56,6 +56,10 @@ function money(value: number) {
return `¥${Number(value || 0).toFixed(2)}` return `¥${Number(value || 0).toFixed(2)}`
} }
function listingPrice(row: Listing) {
return money(row.price_daily || row.price_hourly)
}
function extractObjectKey(url: string) { function extractObjectKey(url: string) {
try { try {
const parsed = new URL(url, window.location.origin) const parsed = new URL(url, window.location.origin)
@@ -110,8 +114,8 @@ function readError(error: unknown, fallback: string) {
<strong>{{ listing.review_status }}</strong> <strong>{{ listing.review_status }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>时租</span> <span>价格</span>
<strong>{{ money(listing.price_hourly) }}</strong> <strong>{{ listingPrice(listing) }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>押金</span> <span>押金</span>
@@ -132,13 +136,11 @@ function readError(error: unknown, fallback: string) {
</div> </div>
<div class="order-panel dashboard-panel"> <div class="order-panel dashboard-panel">
<h2>号主与租期</h2> <h2>号主与价格</h2>
<p>号主{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p> <p>号主{{ listing.owner_phone || listing.owner_nickname || listing.owner_id }}</p>
<p>号主 ID{{ listing.owner_id }}</p> <p>号主 ID{{ listing.owner_id }}</p>
<p>最短租期{{ listing.min_rent_hours }} 小时</p> <p>价格{{ listingPrice(listing) }}</p>
<p>最长租期{{ listing.max_rent_hours }} 小时</p> <p>押金{{ money(listing.deposit_amount) }}</p>
<p>日租{{ money(listing.price_daily) }}</p>
<p>周租{{ money(listing.price_weekly) }}</p>
</div> </div>
</div> </div>
@@ -62,6 +62,10 @@ function openEvidence(row: Listing) {
evidenceListing.value = row evidenceListing.value = row
} }
function listingPrice(row: Listing) {
return `¥${Number(row.price_daily || row.price_hourly || 0).toFixed(2)}`
}
function extractObjectKey(url: string) { function extractObjectKey(url: string) {
try { try {
const parsed = new URL(url, window.location.origin) const parsed = new URL(url, window.location.origin)
@@ -95,7 +99,7 @@ function readError(error: unknown, fallback: string) {
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Review</p> <p class="eyebrow">Review</p>
<h1>商品审核</h1> <h1>商品审核</h1>
<p>审核号主提交的账号资产价格押金和租期规则</p> <p>审核号主提交的账号资产价格和押金</p>
</div> </div>
<el-button @click="loadListings">刷新</el-button> <el-button @click="loadListings">刷新</el-button>
</div> </div>
@@ -107,7 +111,9 @@ function readError(error: unknown, fallback: string) {
<el-table-column prop="login_platform" label="平台" width="120" /> <el-table-column prop="login_platform" label="平台" width="120" />
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" /> <el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
<el-table-column prop="rank_level" label="段位" width="110" /> <el-table-column prop="rank_level" label="段位" width="110" />
<el-table-column prop="price_hourly" label="时租" width="90" /> <el-table-column label="价格" width="90">
<template #default="{ row }">{{ listingPrice(row) }}</template>
</el-table-column>
<el-table-column prop="deposit_amount" label="押金" width="90" /> <el-table-column prop="deposit_amount" label="押金" width="90" />
<el-table-column label="截图" width="90"> <el-table-column label="截图" width="90">
<template #default="{ row }"> <template #default="{ row }">
@@ -41,6 +41,10 @@ function money(value: number) {
return `¥${Number(value || 0).toFixed(2)}` return `¥${Number(value || 0).toFixed(2)}`
} }
function listingPrice(row: Listing) {
return money(row.price_daily || row.price_hourly)
}
function ownerName(row: Listing) { function ownerName(row: Listing) {
return row.owner_phone || row.owner_nickname || `号主 ${row.owner_id}` return row.owner_phone || row.owner_nickname || `号主 ${row.owner_id}`
} }
@@ -84,7 +88,7 @@ function reviewType(status: string) {
<strong>{{ publishedCount }} </strong> <strong>{{ publishedCount }} </strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>租赁中</span> <span>已锁定</span>
<strong>{{ rentedCount }} </strong> <strong>{{ rentedCount }} </strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
@@ -101,7 +105,7 @@ function reviewType(status: string) {
<el-select v-model="filters.status" clearable placeholder="全部状态" class="full-control"> <el-select v-model="filters.status" clearable placeholder="全部状态" class="full-control">
<el-option label="草稿" value="draft" /> <el-option label="草稿" value="draft" />
<el-option label="已上架" value="published" /> <el-option label="已上架" value="published" />
<el-option label="租赁中" value="rented" /> <el-option label="已锁定" value="rented" />
<el-option label="已下架" value="offline" /> <el-option label="已下架" value="offline" />
<el-option label="异常" value="abnormal" /> <el-option label="异常" value="abnormal" />
</el-select> </el-select>
@@ -137,8 +141,8 @@ function reviewType(status: string) {
<el-table-column prop="login_platform" label="平台" width="120" /> <el-table-column prop="login_platform" label="平台" width="120" />
<el-table-column prop="rank_level" label="段位" width="110" /> <el-table-column prop="rank_level" label="段位" width="110" />
<el-table-column prop="haf_coin_amount" label="哈夫币" width="110" /> <el-table-column prop="haf_coin_amount" label="哈夫币" width="110" />
<el-table-column label="时租" width="100"> <el-table-column label="价格" width="100">
<template #default="{ row }">{{ money(row.price_hourly) }}</template> <template #default="{ row }">{{ listingPrice(row) }}</template>
</el-table-column> </el-table-column>
<el-table-column label="押金" width="100"> <el-table-column label="押金" width="100">
<template #default="{ row }">{{ money(row.deposit_amount) }}</template> <template #default="{ row }">{{ money(row.deposit_amount) }}</template>
@@ -91,7 +91,7 @@ function readError(error: unknown, fallback: string) {
<strong>{{ order.handoff_status }}</strong> <strong>{{ order.handoff_status }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>租金</span> <span>订单金额</span>
<strong>¥{{ order.rent_amount }}</strong> <strong>¥{{ order.rent_amount }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
@@ -105,9 +105,8 @@ function readError(error: unknown, fallback: string) {
<h2>用户信息</h2> <h2>用户信息</h2>
<p>租客{{ order.renter_phone || order.renter_id }}</p> <p>租客{{ order.renter_phone || order.renter_id }}</p>
<p>号主{{ order.owner_phone || order.owner_id }}</p> <p>号主{{ order.owner_phone || order.owner_id }}</p>
<p>租期{{ order.rent_hours }} 小时</p>
<p>开始{{ formatDateTime(order.rent_start_at, '未开始') }}</p> <p>开始{{ formatDateTime(order.rent_start_at, '未开始') }}</p>
<p>结束{{ formatDateTime(order.rent_end_at, '未设置') }}</p> <p>预计截止{{ formatDateTime(order.rent_end_at, '未设置') }}</p>
</div> </div>
<div class="order-panel dashboard-panel"> <div class="order-panel dashboard-panel">
+2 -2
View File
@@ -36,7 +36,7 @@ async function loadOrders() {
<div class="toolbar-actions"> <div class="toolbar-actions">
<el-select v-model="status" clearable placeholder="订单状态" style="width: 180px"> <el-select v-model="status" clearable placeholder="订单状态" style="width: 180px">
<el-option label="待交接" value="pending_handoff" /> <el-option label="待交接" value="pending_handoff" />
<el-option label="租赁中" value="renting" /> <el-option label="使用中" value="renting" />
<el-option label="逾期中" value="overdue" /> <el-option label="逾期中" value="overdue" />
<el-option label="待归还确认" value="pending_return_confirm" /> <el-option label="待归还确认" value="pending_return_confirm" />
<el-option label="申诉中" value="disputing" /> <el-option label="申诉中" value="disputing" />
@@ -57,7 +57,7 @@ async function loadOrders() {
<el-table-column prop="status" label="状态" width="140" /> <el-table-column prop="status" label="状态" width="140" />
<el-table-column prop="handoff_status" label="交接" width="180" /> <el-table-column prop="handoff_status" label="交接" width="180" />
<el-table-column prop="settlement_status" label="结算" width="120" /> <el-table-column prop="settlement_status" label="结算" width="120" />
<el-table-column prop="rent_amount" label="租金" width="100" /> <el-table-column prop="rent_amount" label="订单金额" width="100" />
<el-table-column prop="deposit_amount" label="押金" width="100" /> <el-table-column prop="deposit_amount" label="押金" width="100" />
<el-table-column label="创建时间" min-width="180"> <el-table-column label="创建时间" min-width="180">
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template> <template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
@@ -281,17 +281,6 @@ function removeCoinCorrection(index: number) {
publishOptionsDraft.value.ratio_config.coin_corrections.splice(index, 1) publishOptionsDraft.value.ratio_config.coin_corrections.splice(index, 1)
} }
function addRentRule() {
publishOptionsDraft.value.ratio_config.rent_rules.push({
threshold_m: 0,
days: 1,
})
}
function removeRentRule(index: number) {
publishOptionsDraft.value.ratio_config.rent_rules.splice(index, 1)
}
function addHomeBanner() { function addHomeBanner() {
homeBannersDraft.value.push({ homeBannersDraft.value.push({
eyebrow: '首页推荐', eyebrow: '首页推荐',
@@ -642,23 +631,6 @@ function readError(error: unknown, fallback: string) {
</el-table-column> </el-table-column>
</el-table> </el-table>
<div class="editor-block-title subtle-title">
<span>租期规则</span>
<el-button size="small" @click="addRentRule">添加租期</el-button>
</div>
<el-table :data="publishOptionsDraft.ratio_config.rent_rules" size="small" border>
<el-table-column label="大于等于 M" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.threshold_m" :min="0" :step="10" /></template>
</el-table-column>
<el-table-column label="租期天数" min-width="130">
<template #default="{ row }"><el-input-number v-model="row.days" :min="1" :step="1" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }">
<el-button size="small" type="danger" plain @click="removeRentRule($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div> </div>
<div class="editor-block"> <div class="editor-block">
@@ -62,7 +62,7 @@ function directionLabel(direction: string) {
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Wallet Ledger</p> <p class="eyebrow">Wallet Ledger</p>
<h1>资金流水</h1> <h1>资金流水</h1>
<p>查看租金押金冻结解冻退款和结算流水当前为开发态账务记录不代表真实支付余额</p> <p>查看订单金额押金冻结解冻退款和结算流水当前为开发态账务记录不代表真实支付余额</p>
</div> </div>
<div class="toolbar-actions"> <div class="toolbar-actions">
<el-button @click="resetFilters">重置</el-button> <el-button @click="resetFilters">重置</el-button>
@@ -18,7 +18,6 @@ import {
getListingSubtitle, getListingSubtitle,
getListingTitle, getListingTitle,
getLoginMethod, getLoginMethod,
getRentDays,
getServerRegion, getServerRegion,
readAssetNumber, readAssetNumber,
readAssetString, readAssetString,
@@ -29,14 +28,12 @@ const router = useRouter();
const session = useSessionStore(); const session = useSessionStore();
const loading = ref(false); const loading = ref(false);
const ordering = ref(false); const ordering = ref(false);
const rentHours = ref(1);
const listing = ref<Listing | null>(null); const listing = ref<Listing | null>(null);
onMounted(async () => { onMounted(async () => {
loading.value = true; loading.value = true;
try { try {
listing.value = await fetchListing(String(route.params.id)); listing.value = await fetchListing(String(route.params.id));
rentHours.value = Math.max(listing.value.min_rent_hours || 1, 1);
} catch { } catch {
showToast({ message: "加载失败", icon: "warning-o" }); showToast({ message: "加载失败", icon: "warning-o" });
} finally { } finally {
@@ -44,10 +41,9 @@ onMounted(async () => {
} }
}); });
/* 预计租金 */ const orderTotal = computed(() => {
const totalRent = computed(() => {
if (!listing.value) return "0.00"; if (!listing.value) return "0.00";
return (listing.value.price_hourly * rentHours.value).toFixed(2); return getListingDisplayPrice(listing.value).toFixed(2);
}); });
const detailMetrics = computed(() => { const detailMetrics = computed(() => {
@@ -136,7 +132,7 @@ async function handleCreateOrder() {
ordering.value = true; ordering.value = true;
try { try {
const order = await createOrder(listing.value.id, rentHours.value); await createOrder(listing.value.id);
showToast({ message: "订单已创建,账号已锁定", icon: "passed" }); showToast({ message: "订单已创建,账号已锁定", icon: "passed" });
await router.push(`/m/orders`); await router.push(`/m/orders`);
} catch (error) { } catch (error) {
@@ -255,10 +251,6 @@ function isNavActive(path: string) {
<span class="info-label">M单价</span> <span class="info-label">M单价</span>
<span class="info-text">{{ formatRatio(listing) }}</span> <span class="info-text">{{ formatRatio(listing) }}</span>
</div> </div>
<div class="info-line">
<span class="info-label">租期</span>
<span class="info-text">{{ getRentDays(listing) }}</span>
</div>
<div class="info-line"> <div class="info-line">
<span class="info-label">常用登录地</span> <span class="info-label">常用登录地</span>
<span class="info-text">{{ assetRegions(listing).join("、") || "--" }}</span> <span class="info-text">{{ assetRegions(listing).join("、") || "--" }}</span>
@@ -316,26 +308,9 @@ function isNavActive(path: string) {
<!-- 底部下单栏固定 --> <!-- 底部下单栏固定 -->
<div class="order-bar"> <div class="order-bar">
<div class="order-bar-left"> <div class="order-bar-left">
<div class="rent-control">
<button
class="rent-btn"
:disabled="rentHours <= listing.min_rent_hours"
@click="rentHours--"
>
</button>
<span class="rent-value">{{ rentHours }}小时</span>
<button
class="rent-btn"
:disabled="rentHours >= listing.max_rent_hours"
@click="rentHours++"
>
+
</button>
</div>
<div class="order-price"> <div class="order-price">
<span class="price-label">租金</span> <span class="price-label">价格</span>
<span class="price-amount">¥{{ totalRent }}</span> <span class="price-amount">¥{{ orderTotal }}</span>
</div> </div>
</div> </div>
<van-button <van-button
@@ -528,7 +503,7 @@ function isNavActive(path: string) {
color: #ff5f00; color: #ff5f00;
} }
/* ========== 租期信息 ========== */ /* ========== 账号资料 ========== */
.info-line { .info-line {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -676,38 +651,6 @@ function isNavActive(path: string) {
gap: 2px; gap: 2px;
} }
.rent-control {
display: flex;
align-items: center;
gap: 6px;
}
.rent-btn {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 1px solid #ddd;
border-radius: 6px;
background: #f5f7fa;
color: #333;
font-size: 16px;
font-weight: 700;
cursor: pointer;
}
.rent-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.rent-value {
font-size: 14px;
font-weight: 700;
min-width: 48px;
text-align: center;
}
.order-price { .order-price {
display: flex; display: flex;
align-items: baseline; align-items: baseline;
@@ -35,7 +35,7 @@ async function loadOrders() {
const statusTabs = [ const statusTabs = [
{ key: "all", label: "全部" }, { key: "all", label: "全部" },
{ key: "pending_handoff", label: "待交接" }, { key: "pending_handoff", label: "待交接" },
{ key: "renting", label: "租赁中" }, { key: "renting", label: "使用中" },
{ key: "pending_return_confirm", label: "待归还" }, { key: "pending_return_confirm", label: "待归还" },
{ key: "completed", label: "已完成" }, { key: "completed", label: "已完成" },
]; ];
@@ -64,7 +64,7 @@ function statusColor(status: string) {
function statusLabel(status: string) { function statusLabel(status: string) {
const map: Record<string, string> = { const map: Record<string, string> = {
pending_handoff: "待交接", pending_handoff: "待交接",
renting: "租赁中", renting: "使用中",
overdue: "已逾期", overdue: "已逾期",
pending_return_confirm: "待归还", pending_return_confirm: "待归还",
completed: "已完成", completed: "已完成",
@@ -138,11 +138,7 @@ function goDetail(id: number) {
<h3 class="order-title">{{ order.title }}</h3> <h3 class="order-title">{{ order.title }}</h3>
<div class="info-grid"> <div class="info-grid">
<div class="info-item"> <div class="info-item">
<span class="info-label">租期</span> <span class="info-label">订单金额</span>
<span class="info-value">{{ order.rent_hours }}小时</span>
</div>
<div class="info-item">
<span class="info-label">租金</span>
<span class="info-value price">¥{{ order.rent_amount }}</span> <span class="info-value price">¥{{ order.rent_amount }}</span>
</div> </div>
<div class="info-item"> <div class="info-item">
@@ -133,9 +133,6 @@ const calculatedFinalPrice = computed(() =>
const calculatedRatioText = computed(() => const calculatedRatioText = computed(() =>
calculatedRatio.value > 0 ? `1:${formatNumber(calculatedRatio.value)}` : "" calculatedRatio.value > 0 ? `1:${formatNumber(calculatedRatio.value)}` : ""
); );
const calculatedRentDays = computed(() =>
coinMAmount.value > 0 ? calculateRentDays(coinMAmount.value) : 0
);
onMounted(() => { onMounted(() => {
restoreDraft(); restoreDraft();
@@ -356,8 +353,7 @@ async function handleSubmit() {
loading.value = true; loading.value = true;
try { try {
const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}M哈夫币`; const title = `${form.server_region} ${form.rank_level} ${form.haf_coin_amount}M哈夫币`;
const rentDays = Math.max(calculatedRentDays.value, 1); const dailyPrice = calculatedFinalPrice.value;
const dailyPrice = roundMoney(calculatedFinalPrice.value / rentDays);
const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01); const hourlyPrice = Math.max(roundMoney(dailyPrice / 24), 0.01);
await createListing({ await createListing({
@@ -373,8 +369,6 @@ async function handleSubmit() {
price_daily: dailyPrice, price_daily: dailyPrice,
price_weekly: roundMoney(dailyPrice * 7), price_weekly: roundMoney(dailyPrice * 7),
deposit_amount: Number(form.deposit_amount), deposit_amount: Number(form.deposit_amount),
min_rent_hours: 24,
max_rent_hours: rentDays * 24,
}); });
localStorage.removeItem(draftKey); localStorage.removeItem(draftKey);
showToast({ message: "发布成功", icon: "passed" }); showToast({ message: "发布成功", icon: "passed" });
@@ -402,7 +396,7 @@ function validateForm() {
if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) { if (form.deposit_amount === "" || Number(form.deposit_amount) < 0) {
return "请填写押金"; return "请填写押金";
} }
if (!calculatedFinalPrice.value || !calculatedRentDays.value) { if (!calculatedFinalPrice.value) {
return "请完善币数、保险、体力和负重后再发布"; return "请完善币数、保险、体力和负重后再发布";
} }
for (const item of screenshotSlots.value) { for (const item of screenshotSlots.value) {
@@ -426,6 +420,7 @@ function buildAssetSummary() {
face_owner: form.face_owner, face_owner: form.face_owner,
secret_kd: form.secret_kd, secret_kd: form.secret_kd,
fire_level: Number(form.fire_level), fire_level: Number(form.fire_level),
publish_ratio: calculatedRatio.value,
season_insurance: form.season_insurance, season_insurance: form.season_insurance,
stamina_level: form.stamina_level, stamina_level: form.stamina_level,
load_level: form.load_level, load_level: form.load_level,
@@ -535,12 +530,6 @@ function getCoinCorrection(coinM: number) {
.find((item) => coinM > item.threshold_m)?.correction || 0; .find((item) => coinM > item.threshold_m)?.correction || 0;
} }
function calculateRentDays(coinM: number) {
return [...ratioConfig.value.rent_rules]
.sort((a, b) => b.threshold_m - a.threshold_m)
.find((item) => coinM >= item.threshold_m)?.days || 1;
}
function formatNumber(value: number) { function formatNumber(value: number) {
return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`; return Number.isInteger(value) ? `${value}` : `${Math.round(value * 10) / 10}`;
} }
@@ -795,7 +784,7 @@ function readError(error: unknown, fallback: string) {
/> />
</div> </div>
<p class="field-hint"> <p class="field-hint">
此在线时间指的是百分百能够联系上您的时间若是在此期间联系不上您导致无法上号会扣除您的部分租金或上架押金在线时长太短可能无法上架请预留充足时间用于扫码以及冻结人脸请谨慎填写 此在线时间指的是百分百能够联系上您的时间若是在此期间联系不上您导致无法上号会扣除您的部分订单金额或上架押金在线时长太短可能无法上架请预留充足时间用于扫码以及冻结人脸请谨慎填写
</p> </p>
<van-field label="封禁记录" required class="publish-field"> <van-field label="封禁记录" required class="publish-field">
+7 -23
View File
@@ -27,38 +27,24 @@ export function formatHafCoinM(amountWan: number) {
} }
export function getListingDisplayPrice(item: Listing) { export function getListingDisplayPrice(item: Listing) {
const rentDays = getRentDays(item);
if (rentDays > 0 && item.price_daily > 0) {
return roundMoney(item.price_daily * rentDays);
}
return Number(item.price_daily || item.price_hourly || 0); return Number(item.price_daily || item.price_hourly || 0);
} }
export function getRentDays(item: Listing) {
if (item.max_rent_hours >= 24) return Math.max(Math.round(item.max_rent_hours / 24), 1);
return 1;
}
export function getRatioValue(item: Listing) { export function getRatioValue(item: Listing) {
const ratio = readAssetNumber(item, "publish_ratio");
if (ratio > 0) return ratio;
const price = getListingDisplayPrice(item); const price = getListingDisplayPrice(item);
if (price <= 0) return 0; if (price <= 0) return 0;
return getCoinWan(item) / price; return getCoinWan(item) / price;
} }
export function formatRatio(item: Listing) { export function formatRatio(item: Listing) {
const pricePerM = getPricePerM(item); const ratio = getRatioValue(item);
return pricePerM > 0 ? `1M=¥${formatMoney(pricePerM)}` : "--"; return ratio > 0 ? `1:${formatRatioNumber(ratio)}` : "--";
} }
export function getValuePerYuanText(item: Listing) { export function getValuePerYuanText(item: Listing) {
const pricePerM = getPricePerM(item); return formatRatio(item);
return pricePerM > 0 ? `1M=¥${formatMoney(pricePerM)}` : "";
}
export function getPricePerM(item: Listing) {
const coinM = getCoinM(item);
if (coinM <= 0) return 0;
return roundMoney(getListingDisplayPrice(item) / coinM);
} }
export function getLoginMethod(item: Listing) { export function getLoginMethod(item: Listing) {
@@ -83,9 +69,7 @@ export function getListingTitle(item: Listing) {
} }
export function getListingSubtitle(item: Listing) { export function getListingSubtitle(item: Listing) {
return [getValuePerYuanText(item), `租期${getRentDays(item)}`] return getValuePerYuanText(item);
.filter(Boolean)
.join(" ");
} }
export function getListingChips(item: Listing): ListingDisplayChip[] { export function getListingChips(item: Listing): ListingDisplayChip[] {
@@ -225,7 +209,7 @@ function roundMoney(value: number) {
return Math.round(value * 100) / 100; return Math.round(value * 100) / 100;
} }
function formatMoney(value: number) { function formatRatioNumber(value: number) {
const rounded = roundMoney(value); const rounded = roundMoney(value);
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2); return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2);
} }
+11 -27
View File
@@ -10,7 +10,6 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const ordering = ref(false); const ordering = ref(false);
const rentHours = ref(1);
const listing = ref<Listing | null>(null); const listing = ref<Listing | null>(null);
onMounted(async () => { onMounted(async () => {
@@ -26,7 +25,7 @@ async function handleCreateOrder() {
if (!listing.value) return; if (!listing.value) return;
ordering.value = true; ordering.value = true;
try { try {
const order = await createOrder(listing.value.id, rentHours.value); const order = await createOrder(listing.value.id);
ElMessage.success("订单已创建,账号已锁定"); ElMessage.success("订单已创建,账号已锁定");
await router.push(`/orders/${order.id}`); await router.push(`/orders/${order.id}`);
} catch (error) { } catch (error) {
@@ -44,6 +43,10 @@ function readError(error: unknown, fallback: string) {
} }
return fallback; return fallback;
} }
function listingPrice(item: Listing) {
return Number(item.price_daily || item.price_hourly || 0).toFixed(2);
}
</script> </script>
<template> <template>
@@ -78,12 +81,8 @@ function readError(error: unknown, fallback: string) {
<strong>{{ listing.haf_coin_amount }}</strong> <strong>{{ listing.haf_coin_amount }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>时租</span> <span>价格</span>
<strong>¥{{ listing.price_hourly }}</strong> <strong>¥{{ listingPrice(listing) }}</strong>
</div>
<div class="metric-card">
<span>日租</span>
<strong>¥{{ listing.price_daily }}</strong>
</div> </div>
<div class="metric-card"> <div class="metric-card">
<span>押金</span> <span>押金</span>
@@ -93,27 +92,12 @@ function readError(error: unknown, fallback: string) {
</section> </section>
<aside class="order-panel pc-order-card"> <aside class="order-panel pc-order-card">
<h2>立即租用</h2> <h2>立即下单</h2>
<p class="order-safe-text"> <p class="order-safe-text">平台托管订单与押金按平台交接流程完成账号使用</p>
平台托管订单与押金租期 {{ listing.min_rent_hours }}-{{
listing.max_rent_hours
}}
小时
</p>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="租用时长">
<el-input-number
v-model="rentHours"
:min="listing.min_rent_hours"
:max="listing.max_rent_hours"
class="full-control"
/>
</el-form-item>
<div class="order-total-box"> <div class="order-total-box">
<span>预计租金</span> <span>价格</span>
<strong <strong>¥{{ listingPrice(listing) }}</strong>
>¥{{ (listing.price_hourly * rentHours).toFixed(2) }}</strong
>
<em>押金 ¥{{ listing.deposit_amount }}</em> <em>押金 ¥{{ listing.deposit_amount }}</em>
</div> </div>
<el-button <el-button
+10 -9
View File
@@ -36,20 +36,20 @@ const filteredListings = computed(() => {
const matchCoin = const matchCoin =
!filters.minCoin || item.haf_coin_amount >= filters.minCoin; !filters.minCoin || item.haf_coin_amount >= filters.minCoin;
const matchPrice = const matchPrice =
!filters.maxPrice || item.price_hourly <= filters.maxPrice; !filters.maxPrice || listingPrice(item) <= filters.maxPrice;
return ( return (
matchKeyword && matchRegion && matchPlatform && matchCoin && matchPrice matchKeyword && matchRegion && matchPlatform && matchCoin && matchPrice
); );
}); });
return [...result].sort((a, b) => { return [...result].sort((a, b) => {
if (sortBy.value === "price-asc") return a.price_hourly - b.price_hourly; if (sortBy.value === "price-asc") return listingPrice(a) - listingPrice(b);
if (sortBy.value === "coin-desc") if (sortBy.value === "coin-desc")
return b.haf_coin_amount - a.haf_coin_amount; return b.haf_coin_amount - a.haf_coin_amount;
if (sortBy.value === "deposit-asc") if (sortBy.value === "deposit-asc")
return a.deposit_amount - b.deposit_amount; return a.deposit_amount - b.deposit_amount;
return ( return (
b.haf_coin_amount - a.haf_coin_amount || a.price_hourly - b.price_hourly b.haf_coin_amount - a.haf_coin_amount || listingPrice(a) - listingPrice(b)
); );
}); });
}); });
@@ -77,6 +77,10 @@ function resetFilters() {
function uniqueOptions(values: string[]) { function uniqueOptions(values: string[]) {
return [...new Set(values.filter(Boolean))]; return [...new Set(values.filter(Boolean))];
} }
function listingPrice(item: Listing) {
return Number(item.price_daily || item.price_hourly || 0);
}
</script> </script>
<template> <template>
@@ -145,7 +149,7 @@ function uniqueOptions(values: string[]) {
class="full-control" class="full-control"
/> />
</el-form-item> </el-form-item>
<el-form-item label="最高时租"> <el-form-item label="最高价格">
<el-input-number <el-input-number
v-model="filters.maxPrice" v-model="filters.maxPrice"
:min="0" :min="0"
@@ -203,12 +207,9 @@ function uniqueOptions(values: string[]) {
</div> </div>
</div> </div>
<div class="resource-price-box"> <div class="resource-price-box">
<span>时租</span> <span>价格</span>
<strong>¥{{ item.price_hourly }}</strong> <strong>¥{{ listingPrice(item).toFixed(2) }}</strong>
<em>押金 ¥{{ item.deposit_amount }}</em> <em>押金 ¥{{ item.deposit_amount }}</em>
<small
>{{ item.min_rent_hours }}-{{ item.max_rent_hours }} 小时</small
>
</div> </div>
</RouterLink> </RouterLink>
</div> </div>
@@ -3,7 +3,7 @@
<div class="page-header"> <div class="page-header">
<p class="eyebrow">Earnings</p> <p class="eyebrow">Earnings</p>
<h1>收益流水</h1> <h1>收益流水</h1>
<p>查看租金平台抽成结算和冻结金额</p> <p>查看订单金额平台抽成结算和冻结金额</p>
</div> </div>
</section> </section>
</template> </template>
@@ -21,14 +21,19 @@ const form = reactive({
price_daily: 50, price_daily: 50,
price_weekly: 300, price_weekly: 300,
deposit_amount: 100, deposit_amount: 100,
min_rent_hours: 1,
max_rent_hours: 24,
}) })
async function handleSubmit() { async function handleSubmit() {
loading.value = true loading.value = true
try { try {
await createListing({ ...form, screenshot_urls: screenshotUrls.value }) const price = Number(form.price_daily || 0)
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('发布已创建')
await router.push('/seller/listings') await router.push('/seller/listings')
} catch (error) { } catch (error) {
@@ -115,24 +120,12 @@ function readError(error: unknown, fallback: string) {
<el-form-item label="哈夫币数量"> <el-form-item label="哈夫币数量">
<el-input-number v-model="form.haf_coin_amount" :min="0" /> <el-input-number v-model="form.haf_coin_amount" :min="0" />
</el-form-item> </el-form-item>
<el-form-item label="时租"> <el-form-item label="价格">
<el-input-number v-model="form.price_hourly" :min="1" />
</el-form-item>
<el-form-item label="日租">
<el-input-number v-model="form.price_daily" :min="0" /> <el-input-number v-model="form.price_daily" :min="0" />
</el-form-item> </el-form-item>
<el-form-item label="周租">
<el-input-number v-model="form.price_weekly" :min="0" />
</el-form-item>
<el-form-item label="押金"> <el-form-item label="押金">
<el-input-number v-model="form.deposit_amount" :min="0" /> <el-input-number v-model="form.deposit_amount" :min="0" />
</el-form-item> </el-form-item>
<el-form-item label="最短租期(小时)">
<el-input-number v-model="form.min_rent_hours" :min="1" />
</el-form-item>
<el-form-item label="最长租期(小时)">
<el-input-number v-model="form.max_rent_hours" :min="form.min_rent_hours" />
</el-form-item>
</div> </div>
<el-button type="primary" :loading="loading" @click="handleSubmit">保存发布</el-button> <el-button type="primary" :loading="loading" @click="handleSubmit">保存发布</el-button>
</el-form> </el-form>
@@ -29,6 +29,10 @@ async function offline(id: number) {
ElMessage.success('已下架') ElMessage.success('已下架')
await loadListings() await loadListings()
} }
function listingPrice(row: Listing) {
return `¥${Number(row.price_daily || row.price_hourly || 0).toFixed(2)}`
}
</script> </script>
<template> <template>
@@ -48,7 +52,9 @@ async function offline(id: number) {
<el-table-column prop="title" label="标题" min-width="180" /> <el-table-column prop="title" label="标题" min-width="180" />
<el-table-column prop="server_region" label="区服" width="120" /> <el-table-column prop="server_region" label="区服" width="120" />
<el-table-column prop="haf_coin_amount" label="哈夫币" width="120" /> <el-table-column prop="haf_coin_amount" label="哈夫币" width="120" />
<el-table-column prop="price_hourly" label="时租" width="100" /> <el-table-column label="价格" width="100">
<template #default="{ row }">{{ listingPrice(row) }}</template>
</el-table-column>
<el-table-column prop="deposit_amount" label="押金" width="100" /> <el-table-column prop="deposit_amount" label="押金" width="100" />
<el-table-column prop="status" label="状态" width="110" /> <el-table-column prop="status" label="状态" width="110" />
<el-table-column prop="review_status" label="审核" width="110" /> <el-table-column prop="review_status" label="审核" width="110" />