拆分订单详情结账逻辑

This commit is contained in:
yml
2026-06-09 22:07:55 +08:00
parent d68020db4e
commit 3bbe356c3f
9 changed files with 1445 additions and 1402 deletions
@@ -1,80 +1,194 @@
import type { Order } from '../api/orders'
import { computed, type Ref } from 'vue'
export interface SnapshotResource {
key: string
label: string
quantity: number
unitPrice: number
chargeMode: '赠送' | '收费'
import { centToYuan, formatMoney } from '@/shared/utils/money'
import type { Checkout, Order } from '../api/orders'
export interface CheckoutFormSnapshot {
content: string
coin_consumed_m: number
}
export function readSnapshot(order: Order | null) {
if (!order?.listing_snapshot) return null
try {
return JSON.parse(order.listing_snapshot) as Record<string, any>
} catch {
return null
export interface CounterFormSnapshot {
consumableAmountYuan: number
coin_consumed_m: number
otherAmountYuan: number
depositDeductAmountYuan: number
reason: string
evidenceText: string
}
export interface CheckoutResource {
key: string
label: string
price: string
mode: string
quantity: number
unitPrice: number
}
export function useOrderCheckoutSnapshot(options: {
order: Ref<Order | null>
checkoutForm: Ref<CheckoutFormSnapshot>
resourceUsage: Ref<Record<string, number>>
}) {
const checkoutResources = computed(() => readSnapshotResources(options.order.value))
const resourceChargeAmount = computed(() =>
calculateResourceChargeAmount(checkoutResources.value, options.resourceUsage.value)
)
const snapshotHafCoinM = computed(() => getSnapshotHafCoinM(options.order.value))
const remainingHafCoinM = computed(() =>
roundQuantity(
Math.max(snapshotHafCoinM.value - Number(options.checkoutForm.value.coin_consumed_m || 0), 0)
)
)
function hydrateResourceUsage() {
options.resourceUsage.value = normalizeResourceUsage(
checkoutResources.value,
options.resourceUsage.value
)
}
function readResourceUsage(key: string) {
return readResourceUsageAmount(options.resourceUsage.value, key)
}
function resourceLineAmount(item: CheckoutResource) {
return roundMoney(readResourceUsage(item.key) * item.unitPrice)
}
function checkoutContentWithSummary() {
return buildCheckoutContentWithSummary({
content: options.checkoutForm.value.content,
coinConsumedM: options.checkoutForm.value.coin_consumed_m,
checkoutResources: checkoutResources.value,
resourceUsage: options.resourceUsage.value,
remainingHafCoinM: remainingHafCoinM.value,
})
}
return {
checkoutResources,
resourceChargeAmount,
snapshotHafCoinM,
remainingHafCoinM,
hydrateResourceUsage,
readResourceUsage,
isChargedResource,
resourceLineAmount,
checkoutContentWithSummary,
}
}
export function readNumber(value: unknown): number {
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : 0
export function readSnapshot(order: Order | null) {
const snapshot = order?.account_snapshot
return isRecord(snapshot) ? snapshot : null
}
export function roundQuantity(value: number): number {
return Math.round(value * 10) / 10
export function readAssetSummary(order: Order | null) {
const summary = readSnapshot(order)?.asset_summary
return isRecord(summary) ? summary : null
}
export function roundMoney(value: number): number {
return Math.round(value * 100) / 100
export function readSnapshotResources(order: Order | null): CheckoutResource[] {
const resources = readAssetSummary(order)?.resources
if (!Array.isArray(resources)) return []
return resources
.filter(isRecord)
.map(item => {
const key = String(item.key || item.label || '')
const label = String(item.label || key || '额外消耗品')
const price = String(item.price || '')
return {
key,
label,
price,
mode: String(item.mode || '收费'),
quantity: readNumber(item.quantity),
unitPrice: readUnitPrice(price),
}
})
.filter(item => item.key && item.quantity > 0)
}
export function readSnapshotResources(order: Order | null): SnapshotResource[] {
const snapshot = readSnapshot(order)
if (!snapshot?.quantities) return []
const quantities = snapshot.quantities as Record<string, any>[]
return quantities
.map(item => ({
key: String(item.key || ''),
label: String(item.label || ''),
quantity: readNumber(item.quantity),
unitPrice: readNumber(item.price),
chargeMode: (item.charge_mode === '收费' ? '收费' : '赠送') as SnapshotResource['chargeMode'],
}))
.filter(item => item.key && item.label)
export function normalizeResourceUsage(
resources: CheckoutResource[],
current: Record<string, number>
) {
const next: Record<string, number> = {}
for (const item of resources) {
next[item.key] = Math.min(Math.max(Number(current[item.key] || 0), 0), item.quantity)
}
return next
}
export function isChargedResource(resource: SnapshotResource): boolean {
return resource.chargeMode === '收费'
export function readResourceUsageAmount(resourceUsage: Record<string, number>, key: string) {
return Math.max(Number(resourceUsage[key] || 0), 0)
}
export function getSnapshotHafCoinM(order: Order | null): number {
const snapshot = readSnapshot(order)
return roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000)
export function isChargedResource(item: CheckoutResource) {
return item.mode !== '赠送'
}
export function calculateResourceChargeAmount(
resources: SnapshotResource[],
resources: CheckoutResource[],
resourceUsage: Record<string, number>
): number {
) {
return roundMoney(
resources.reduce((sum, item) => {
if (!isChargedResource(item)) return sum
const used = resourceUsage[item.key] || 0
const used = readResourceUsageAmount(resourceUsage, item.key)
return sum + used * item.unitPrice
}, 0)
)
}
export function calculateCheckoutTotal(
resourceCharge: number,
consumableAmount: number,
coinConsumed: number,
otherAmount: number
): number {
return roundMoney(resourceCharge + consumableAmount + coinConsumed + otherAmount)
export function buildCheckoutContentWithSummary(options: {
content: string
coinConsumedM: number
checkoutResources: CheckoutResource[]
resourceUsage: Record<string, number>
remainingHafCoinM: number
}) {
const lines = [options.content.trim()].filter(Boolean)
const usedResources = options.checkoutResources.filter(
item => readResourceUsageAmount(options.resourceUsage, item.key) > 0
)
if (usedResources.length) {
lines.push(
`额外消耗品:${usedResources
.map(item => {
const used = readResourceUsageAmount(options.resourceUsage, item.key)
const amount = roundMoney(used * item.unitPrice)
return `${item.label} ${used}/${item.quantity}${
isChargedResource(item) ? `,金额¥${money(amount)}` : ',赠送不扣款'
}`
})
.join('')}`
)
}
if (Number(options.coinConsumedM || 0) > 0) {
lines.push(
`哈夫币消耗:${quantity(options.coinConsumedM)}M,预计剩余${quantity(
options.remainingHafCoinM
)}M`
)
}
if (lines.length === 0) {
lines.push('租客发起结账。')
}
return lines.join('\n')
}
export function hydrateCounterFormFromCheckout(checkout: Checkout): CounterFormSnapshot {
return {
consumableAmountYuan: amountYuan(checkout.consumable_amount_cent),
coin_consumed_m: checkout.coin_consumed_m,
otherAmountYuan: amountYuan(checkout.other_amount_cent),
depositDeductAmountYuan: amountYuan(checkout.deposit_deduct_amount_cent),
reason: '',
evidenceText: '',
}
}
export function calculateCounterTotal(counterForm: {
@@ -82,7 +196,7 @@ export function calculateCounterTotal(counterForm: {
coin_consumed_m: number
otherAmountYuan: number
depositDeductAmountYuan: number
}): number {
}) {
return roundMoney(
counterForm.consumableAmountYuan +
counterForm.coin_consumed_m +
@@ -91,57 +205,90 @@ export function calculateCounterTotal(counterForm: {
)
}
export function hydrateResourceUsageFromOrder(
order: Order | null,
resources: SnapshotResource[]
): Record<string, number> {
const usage: Record<string, number> = {}
if (!order?.checkout_info) {
return usage
}
try {
const info = JSON.parse(order.checkout_info) as Record<string, any>
const consumedResources = info.consumed_resources as Record<string, number> | undefined
if (consumedResources) {
resources.forEach(res => {
if (res.key in consumedResources) {
usage[res.key] = consumedResources[res.key] ?? 0
}
})
}
} catch {
// ignore
}
return usage
export function getSnapshotHafCoinM(order: Order | null) {
const snapshot = readSnapshot(order)
return roundQuantity(readNumber(snapshot?.haf_coin_amount) / 1000000)
}
export function hydrateCounterFormFromOrder(order: Order | null) {
if (!order?.counter_info) {
return null
}
try {
const info = JSON.parse(order.counter_info) as Record<string, any>
return {
consumableAmountYuan: readNumber(info.consumableAmountYuan),
coin_consumed_m: readNumber(info.coin_consumed_m),
otherAmountYuan: readNumber(info.otherAmountYuan),
depositDeductAmountYuan: readNumber(info.depositDeductAmountYuan),
reason: String(info.reason || ''),
evidenceText: String(info.evidence || ''),
}
} catch {
return null
}
export function orderEstimatedEndAt(order: Order | null) {
const rentedAt = order?.rented_at
const durationHours = Number(order?.estimated_duration_hours || 0)
if (!rentedAt || durationHours <= 0) return undefined
return new Date(new Date(rentedAt).getTime() + durationHours * 60 * 60 * 1000).toISOString()
}
export function readError(error: unknown, fallback: string): string {
if (error && typeof error === 'object' && 'message' in error) {
return String(error.message)
export function orderRentAmount(item: Order, userID: number | undefined | null) {
if (item.owner_id === userID) return amountYuan(item.owner_rent_amount_cent)
if (item.renter_id === userID) return amountYuan(item.rent_amount_cent)
return amountYuan(item.display_amount_cent)
}
export function ownerActualIncome(item: Order, userID: number | undefined | null) {
if (item.owner_id !== userID) return null
const value = item.checkout?.owner_income_amount_cent
return typeof value === 'number' ? centToYuan(value) : null
}
export function formatHandoffRecordType(type: string) {
const typeMap: Record<string, string> = {
owner_handoff: '卖家交接',
renter_checkout: '买家结账',
owner_counter_checkout: '卖家反驳结账',
renter_confirm_checkout: '买家确认结账',
owner_accept_checkout: '卖家接受结账',
admin_arbitration: '客服仲裁',
}
return fallback
return typeMap[type] || type
}
export function linesToList(value: string) {
return value
.split('\n')
.map(item => item.trim())
.filter(Boolean)
}
export function money(value: unknown) {
return formatMoney(readNumber(value))
}
export function amountYuan(cent: unknown) {
if (cent !== undefined && cent !== null) return centToYuan(readNumber(cent))
return 0
}
export function quantity(value: unknown) {
const rounded = roundQuantity(readNumber(value))
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
}
export function readUnitPrice(priceText: string) {
const normalized = priceText.replace(//g, ',').trim()
const fractionMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元\s*\/\s*(\d+(?:\.\d+)?)/)
if (fractionMatch) {
const amount = Number(fractionMatch[1])
const count = Number(fractionMatch[2])
return count > 0 ? roundMoney(amount / count) : 0
}
const singleMatch = normalized.match(/(\d+(?:\.\d+)?)\s*元/)
if (singleMatch) return Number(singleMatch[1])
const fallback = normalized.match(/(\d+(?:\.\d+)?)/)
return fallback ? Number(fallback[1]) : 0
}
export function roundMoney(value: number) {
return Math.round(Number(value || 0) * 10) / 10
}
export function roundQuantity(value: number) {
return Math.round(value * 100) / 100
}
export function readNumber(value: unknown) {
const number = Number(value || 0)
return Number.isFinite(number) ? number : 0
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}