优化订单结算金额展示和钱包流水号
This commit is contained in:
@@ -698,7 +698,11 @@ func (r *Repository) ListForUser(userID uint64) ([]OrderDTO, error) {
|
||||
}
|
||||
items := make([]OrderDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toDTOForUser(userID))
|
||||
dto := row.toDTOForUser(userID)
|
||||
if shouldAttachCheckout(row.Status) {
|
||||
dto.Checkout = r.latestCheckoutDTOForUser(row.ID, userID, row.RentalOrder)
|
||||
}
|
||||
items = append(items, dto)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -1312,6 +1316,15 @@ func (r *Repository) latestCheckoutAdminDTO(orderID uint64) *CheckoutDTO {
|
||||
return &dto
|
||||
}
|
||||
|
||||
func shouldAttachCheckout(status string) bool {
|
||||
switch status {
|
||||
case "pending_checkout_confirm", "pending_checkout_accept", "checkout_disputing", "completed":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -2,7 +2,6 @@ package wallet
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
@@ -290,9 +289,13 @@ func toLedgerDTO(row model.WalletLedger) LedgerDTO {
|
||||
}
|
||||
|
||||
func newLedgerNo() (string, error) {
|
||||
buf := make([]byte, 4)
|
||||
// 生成格式:WL + YYYYMMDDHHMMSS + 毫秒 + 4位随机数
|
||||
// 例如:WL202606051234561230456,便于用户和客服识别。
|
||||
now := time.Now()
|
||||
buf := make([]byte, 2)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("WL%d%s", time.Now().UnixNano(), hex.EncodeToString(buf)), nil
|
||||
randomNum := (int(buf[0])<<8 | int(buf[1])) % 10000
|
||||
return fmt.Sprintf("WL%s%03d%04d", now.Format("20060102150405"), now.Nanosecond()/1_000_000, randomNum), nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
@@ -29,3 +30,14 @@ func TestApplyEntryRoundsMoneyBeforeComparing(t *testing.T) {
|
||||
t.Fatalf("FrozenBalance = %v, want 0", account.FrozenBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLedgerNoUsesReadableFormat(t *testing.T) {
|
||||
ledgerNo, err := newLedgerNo()
|
||||
if err != nil {
|
||||
t.Fatalf("newLedgerNo() error = %v", err)
|
||||
}
|
||||
matched := regexp.MustCompile(`^WL\d{21}$`).MatchString(ledgerNo)
|
||||
if !matched {
|
||||
t.Fatalf("ledgerNo = %q, want WL + 21 digits", ledgerNo)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,10 @@ 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 orderAmountLabel = computed(() => (isOwner.value ? "预计租金" : "支付租金"));
|
||||
const orderRentDisplayAmount = computed(() => (order.value ? orderRentAmount(order.value) : 0));
|
||||
const ownerIncomeDisplayAmount = computed(() => (order.value ? ownerActualIncome(order.value) : null));
|
||||
const ownerIncomeLabel = computed(() => (order.value?.status === "completed" ? "实际到手" : "结账预计到手"));
|
||||
const canOpenDispute = computed(() => {
|
||||
if (!order.value || (!isOwner.value && !isRenter.value)) return false;
|
||||
return !["completed", "cancelled", "closed", "disputing", "checkout_disputing", "abnormal"].includes(order.value.status);
|
||||
@@ -510,6 +513,18 @@ function money(value: unknown) {
|
||||
return `${roundMoney(readNumber(value))}`;
|
||||
}
|
||||
|
||||
function orderRentAmount(item: Order) {
|
||||
if (item.owner_id === session.userId) return Number(item.owner_rent_amount ?? item.display_amount ?? 0);
|
||||
if (item.renter_id === session.userId) return Number(item.rent_amount ?? item.display_amount ?? 0);
|
||||
return Number(item.display_amount ?? 0);
|
||||
}
|
||||
|
||||
function ownerActualIncome(item: Order) {
|
||||
if (item.owner_id !== session.userId) return null;
|
||||
const value = item.checkout?.owner_income_amount;
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
||||
|
||||
function quantity(value: unknown) {
|
||||
const rounded = roundQuantity(readNumber(value));
|
||||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2);
|
||||
@@ -593,7 +608,11 @@ function getStatusTagType(status: string) {
|
||||
<div class="order-meta-grid">
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">{{ orderAmountLabel }}</span>
|
||||
<strong class="meta-value accent">¥{{ money(order.display_amount) }}</strong>
|
||||
<strong class="meta-value accent">¥{{ money(orderRentDisplayAmount) }}</strong>
|
||||
</div>
|
||||
<div v-if="isOwner && ownerIncomeDisplayAmount !== null" class="meta-item">
|
||||
<span class="meta-label">{{ ownerIncomeLabel }}</span>
|
||||
<strong class="meta-value income">¥{{ money(ownerIncomeDisplayAmount) }}</strong>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<span class="meta-label">押金</span>
|
||||
@@ -1224,6 +1243,11 @@ function getStatusTagType(status: string) {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta-value.income {
|
||||
color: #16a34a;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.action-card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,25 @@ function readError(error: unknown, fallback: string) {
|
||||
function money(value: unknown) {
|
||||
return Math.round(Number(value || 0));
|
||||
}
|
||||
|
||||
function isOwner(order: Order) {
|
||||
return order.owner_id === session.userId;
|
||||
}
|
||||
|
||||
function amountLabel(order: Order) {
|
||||
return isOwner(order) ? "预计租金" : "支付租金";
|
||||
}
|
||||
|
||||
function orderRentAmount(order: Order) {
|
||||
if (isOwner(order)) return Number(order.owner_rent_amount ?? order.display_amount ?? 0);
|
||||
return Number(order.rent_amount ?? order.display_amount ?? 0);
|
||||
}
|
||||
|
||||
function ownerActualIncome(order: Order) {
|
||||
if (!isOwner(order)) return null;
|
||||
const value = order.checkout?.owner_income_amount;
|
||||
return typeof value === "number" ? value : null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -187,8 +206,9 @@ function money(value: unknown) {
|
||||
|
||||
<div class="price-row">
|
||||
<div class="price-item">
|
||||
<span class="price-label">我的金额</span>
|
||||
<span class="price-val">¥{{ money(order.display_amount) }}</span>
|
||||
<span class="price-label">{{ amountLabel(order) }}</span>
|
||||
<span class="price-val">¥{{ money(orderRentAmount(order)) }}</span>
|
||||
<span v-if="ownerActualIncome(order) !== null" class="price-sub">实际到手 ¥{{ money(ownerActualIncome(order)) }}</span>
|
||||
</div>
|
||||
<div class="price-item">
|
||||
<span class="price-label">押金金额</span>
|
||||
@@ -419,6 +439,12 @@ function money(value: unknown) {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.price-sub {
|
||||
color: #10b981;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ========== 卡片底部 ========== */
|
||||
.card-footer {
|
||||
display: flex;
|
||||
|
||||
@@ -77,7 +77,10 @@ let autoPayHandled = 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 orderAmountLabel = computed(() => (isOwner.value ? '预计租金' : '支付租金'))
|
||||
const orderRentDisplayAmount = computed(() => (order.value ? orderRentAmount(order.value) : 0))
|
||||
const ownerIncomeDisplayAmount = computed(() => (order.value ? ownerActualIncome(order.value) : null))
|
||||
const ownerIncomeLabel = computed(() => (order.value?.status === 'completed' ? '实际到手' : '结账预计到手'))
|
||||
const canOpenDispute = computed(() => {
|
||||
if (!order.value || (!isOwner.value && !isRenter.value)) return false
|
||||
return !['completed', 'cancelled', 'closed', 'disputing', 'checkout_disputing', 'abnormal'].includes(order.value.status)
|
||||
@@ -567,6 +570,18 @@ function money(value: unknown) {
|
||||
return `${roundMoney(readNumber(value))}`
|
||||
}
|
||||
|
||||
function orderRentAmount(item: Order) {
|
||||
if (item.owner_id === session.userId) return Number(item.owner_rent_amount ?? item.display_amount ?? 0)
|
||||
if (item.renter_id === session.userId) return Number(item.rent_amount ?? item.display_amount ?? 0)
|
||||
return Number(item.display_amount ?? 0)
|
||||
}
|
||||
|
||||
function ownerActualIncome(item: Order) {
|
||||
if (item.owner_id !== session.userId) return null
|
||||
const value = item.checkout?.owner_income_amount
|
||||
return typeof value === 'number' ? value : null
|
||||
}
|
||||
|
||||
function quantity(value: unknown) {
|
||||
const rounded = roundQuantity(readNumber(value))
|
||||
return Number.isInteger(rounded) ? `${rounded}` : rounded.toFixed(2)
|
||||
@@ -635,7 +650,11 @@ function linesToList(value: string) {
|
||||
</div>
|
||||
<div class="metric-card highlight">
|
||||
<span class="metric-label">{{ orderAmountLabel }}</span>
|
||||
<strong class="metric-value amount">¥{{ money(order.display_amount) }}</strong>
|
||||
<strong class="metric-value amount">¥{{ money(orderRentDisplayAmount) }}</strong>
|
||||
</div>
|
||||
<div v-if="isOwner && ownerIncomeDisplayAmount !== null" class="metric-card income">
|
||||
<span class="metric-label">{{ ownerIncomeLabel }}</span>
|
||||
<strong class="metric-value amount">¥{{ money(ownerIncomeDisplayAmount) }}</strong>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<span class="metric-label">押金</span>
|
||||
@@ -1028,6 +1047,19 @@ function linesToList(value: string) {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.metric-card.income {
|
||||
background: #f0fdf4;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
|
||||
.metric-card.income .metric-label {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.metric-card.income .metric-value {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
|
||||
@@ -117,7 +117,19 @@ function isOwner(order: Order) {
|
||||
}
|
||||
|
||||
function amountLabel(order: Order) {
|
||||
return isRenter(order) ? '支付金额' : '租金收入'
|
||||
return isRenter(order) ? '支付租金' : '预计租金'
|
||||
}
|
||||
|
||||
function orderRentAmount(order: Order) {
|
||||
if (isOwner(order)) return Number(order.owner_rent_amount ?? order.display_amount ?? 0)
|
||||
if (isRenter(order)) return Number(order.rent_amount ?? order.display_amount ?? 0)
|
||||
return Number(order.display_amount ?? 0)
|
||||
}
|
||||
|
||||
function ownerActualIncome(order: Order) {
|
||||
if (!isOwner(order)) return null
|
||||
const value = order.checkout?.owner_income_amount
|
||||
return typeof value === 'number' ? value : null
|
||||
}
|
||||
|
||||
function money(value: unknown) {
|
||||
@@ -200,11 +212,12 @@ function getCountdownMinutes(order: Order) {
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="账号" min-width="180" />
|
||||
<el-table-column label="金额" width="100">
|
||||
<el-table-column label="金额" width="132">
|
||||
<template #default="{ row }">
|
||||
<div class="amount-cell">
|
||||
<span class="amount-value">¥{{ money(row.display_amount) }}</span>
|
||||
<span class="amount-value">¥{{ money(orderRentAmount(row)) }}</span>
|
||||
<span class="amount-label">{{ amountLabel(row) }}</span>
|
||||
<span v-if="ownerActualIncome(row) !== null" class="amount-sub">实际到手 ¥{{ money(ownerActualIncome(row)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -269,7 +282,11 @@ function getCountdownMinutes(order: Order) {
|
||||
<div class="card-meta">
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">{{ amountLabel(order) }}</span>
|
||||
<span class="meta-value amount">¥{{ money(order.display_amount) }}</span>
|
||||
<span class="meta-value amount">¥{{ money(orderRentAmount(order)) }}</span>
|
||||
</div>
|
||||
<div v-if="ownerActualIncome(order) !== null" class="meta-row">
|
||||
<span class="meta-label">实际到手</span>
|
||||
<span class="meta-value amount">¥{{ money(ownerActualIncome(order)) }}</span>
|
||||
</div>
|
||||
<div class="meta-row">
|
||||
<span class="meta-label">押金</span>
|
||||
@@ -411,6 +428,12 @@ function getCountdownMinutes(order: Order) {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.amount-sub {
|
||||
font-size: 12px;
|
||||
color: #10b981;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user