金额使用整数, 不再扣押金, 价格每个人看到自己的-1

This commit is contained in:
yml2213
2026-05-27 00:22:26 +08:00
parent e270ce8bfc
commit 89f4dabfb3
27 changed files with 351 additions and 119 deletions
+12 -6
View File
@@ -3,6 +3,7 @@ package dispute
import (
"encoding/json"
"errors"
"math"
"time"
"hfb_sys/backend/internal/model"
@@ -302,10 +303,10 @@ type arbitrationSettlement struct {
}
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (arbitrationSettlement, error) {
total := order.RentAmount + order.DepositAmount
ownerRentAmount := order.OwnerRentAmount
total := roundMoney(order.RentAmount + order.DepositAmount)
ownerRentAmount := roundMoney(order.OwnerRentAmount)
if ownerRentAmount <= 0 || ownerRentAmount > order.RentAmount {
ownerRentAmount = order.RentAmount
ownerRentAmount = roundMoney(order.RentAmount)
}
settlement := arbitrationSettlement{}
orderID := order.ID
@@ -359,6 +360,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
case "full_refund":
addRenterRefund(total, "仲裁全额退款")
case "partial_refund":
req.Amount = roundMoney(req.Amount)
if req.Amount <= 0 || req.Amount > total {
return settlement, ErrInvalidDispute
}
@@ -368,7 +370,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主")
addRenterRefund(order.DepositAmount, "仲裁释放押金给租客")
case "deduct_deposit", "compensate_owner":
deductAmount := req.Amount
deductAmount := roundMoney(req.Amount)
if deductAmount <= 0 {
deductAmount = order.DepositAmount
}
@@ -390,9 +392,13 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (
func minMoney(a float64, b float64) float64 {
if a < b {
return a
return roundMoney(a)
}
return b
return roundMoney(b)
}
func roundMoney(value float64) float64 {
return math.Round(value)
}
func (r *Repository) baseQuery() *gorm.DB {
+21 -3
View File
@@ -61,11 +61,12 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo
return err
}
price := normalizedListingPrice(req)
depositAmount := roundMoney(req.DepositAmount)
listing := model.RentalListing{
AccountID: account.ID,
OwnerID: ownerID,
Price: price,
DepositAmount: req.DepositAmount,
DepositAmount: depositAmount,
Status: listingStatus,
ReviewStatus: reviewStatus,
PublishedAt: publishedAt,
@@ -109,7 +110,7 @@ func (r *Repository) Update(ownerID uint64, listingID uint64, req UpdateRequest,
price := normalizedListingPrice(req)
listing.Price = price
listing.DepositAmount = req.DepositAmount
listing.DepositAmount = roundMoney(req.DepositAmount)
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus
listing.ReviewStatus = reviewStatus
@@ -488,7 +489,15 @@ func rowsToDTO(rows []listingRow) []ListingDTO {
}
func normalizedListingPrice(req CreateRequest) float64 {
return req.Price
if req.AssetSummary != nil {
if breakdown, ok := req.AssetSummary["price_breakdown"].(map[string]any); ok {
buyerPrice := readSummaryNumber(breakdown["buyer_total_price"])
if buyerPrice > 0 {
return roundMoney(buyerPrice)
}
}
}
return roundMoney(req.Price)
}
func publicListings(items []ListingDTO) []ListingDTO {
@@ -524,6 +533,15 @@ func applySellerListingPrice(item *ListingDTO) {
if sellerPrice > 0 {
item.Price = sellerPrice
}
sellerRatio := readSummaryNumber(breakdown["seller_ratio"])
if sellerRatio > 0 {
item.AssetSummary["publish_ratio"] = sellerRatio
}
delete(breakdown, "buyer_coin_base_price")
delete(breakdown, "buyer_total_price")
delete(breakdown, "buyer_ratio")
delete(breakdown, "platform_markup_amount")
delete(breakdown, "platform_rule_type")
}
func (row listingRow) toDTO() ListingDTO {
+1 -1
View File
@@ -313,7 +313,7 @@ func readUnitPrice(priceText string) float64 {
}
func roundMoney(value float64) float64 {
return math.Round(value*100) / 100
return math.Round(value)
}
func readFireLevel(summary map[string]any) (int, bool) {
@@ -11,8 +11,8 @@ func TestConsumableValueOnlyCountsChargedResources(t *testing.T) {
},
})
if value != 1.5 {
t.Fatalf("expected 1.5, got %.2f", value)
if value != 2 {
t.Fatalf("expected 2, got %.2f", value)
}
}
@@ -21,7 +21,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
Title: "测试账号",
ServerRegion: "烽火地带",
Price: 100,
DepositAmount: 1.5,
DepositAmount: 2,
HafCoinAmount: 1000000,
ScreenshotURLS: []string{"https://example.com/a.png"},
AssetSummary: map[string]any{
@@ -35,7 +35,7 @@ func TestValidateRequestRequiresDepositAboveConsumables(t *testing.T) {
t.Fatalf("expected ErrDepositTooLow, got %v", err)
}
req.DepositAmount = 2.01
req.DepositAmount = 3
if err := validateRequest(req, publishRules{}); err != nil {
t.Fatalf("expected valid request, got %v", err)
}
+12 -8
View File
@@ -20,10 +20,12 @@ type OrderDTO struct {
LoginPlatform string `json:"login_platform"`
RentedAt *time.Time `json:"rented_at"`
EstimatedDurationHours int `json:"estimated_duration_hours"`
RentAmount float64 `json:"rent_amount"`
OwnerRentAmount float64 `json:"owner_rent_amount"`
PriceRole string `json:"price_role,omitempty"`
DisplayAmount float64 `json:"display_amount"`
RentAmount *float64 `json:"rent_amount,omitempty"`
OwnerRentAmount *float64 `json:"owner_rent_amount,omitempty"`
DepositAmount float64 `json:"deposit_amount"`
PlatformFee float64 `json:"platform_fee"`
PlatformFee *float64 `json:"platform_fee,omitempty"`
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
Status string `json:"status"`
HandoffStatus string `json:"handoff_status"`
@@ -88,16 +90,18 @@ type CheckoutDTO struct {
OrderID uint64 `json:"order_id"`
InitiatedBy uint64 `json:"initiated_by"`
Status string `json:"status"`
RentAmount float64 `json:"rent_amount"`
OwnerRentAmount float64 `json:"owner_rent_amount"`
PlatformFee float64 `json:"platform_fee"`
PriceRole string `json:"price_role,omitempty"`
DisplayAmount float64 `json:"display_amount"`
RentAmount *float64 `json:"rent_amount,omitempty"`
OwnerRentAmount *float64 `json:"owner_rent_amount,omitempty"`
PlatformFee *float64 `json:"platform_fee,omitempty"`
DepositAmount float64 `json:"deposit_amount"`
ConsumableAmount float64 `json:"consumable_amount"`
CoinConsumedM float64 `json:"coin_consumed_m"`
OtherAmount float64 `json:"other_amount"`
DepositDeductAmount float64 `json:"deposit_deduct_amount"`
RenterRefundAmount float64 `json:"renter_refund_amount"`
OwnerIncomeAmount float64 `json:"owner_income_amount"`
RenterRefundAmount *float64 `json:"renter_refund_amount,omitempty"`
OwnerIncomeAmount *float64 `json:"owner_income_amount,omitempty"`
Content string `json:"content"`
EvidenceURLS []string `json:"evidence_urls"`
OwnerAdjustmentReason string `json:"owner_adjustment_reason"`
+179 -23
View File
@@ -624,7 +624,12 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC
if err != nil {
return nil, err
}
dto := toCheckoutDTO(*checkout)
dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{
OwnerID: userID,
RentAmount: checkout.RentAmount,
OwnerRentAmount: checkout.OwnerRentAmount,
DepositAmount: checkout.DepositAmount,
})
return &dto, nil
}
@@ -665,7 +670,7 @@ func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) {
}
items := make([]OrderDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
items = append(items, row.toDTOForUser(userID))
}
return items, nil
}
@@ -681,7 +686,7 @@ func (r *Repository) ListAdmin() ([]OrderDTO, error) {
}
items := make([]OrderDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toDTO())
items = append(items, row.toAdminDTO())
}
return items, nil
}
@@ -691,8 +696,8 @@ func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) {
if err := r.adminQuery().Where("o.id = ?", orderID).First(&row).Error; err != nil {
return nil, err
}
dto := row.toDTO()
dto.Checkout = r.latestCheckoutDTO(orderID)
dto := row.toAdminDTO()
dto.Checkout = r.latestCheckoutAdminDTO(orderID)
return &dto, nil
}
@@ -882,8 +887,8 @@ func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, erro
First(&row).Error; err != nil {
return nil, err
}
dto := row.toDTO()
dto.Checkout = r.latestCheckoutDTO(orderID)
dto := row.toDTOForUser(userID)
dto.Checkout = r.latestCheckoutDTOForUser(orderID, userID, row.RentalOrder)
return &dto, nil
}
@@ -1018,7 +1023,10 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
if consumableAmount < 0 || coinConsumedM < 0 || otherAmount < 0 || explicitDeduct < 0 {
return model.OrderCheckout{}, ErrInvalidCheckoutAmount
}
deductAmount := consumableAmount + otherAmount
consumableAmount = roundMoney(consumableAmount)
otherAmount = roundMoney(otherAmount)
explicitDeduct = roundMoney(explicitDeduct)
deductAmount := otherAmount
if useExplicitDeduct {
deductAmount = explicitDeduct
}
@@ -1038,9 +1046,9 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
OwnerRentAmount: order.OwnerRentAmount,
PlatformFee: order.PlatformFee,
DepositAmount: order.DepositAmount,
ConsumableAmount: roundMoney(consumableAmount),
CoinConsumedM: roundMoney(coinConsumedM),
OtherAmount: roundMoney(otherAmount),
ConsumableAmount: consumableAmount,
CoinConsumedM: roundQuantity(coinConsumedM),
OtherAmount: otherAmount,
DepositDeductAmount: roundMoney(deductAmount),
RenterRefundAmount: roundMoney(renterRefund),
OwnerIncomeAmount: roundMoney(order.OwnerRentAmount + deductAmount),
@@ -1069,6 +1077,10 @@ func decodeStringList(raw datatypes.JSON) []string {
}
func roundMoney(value float64) float64 {
return math.Round(value)
}
func roundQuantity(value float64) float64 {
return math.Round(value*100) / 100
}
@@ -1080,12 +1092,21 @@ func hasOpenCheckout(tx *gorm.DB, orderID uint64) (bool, error) {
return count > 0, err
}
func (r *Repository) latestCheckoutDTO(orderID uint64) *CheckoutDTO {
func (r *Repository) latestCheckoutAdminDTO(orderID uint64) *CheckoutDTO {
var checkout model.OrderCheckout
if err := r.db.Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil {
return nil
}
dto := toCheckoutDTO(checkout)
dto := toCheckoutAdminDTO(checkout)
return &dto
}
func (r *Repository) latestCheckoutDTOForUser(orderID uint64, userID uint64, order model.RentalOrder) *CheckoutDTO {
var checkout model.OrderCheckout
if err := r.db.Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil {
return nil
}
dto := toCheckoutDTOForUser(checkout, userID, order)
return &dto
}
@@ -1120,9 +1141,12 @@ type orderRow struct {
RenterPhone string
}
func (row orderRow) toDTO() OrderDTO {
func (row orderRow) toAdminDTO() OrderDTO {
rentedAt := row.RentedAt
durationHours := orderDurationHours(row.RentalOrder)
rentAmount := row.RentAmount
ownerRentAmount := row.OwnerRentAmount
platformFee := row.PlatformFee
return OrderDTO{
ID: row.ID,
OrderNo: row.OrderNo,
@@ -1137,10 +1161,12 @@ func (row orderRow) toDTO() OrderDTO {
LoginPlatform: row.LoginPlatform,
RentedAt: rentedAt,
EstimatedDurationHours: durationHours,
RentAmount: row.RentAmount,
OwnerRentAmount: row.OwnerRentAmount,
PriceRole: "admin",
DisplayAmount: row.RentAmount,
RentAmount: &rentAmount,
OwnerRentAmount: &ownerRentAmount,
DepositAmount: row.DepositAmount,
PlatformFee: row.PlatformFee,
PlatformFee: &platformFee,
AccountSnapshot: row.AccountSnapshot,
Status: row.Status,
HandoffStatus: row.HandoffStatus,
@@ -1150,6 +1176,12 @@ func (row orderRow) toDTO() OrderDTO {
}
}
func (row orderRow) toDTOForUser(userID uint64) OrderDTO {
dto := row.toAdminDTO()
applyOrderPriceView(&dto, row.RentalOrder, userID)
return dto
}
func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO {
return HandoffRecordDTO{
ID: record.ID,
@@ -1164,22 +1196,29 @@ func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO {
}
}
func toCheckoutDTO(checkout model.OrderCheckout) CheckoutDTO {
func toCheckoutAdminDTO(checkout model.OrderCheckout) CheckoutDTO {
rentAmount := checkout.RentAmount
ownerRentAmount := checkout.OwnerRentAmount
platformFee := checkout.PlatformFee
renterRefundAmount := checkout.RenterRefundAmount
ownerIncomeAmount := checkout.OwnerIncomeAmount
return CheckoutDTO{
ID: checkout.ID,
OrderID: checkout.OrderID,
InitiatedBy: checkout.InitiatedBy,
Status: checkout.Status,
RentAmount: checkout.RentAmount,
OwnerRentAmount: checkout.OwnerRentAmount,
PlatformFee: checkout.PlatformFee,
PriceRole: "admin",
DisplayAmount: checkout.RentAmount,
RentAmount: &rentAmount,
OwnerRentAmount: &ownerRentAmount,
PlatformFee: &platformFee,
DepositAmount: checkout.DepositAmount,
ConsumableAmount: checkout.ConsumableAmount,
CoinConsumedM: checkout.CoinConsumedM,
OtherAmount: checkout.OtherAmount,
DepositDeductAmount: checkout.DepositDeductAmount,
RenterRefundAmount: checkout.RenterRefundAmount,
OwnerIncomeAmount: checkout.OwnerIncomeAmount,
RenterRefundAmount: &renterRefundAmount,
OwnerIncomeAmount: &ownerIncomeAmount,
Content: checkout.Content,
EvidenceURLS: decodeStringList(checkout.EvidenceURLS),
OwnerAdjustmentReason: checkout.OwnerAdjustmentReason,
@@ -1191,6 +1230,123 @@ func toCheckoutDTO(checkout model.OrderCheckout) CheckoutDTO {
}
}
func toCheckoutDTOForUser(checkout model.OrderCheckout, userID uint64, order model.RentalOrder) CheckoutDTO {
dto := toCheckoutAdminDTO(checkout)
applyCheckoutPriceView(&dto, order, userID)
return dto
}
func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64) {
if dto == nil {
return
}
dto.PlatformFee = nil
switch {
case userID == order.OwnerID:
ownerAmount := order.OwnerRentAmount
if ownerAmount <= 0 {
ownerAmount = order.RentAmount
}
dto.PriceRole = "owner"
dto.DisplayAmount = ownerAmount
dto.RentAmount = nil
dto.OwnerRentAmount = &ownerAmount
sanitizeOrderSnapshot(&dto.AccountSnapshot, "owner")
case userID == order.RenterID:
rentAmount := order.RentAmount
dto.PriceRole = "renter"
dto.DisplayAmount = rentAmount
dto.RentAmount = &rentAmount
dto.OwnerRentAmount = nil
sanitizeOrderSnapshot(&dto.AccountSnapshot, "renter")
default:
dto.PriceRole = ""
dto.DisplayAmount = 0
dto.RentAmount = nil
dto.OwnerRentAmount = nil
sanitizeOrderSnapshot(&dto.AccountSnapshot, "")
}
}
func applyCheckoutPriceView(dto *CheckoutDTO, order model.RentalOrder, userID uint64) {
if dto == nil {
return
}
dto.PlatformFee = nil
dto.RenterRefundAmount = nil
dto.OwnerIncomeAmount = nil
switch {
case userID == order.OwnerID:
ownerAmount := order.OwnerRentAmount
if ownerAmount <= 0 {
ownerAmount = order.RentAmount
}
ownerIncomeAmount := dto.DepositDeductAmount + ownerAmount
dto.PriceRole = "owner"
dto.DisplayAmount = ownerAmount
dto.RentAmount = nil
dto.OwnerRentAmount = &ownerAmount
dto.OwnerIncomeAmount = &ownerIncomeAmount
case userID == order.RenterID:
rentAmount := order.RentAmount
renterRefundAmount := order.DepositAmount - dto.DepositDeductAmount
if renterRefundAmount < 0 {
renterRefundAmount = 0
}
dto.PriceRole = "renter"
dto.DisplayAmount = rentAmount
dto.RentAmount = &rentAmount
dto.OwnerRentAmount = nil
dto.RenterRefundAmount = &renterRefundAmount
default:
dto.PriceRole = ""
dto.DisplayAmount = 0
dto.RentAmount = nil
dto.OwnerRentAmount = nil
}
}
func sanitizeOrderSnapshot(snapshot *datatypes.JSON, role string) {
if snapshot == nil || len(*snapshot) == 0 {
return
}
var payload map[string]any
if err := json.Unmarshal(*snapshot, &payload); err != nil {
return
}
rawSummary, ok := payload["asset_summary"]
if !ok {
return
}
var summary map[string]any
switch typed := rawSummary.(type) {
case map[string]any:
summary = typed
case string:
if err := json.Unmarshal([]byte(typed), &summary); err != nil {
return
}
default:
raw, err := json.Marshal(typed)
if err != nil || json.Unmarshal(raw, &summary) != nil {
return
}
}
breakdown, _ := summary["price_breakdown"].(map[string]any)
if role == "owner" && breakdown != nil {
if sellerRatio := readJSONNumber(breakdown["seller_ratio"]); sellerRatio > 0 {
summary["publish_ratio"] = sellerRatio
}
}
delete(summary, "price_breakdown")
payload["asset_summary"] = summary
raw, err := json.Marshal(payload)
if err != nil {
return
}
*snapshot = datatypes.JSON(raw)
}
func makeAccountSnapshot(account model.GameAccount) (datatypes.JSON, error) {
payload := map[string]any{
"account_id": account.ID,
@@ -202,7 +202,7 @@ func applyEntry(account *model.WalletAccount, entry Entry) (float64, error) {
}
func roundWalletMoney(value float64) float64 {
return math.Round(value*100) / 100
return math.Round(value)
}
func toAccountDTO(account model.WalletAccount) *AccountDTO {
+12 -8
View File
@@ -16,10 +16,12 @@ export interface Order {
login_platform: string
rented_at?: string
estimated_duration_hours: number
rent_amount: number
owner_rent_amount: number
price_role?: 'renter' | 'owner' | 'admin' | string
display_amount: number
rent_amount?: number
owner_rent_amount?: number
deposit_amount: number
platform_fee: number
platform_fee?: number
account_snapshot?: Record<string, unknown>
status: OrderStatus
handoff_status: HandoffStatus
@@ -34,16 +36,18 @@ export interface Checkout {
order_id: number
initiated_by: number
status: SettlementStatus
rent_amount: number
owner_rent_amount: number
platform_fee: number
price_role?: 'renter' | 'owner' | 'admin' | string
display_amount: number
rent_amount?: number
owner_rent_amount?: number
platform_fee?: number
deposit_amount: number
consumable_amount: number
coin_consumed_m: number
other_amount: number
deposit_deduct_amount: number
renter_refund_amount: number
owner_income_amount: number
renter_refund_amount?: number
owner_income_amount?: number
content: string
evidence_urls: string[]
owner_adjustment_reason: string
+1 -1
View File
@@ -13,7 +13,7 @@ export const dailyLossOptions = [10, 20, 30, 40, 50]
export const commonOnlineTimes = ['00:00', '08:00', '10:00', '12:00', '14:00', '18:00', '20:00', '22:00', '23:59']
export function roundMoney(value: number) {
return Math.round(value * 100) / 100
return Math.round(value)
}
export function roundRatio(value: number) {
+33 -22
View File
@@ -64,6 +64,7 @@ const disputeEvidenceText = ref('')
const isOwner = computed(() => order.value?.owner_id === session.userId)
const isRenter = computed(() => order.value?.renter_id === session.userId)
const orderAmountLabel = computed(() => (isOwner.value ? '我的租金' : '订单金额'))
const canOpenDispute = computed(() => {
if (!order.value || (!isOwner.value && !isRenter.value)) return false
return !['completed', 'cancelled', 'closed', 'disputing', 'checkout_disputing', 'abnormal'].includes(order.value.status)
@@ -86,10 +87,10 @@ const resourceChargeAmount = computed(() => {
})
const snapshotHafCoinM = computed(() => {
const snapshot = readSnapshot()
return roundMoney(readNumber(snapshot?.haf_coin_amount) / 1000000)
return roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000)
})
const remainingHafCoinM = computed(() => {
return roundMoney(Math.max(snapshotHafCoinM.value - Number(checkoutForm.value.coin_consumed_m || 0), 0))
return roundQuantity(Math.max(snapshotHafCoinM.value - Number(checkoutForm.value.coin_consumed_m || 0), 0))
})
onMounted(loadOrder)
@@ -387,12 +388,12 @@ function checkoutContentWithSummary() {
if (usedResources.length) {
lines.push(
`额外消耗品:${usedResources
.map((item) => `${item.label} ${readResourceUsage(item.key)}/${item.quantity}${isChargedResource(item) ? `扣款¥${resourceLineAmount(item).toFixed(2)}` : ',赠送不扣款'}`)
.map((item) => `${item.label} ${readResourceUsage(item.key)}/${item.quantity}${isChargedResource(item) ? `金额¥${money(resourceLineAmount(item))}` : ',赠送不扣款'}`)
.join('')}`,
)
}
if (Number(checkoutForm.value.coin_consumed_m || 0) > 0) {
lines.push(`哈夫币消耗:${Number(checkoutForm.value.coin_consumed_m).toFixed(2)}M,预计剩余${remainingHafCoinM.value.toFixed(2)}M`)
lines.push(`哈夫币消耗:${quantity(Number(checkoutForm.value.coin_consumed_m))}M,预计剩余${quantity(remainingHafCoinM.value)}M`)
}
if (lines.length === 0) {
lines.push('租客发起结账。')
@@ -415,9 +416,22 @@ function readUnitPrice(priceText: string) {
}
function roundMoney(value: number) {
return Math.round(value)
}
function roundQuantity(value: number) {
return Math.round(value * 100) / 100
}
function money(value: unknown) {
return `${roundMoney(readNumber(value))}`
}
function quantity(value: unknown) {
const rounded = roundQuantity(readNumber(value))
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
}
function readNumber(value: unknown) {
const number = Number(value || 0)
return Number.isFinite(number) ? number : 0
@@ -462,16 +476,12 @@ function linesToList(value: string) {
<strong>{{ handoffStatusLabel(order.handoff_status) }}</strong>
</div>
<div class="metric-card">
<span>订单金额</span>
<strong>¥{{ order.rent_amount }}</strong>
</div>
<div class="metric-card">
<span>平台费用</span>
<strong>¥{{ order.platform_fee }}</strong>
<span>{{ orderAmountLabel }}</span>
<strong>¥{{ money(order.display_amount) }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>¥{{ order.deposit_amount }}</strong>
<strong>¥{{ money(order.deposit_amount) }}</strong>
</div>
</div>
@@ -518,7 +528,7 @@ function linesToList(value: string) {
<div class="checkout-resource-panel panel-action">
<div class="checkout-resource-head">
<strong>额外消耗品</strong>
<span>扣款合计¥{{ resourceChargeAmount.toFixed(2) }}</span>
<span>金额合计¥{{ money(resourceChargeAmount) }}</span>
</div>
<el-empty v-if="checkoutResources.length === 0" description="订单快照中暂无额外消耗品" />
<div v-for="item in checkoutResources" v-else :key="item.key" class="checkout-resource-row">
@@ -533,16 +543,16 @@ function linesToList(value: string) {
:precision="0"
controls-position="right"
/>
<span class="checkout-resource-amount">¥{{ resourceLineAmount(item).toFixed(2) }}</span>
<span class="checkout-resource-amount">¥{{ money(resourceLineAmount(item)) }}</span>
</div>
</div>
<el-form class="form-grid panel-action" label-position="top">
<el-form-item label="消耗哈夫币(M">
<el-input-number v-model="checkoutForm.coin_consumed_m" class="full-control" :min="0" :max="snapshotHafCoinM" :precision="2" controls-position="right" />
<span class="field-hint">订单快照 {{ snapshotHafCoinM.toFixed(2) }}M预计剩余 {{ remainingHafCoinM.toFixed(2) }}M</span>
<span class="field-hint">订单快照 {{ quantity(snapshotHafCoinM) }}M预计剩余 {{ quantity(remainingHafCoinM) }}M</span>
</el-form-item>
<el-form-item label="其他扣款(元)">
<el-input-number v-model="checkoutForm.other_amount" class="full-control" :min="0" :precision="2" controls-position="right" />
<el-input-number v-model="checkoutForm.other_amount" class="full-control" :min="0" :precision="0" controls-position="right" />
</el-form-item>
</el-form>
<el-input
@@ -557,9 +567,10 @@ function linesToList(value: string) {
<div v-if="order && order.checkout && ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.status)" class="order-panel">
<h2>结账明细</h2>
<p>买家租金¥{{ order.checkout.rent_amount }}号主租金¥{{ order.checkout.owner_rent_amount }}平台费用¥{{ order.checkout.platform_fee }}</p>
<p>押金¥{{ order.checkout.deposit_amount }}</p>
<p>扣除¥{{ order.checkout.deposit_deduct_amount }}退还租客¥{{ order.checkout.renter_refund_amount }}号主收入¥{{ order.checkout.owner_income_amount }}</p>
<p>{{ orderAmountLabel }}¥{{ money(order.checkout.display_amount) }}押金¥{{ money(order.checkout.deposit_amount) }}</p>
<p>额外消耗品金额¥{{ money(order.checkout.consumable_amount) }}押金扣除¥{{ money(order.checkout.deposit_deduct_amount) }}</p>
<p v-if="isRenter">退还租客¥{{ money(order.checkout.renter_refund_amount) }}</p>
<p v-if="isOwner">号主收入¥{{ money(order.checkout.owner_income_amount) }}</p>
<p v-if="order.checkout.content">说明{{ order.checkout.content }}</p>
<p v-if="order.checkout.owner_adjustment_reason">修正原因{{ order.checkout.owner_adjustment_reason }}</p>
</div>
@@ -571,17 +582,17 @@ function linesToList(value: string) {
<h2 class="panel-action">修改结账</h2>
<el-form class="form-grid" label-position="top">
<el-form-item label="消耗扣款(元">
<el-input-number v-model="counterForm.consumable_amount" class="full-control" :min="0" :precision="2" controls-position="right" />
<el-form-item label="额外消耗品金额(已含租金">
<el-input-number v-model="counterForm.consumable_amount" class="full-control" :min="0" :precision="0" controls-position="right" />
</el-form-item>
<el-form-item label="消耗哈夫币(M">
<el-input-number v-model="counterForm.coin_consumed_m" class="full-control" :min="0" :precision="2" controls-position="right" />
</el-form-item>
<el-form-item label="其他扣款(元)">
<el-input-number v-model="counterForm.other_amount" class="full-control" :min="0" :precision="2" controls-position="right" />
<el-input-number v-model="counterForm.other_amount" class="full-control" :min="0" :precision="0" controls-position="right" />
</el-form-item>
<el-form-item label="押金扣除(元)">
<el-input-number v-model="counterForm.deposit_deduct_amount" class="full-control" :min="0" :max="order.deposit_amount" :precision="2" controls-position="right" />
<el-input-number v-model="counterForm.deposit_deduct_amount" class="full-control" :min="0" :max="order.deposit_amount" :precision="0" controls-position="right" />
</el-form-item>
</el-form>
<el-input v-model="counterForm.reason" class="panel-action" type="textarea" :rows="3" placeholder="填写修改原因" />
+10 -2
View File
@@ -48,6 +48,10 @@ function orderRole(order: Order) {
if (order.owner_id === session.userId) return '号主'
return '-'
}
function money(value: unknown) {
return Math.round(Number(value || 0))
}
</script>
<template>
@@ -61,8 +65,12 @@ function orderRole(order: Order) {
<el-table v-loading="loading" class="table-panel" :data="orders">
<el-table-column prop="order_no" label="订单号" min-width="210" />
<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 label="我的金额" width="100">
<template #default="{ row }">¥{{ money(row.display_amount) }}</template>
</el-table-column>
<el-table-column label="押金" width="100">
<template #default="{ row }">¥{{ money(row.deposit_amount) }}</template>
</el-table-column>
<el-table-column label="身份" width="80">
<template #default="{ row }">{{ orderRole(row) }}</template>
</el-table-column>
+1 -1
View File
@@ -85,7 +85,7 @@ function readError(error: unknown, fallback: string) {
<div class="table-panel recharge-panel">
<h2>开发充值</h2>
<el-input-number v-model="rechargeAmount" :min="0.01" :precision="2" controls-position="right" />
<el-input-number v-model="rechargeAmount" :min="1" :precision="0" controls-position="right" />
<el-button type="primary" :loading="recharging" @click="handleRecharge">充值</el-button>
</div>
@@ -20,7 +20,7 @@ async function loadDashboard() {
}
function money(value?: number) {
return `¥${Number(value || 0).toFixed(2)}`
return `¥${Math.round(Number(value || 0))}`
}
</script>
@@ -162,7 +162,7 @@ function readError(error: unknown, fallback: string) {
v-model="amount"
class="full-control panel-action"
:min="0"
:precision="2"
:precision="0"
:step="10"
placeholder="裁决金额"
/>
@@ -54,7 +54,7 @@ async function submitAction() {
}
function money(value: number) {
return `¥${Number(value || 0).toFixed(2)}`
return `¥${Math.round(Number(value || 0))}`
}
function listingPrice(row: Listing) {
@@ -63,7 +63,7 @@ function openEvidence(row: Listing) {
}
function listingPrice(row: Listing) {
return `¥${Number(row.price || 0).toFixed(2)}`
return `¥${Math.round(Number(row.price || 0))}`
}
function extractObjectKey(url: string) {
@@ -39,7 +39,7 @@ function resetFilters() {
}
function money(value: number) {
return `¥${Number(value || 0).toFixed(2)}`
return `¥${Math.round(Number(value || 0))}`
}
function listingPrice(row: Listing) {
@@ -75,6 +75,10 @@ function orderEstimatedEndAt() {
if (!rentedAt || durationHours <= 0) return undefined
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
}
function money(value: unknown) {
return Math.round(Number(value || 0))
}
</script>
<template>
@@ -105,15 +109,15 @@ function orderEstimatedEndAt() {
</div>
<div class="metric-card">
<span>订单金额</span>
<strong>¥{{ order.rent_amount }}</strong>
<strong>¥{{ money(order.rent_amount) }}</strong>
</div>
<div class="metric-card">
<span>平台费用</span>
<strong>¥{{ order.platform_fee }}</strong>
<strong>¥{{ money(order.platform_fee) }}</strong>
</div>
<div class="metric-card">
<span>押金</span>
<strong>¥{{ order.deposit_amount }}</strong>
<strong>¥{{ money(order.deposit_amount) }}</strong>
</div>
</div>
@@ -56,7 +56,7 @@ function handleSizeChange() {
}
function money(value: number) {
return `¥${value.toFixed(2)}`
return `¥${Math.round(Number(value || 0))}`
}
function directionType(direction: string) {
@@ -42,8 +42,8 @@ onMounted(async () => {
});
const orderTotal = computed(() => {
if (!listing.value) return "0.00";
return getListingDisplayPrice(listing.value).toFixed(2);
if (!listing.value) return "0";
return `${Math.round(getListingDisplayPrice(listing.value))}`;
});
const detailMetrics = computed(() => {
@@ -72,6 +72,7 @@ const showRejectPopup = ref(false);
const isOwner = computed(() => order.value?.owner_id === session.userId);
const isRenter = computed(() => order.value?.renter_id === session.userId);
const orderAmountLabel = computed(() => (isOwner.value ? "我的租金" : "订单金额"));
const canOpenDispute = computed(() => {
if (!order.value || (!isOwner.value && !isRenter.value)) return false;
return !["completed", "cancelled", "closed", "disputing", "checkout_disputing", "abnormal"].includes(order.value.status);
@@ -94,10 +95,10 @@ const resourceChargeAmount = computed(() => {
});
const snapshotHafCoinM = computed(() => {
const snapshot = readSnapshot();
return roundMoney(readNumber(snapshot?.haf_coin_amount) / 1000000);
return roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000);
});
const remainingHafCoinM = computed(() => {
return roundMoney(Math.max(snapshotHafCoinM.value - Number(checkoutForm.value.coin_consumed_m || 0), 0));
return roundQuantity(Math.max(snapshotHafCoinM.value - Number(checkoutForm.value.coin_consumed_m || 0), 0));
});
onMounted(loadOrder);
@@ -450,14 +451,14 @@ function checkoutContentWithSummary() {
.map(
(item) =>
`${item.label} ${readResourceUsage(item.key)}/${item.quantity}${
isChargedResource(item) ? `扣款¥${resourceLineAmount(item).toFixed(2)}` : ",赠送不扣款"
isChargedResource(item) ? `金额¥${money(resourceLineAmount(item))}` : ",赠送不扣款"
}`
)
.join("")}`
);
}
if (Number(checkoutForm.value.coin_consumed_m || 0) > 0) {
lines.push(`哈夫币消耗:${Number(checkoutForm.value.coin_consumed_m).toFixed(2)}M,预计剩余${remainingHafCoinM.value.toFixed(2)}M`);
lines.push(`哈夫币消耗:${quantity(Number(checkoutForm.value.coin_consumed_m))}M,预计剩余${quantity(remainingHafCoinM.value)}M`);
}
if (lines.length === 0) {
lines.push("租客发起结账。");
@@ -480,9 +481,22 @@ function readUnitPrice(priceText: string) {
}
function roundMoney(value: number) {
return Math.round(value);
}
function roundQuantity(value: number) {
return Math.round(value * 100) / 100;
}
function money(value: unknown) {
return `${roundMoney(readNumber(value))}`;
}
function quantity(value: unknown) {
const rounded = roundQuantity(readNumber(value));
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2);
}
function readNumber(value: unknown) {
const number = Number(value || 0);
return Number.isFinite(number) ? number : 0;
@@ -560,12 +574,12 @@ function getStatusTagType(status: string) {
<h2 class="order-title">{{ order.title }}</h2>
<div class="order-meta-grid">
<div class="meta-item">
<span class="meta-label">订单金额</span>
<strong class="meta-value accent">¥{{ order.rent_amount }}</strong>
<span class="meta-label">{{ orderAmountLabel }}</span>
<strong class="meta-value accent">¥{{ money(order.display_amount) }}</strong>
</div>
<div class="meta-item">
<span class="meta-label">押金</span>
<strong class="meta-value">¥{{ order.deposit_amount }}</strong>
<strong class="meta-value">¥{{ money(order.deposit_amount) }}</strong>
</div>
<div class="meta-item">
<span class="meta-label">交接状态</span>
@@ -711,12 +725,12 @@ function getStatusTagType(status: string) {
min="0"
:max="item.quantity"
/>
<span class="resource-line-amount">¥{{ resourceLineAmount(item).toFixed(2) }}</span>
<span class="resource-line-amount">¥{{ money(resourceLineAmount(item)) }}</span>
</div>
</div>
<div class="resource-panel-footer">
<span>物资费用扣款合计</span>
<strong>¥{{ resourceChargeAmount.toFixed(2) }}</strong>
<span>物资金额合计</span>
<strong>¥{{ money(resourceChargeAmount) }}</strong>
</div>
</div>
@@ -735,7 +749,7 @@ function getStatusTagType(status: string) {
</template>
</van-field>
<div class="coin-hint">
订单快照 {{ snapshotHafCoinM.toFixed(2) }}M预计剩余 {{ remainingHafCoinM.toFixed(2) }}M
订单快照 {{ quantity(snapshotHafCoinM) }}M预计剩余 {{ quantity(remainingHafCoinM) }}M
</div>
<van-field label="其他费用" label-width="100px">
<template #input>
@@ -778,13 +792,12 @@ function getStatusTagType(status: string) {
<section v-if="order.checkout && ['pending_checkout_confirm', 'pending_checkout_accept'].includes(order.status)" class="card-section">
<h3 class="section-title">结账结算账单</h3>
<van-cell-group inset :border="false">
<van-cell title="买家租金" :value="`¥${order.checkout.rent_amount}`" />
<van-cell title="号主实收租金" :value="`¥${order.checkout.owner_rent_amount}`" />
<van-cell title="平台服务费" :value="`¥${order.checkout.platform_fee}`" />
<van-cell title="押金总额" :value="`¥${order.checkout.deposit_amount}`" />
<van-cell title="扣款总计" :value="`¥${order.checkout.deposit_deduct_amount}`" value-class="red-text" />
<van-cell title="退还租客押金" :value="`¥${order.checkout.renter_refund_amount}`" value-class="green-text" />
<van-cell title="号主最终收益" :value="`¥${order.checkout.owner_income_amount}`" value-class="green-text" />
<van-cell :title="orderAmountLabel" :value="`¥${money(order.checkout.display_amount)}`" />
<van-cell title="押金总额" :value="`¥${money(order.checkout.deposit_amount)}`" />
<van-cell title="额外消耗品金额" :value="`¥${money(order.checkout.consumable_amount)}`" />
<van-cell title="押金扣除" :value="`¥${money(order.checkout.deposit_deduct_amount)}`" value-class="red-text" />
<van-cell v-if="isRenter" title="退还租客押金" :value="`¥${money(order.checkout.renter_refund_amount)}`" value-class="green-text" />
<van-cell v-if="isOwner" title="号主最终收益" :value="`¥${money(order.checkout.owner_income_amount)}`" value-class="green-text" />
<van-cell v-if="order.checkout.content" title="结账备注" :label="order.checkout.content" />
<van-cell v-if="order.checkout.owner_adjustment_reason" title="号主修正原因" :label="order.checkout.owner_adjustment_reason" />
</van-cell-group>
@@ -93,6 +93,10 @@ function readError(error: unknown, fallback: string) {
}
return fallback;
}
function money(value: unknown) {
return Math.round(Number(value || 0));
}
</script>
<template>
@@ -164,12 +168,12 @@ function readError(error: unknown, fallback: string) {
<div class="price-row">
<div class="price-item">
<span class="price-label">买家实付租金</span>
<span class="price-val">¥{{ order.rent_amount }}</span>
<span class="price-label">我的金额</span>
<span class="price-val">¥{{ money(order.display_amount) }}</span>
</div>
<div class="price-item">
<span class="price-label">押金金额</span>
<span class="price-val deposit">¥{{ order.deposit_amount }}</span>
<span class="price-val deposit">¥{{ money(order.deposit_amount) }}</span>
</div>
</div>
</div>
@@ -276,7 +276,7 @@ function resolveAvatarURL(url: string | undefined | null) {
<div class="balance-row">
<div class="balance-info">
<span class="balance-label">账户可用余额()</span>
<strong class="balance-value">¥{{ balance.toFixed(2) }}</strong>
<strong class="balance-value">¥{{ Math.round(Number(balance || 0)) }}</strong>
</div>
<button class="withdraw-btn" @click="handleWithdraw">提现</button>
</div>
@@ -375,7 +375,7 @@ function resolveAvatarURL(url: string | undefined | null) {
<span class="ledger-time">{{ formatDateMinute(item.created_at) }}</span>
</div>
<div class="ledger-right" :class="item.direction === 'in' ? 'in-color' : 'out-color'">
{{ item.direction === 'in' ? '+' : '-' }}¥{{ item.amount.toFixed(2) }}
{{ item.direction === 'in' ? '+' : '-' }}¥{{ Math.round(Number(item.amount || 0)) }}
</div>
</div>
</div>
@@ -129,7 +129,7 @@ function getStatusText(item: Listing) {
}
function listingPrice(item: Listing) {
return getListingSellerPrice(item).toFixed(2)
return `${Math.round(getListingSellerPrice(item))}`
}
function goBack() {
@@ -197,7 +197,7 @@ function goBack() {
<div class="price-row">
<span class="price-val">¥{{ listingPrice(item) }}</span>
<span class="deposit-val">押金: ¥{{ item.deposit_amount.toFixed(2) }}</span>
<span class="deposit-val">押金: ¥{{ Math.round(Number(item.deposit_amount || 0)) }}</span>
</div>
</div>
</div>
@@ -45,7 +45,7 @@ function readError(error: unknown, fallback: string) {
}
function listingPrice(item: Listing) {
return Number(item.price || 0).toFixed(2);
return `${Math.round(Number(item.price || 0))}`;
}
</script>
@@ -59,6 +59,10 @@ function actionText(order: Order) {
if (['overdue', 'checkout_disputing', 'disputing', 'abnormal'].includes(order.status)) return '查看处理'
return '详情'
}
function money(value: unknown) {
return Math.round(Number(value || 0))
}
</script>
<template>
@@ -103,7 +107,7 @@ function actionText(order: Order) {
<el-table-column prop="order_no" label="订单号" min-width="220" />
<el-table-column prop="title" label="账号" min-width="180" />
<el-table-column label="金额" width="120">
<template #default="{ row }">¥{{ row.rent_amount }}</template>
<template #default="{ row }">¥{{ money(row.display_amount) }}</template>
</el-table-column>
<el-table-column label="订单状态" width="150">
<template #default="{ row }">{{ orderStatusLabel(row.status) }}</template>
@@ -37,7 +37,7 @@ async function offline(id: number) {
}
function listingPrice(row: Listing) {
return `¥${getListingSellerPrice(row).toFixed(2)}`
return `¥${Math.round(getListingSellerPrice(row))}`
}
</script>