Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6355ca21b0 | ||
|
|
12e9b6d0b2 |
@@ -288,7 +288,7 @@ func awardOrderSide(tx *gorm.DB, order *model.RentalOrder, cfg Config, role stri
|
|||||||
setOrderPointsSnapshot(order, role, existing.BasisAmountCent, existing.PointsPerYuan, existing.PointsMultiplierBps, existing.Points, existing.CreatedAt)
|
setOrderPointsSnapshot(order, role, existing.BasisAmountCent, existing.PointsPerYuan, existing.PointsMultiplierBps, existing.Points, existing.CreatedAt)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if role == "renter" {
|
if role == "renter" {
|
||||||
|
|||||||
@@ -37,7 +37,12 @@ import {
|
|||||||
} from '@/features/seller/composables/usePublishDraft'
|
} from '@/features/seller/composables/usePublishDraft'
|
||||||
import type { PublishForm } from '@/shared/types/publish'
|
import type { PublishForm } from '@/shared/types/publish'
|
||||||
import { yuanToCent } from '@/shared/utils/money'
|
import { yuanToCent } from '@/shared/utils/money'
|
||||||
import { dailyLossOptions, formatNumber, roundRatio } from '@/shared/utils/pricing'
|
import {
|
||||||
|
calculateInsuranceFeeAmount,
|
||||||
|
dailyLossOptions,
|
||||||
|
formatNumber,
|
||||||
|
roundRatio,
|
||||||
|
} from '@/shared/utils/pricing'
|
||||||
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
import { useDistributionSiteStore } from '@/stores/distributionSite'
|
||||||
|
|
||||||
const draftSaveDelay = 400
|
const draftSaveDelay = 400
|
||||||
@@ -113,6 +118,14 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
receiveCostDelta,
|
receiveCostDelta,
|
||||||
isQualityOwner,
|
isQualityOwner,
|
||||||
})
|
})
|
||||||
|
const calculatedInsuranceFee = computed(() =>
|
||||||
|
form.compensation_plan === 'insurance'
|
||||||
|
? calculateInsuranceFeeAmount(pricing.calculatedSellerPrice.value, insuranceFeeBps.value)
|
||||||
|
: 0
|
||||||
|
)
|
||||||
|
const calculatedTakeHomePrice = computed(() =>
|
||||||
|
Math.max(pricing.calculatedSellerPrice.value - calculatedInsuranceFee.value, 0)
|
||||||
|
)
|
||||||
const qualityOwnerSaleRatioSelected = computed(
|
const qualityOwnerSaleRatioSelected = computed(
|
||||||
() =>
|
() =>
|
||||||
isQualityOwner.value &&
|
isQualityOwner.value &&
|
||||||
@@ -1033,6 +1046,8 @@ export function usePublishForm(options: UsePublishFormOptions) {
|
|||||||
isQualityOwnerExclusiveRatioActive,
|
isQualityOwnerExclusiveRatioActive,
|
||||||
insuranceFeeBps,
|
insuranceFeeBps,
|
||||||
insuranceFeePercent,
|
insuranceFeePercent,
|
||||||
|
calculatedInsuranceFee,
|
||||||
|
calculatedTakeHomePrice,
|
||||||
virtualAssetSaleAgreementChecked,
|
virtualAssetSaleAgreementChecked,
|
||||||
sellerAgreementChecked,
|
sellerAgreementChecked,
|
||||||
passwordAndDeviceConfirmed,
|
passwordAndDeviceConfirmed,
|
||||||
|
|||||||
@@ -1109,6 +1109,56 @@
|
|||||||
background: #faf5ff;
|
background: #faf5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.take-home-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview > div {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview span {
|
||||||
|
color: #858c96;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview strong {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: #20242a;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview strong.is-deduction {
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net span {
|
||||||
|
color: #9a3412;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net strong {
|
||||||
|
color: #f97316;
|
||||||
|
font-size: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
.coverage-note {
|
.coverage-note {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #858c96;
|
color: #858c96;
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ const {
|
|||||||
qualityOwnerSaleRatio,
|
qualityOwnerSaleRatio,
|
||||||
minAcceleratedSaleRatio,
|
minAcceleratedSaleRatio,
|
||||||
insuranceFeePercent,
|
insuranceFeePercent,
|
||||||
|
calculatedInsuranceFee,
|
||||||
|
calculatedTakeHomePrice,
|
||||||
showBanRecordRiskHint,
|
showBanRecordRiskHint,
|
||||||
unlockDemulanEnabled,
|
unlockDemulanEnabled,
|
||||||
nextSeasonPassEnabled,
|
nextSeasonPassEnabled,
|
||||||
@@ -937,6 +939,32 @@ const screenshotGuides: Record<string, string> = {
|
|||||||
</em>
|
</em>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="take-home-preview"
|
||||||
|
:class="{ 'has-insurance': form.compensation_plan === 'insurance' }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span>卖家发布价格</span>
|
||||||
|
<strong>¥{{ formatMoney(calculatedSellerPrice) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span v-if="form.compensation_plan === 'insurance'">
|
||||||
|
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)
|
||||||
|
</span>
|
||||||
|
<span v-else>普通赔付</span>
|
||||||
|
<strong :class="{ 'is-deduction': form.compensation_plan === 'insurance' }">
|
||||||
|
{{
|
||||||
|
form.compensation_plan === 'insurance'
|
||||||
|
? `-¥${formatMoney(calculatedInsuranceFee)}`
|
||||||
|
: '¥0.0'
|
||||||
|
}}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="take-home-net">
|
||||||
|
<span>预计到手价格</span>
|
||||||
|
<strong>¥{{ formatMoney(calculatedTakeHomePrice) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="coverage-note">
|
<p class="coverage-note">
|
||||||
无忧租号保险在普通赔付基础上增加账号市场价值保障,服务费按实际到手租金扣除。
|
无忧租号保险在普通赔付基础上增加账号市场价值保障,服务费按实际到手租金扣除。
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -923,6 +923,10 @@
|
|||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-breakdown strong.is-deduction {
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
.summary-list {
|
.summary-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -1256,6 +1260,56 @@
|
|||||||
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.08);
|
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.take-home-preview {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview > div {
|
||||||
|
display: grid;
|
||||||
|
align-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview span {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview strong {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview strong.is-deduction {
|
||||||
|
color: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net {
|
||||||
|
background: #fff7ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net span {
|
||||||
|
color: #9a3412;
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net strong {
|
||||||
|
color: var(--color-orange-primary);
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
.coverage-note {
|
.coverage-note {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #7b8492;
|
color: #7b8492;
|
||||||
@@ -1350,4 +1404,12 @@
|
|||||||
.coverage-option-grid {
|
.coverage-option-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.take-home-preview {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.take-home-preview .take-home-net {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ const {
|
|||||||
qualityOwnerSaleRatio,
|
qualityOwnerSaleRatio,
|
||||||
minAcceleratedSaleRatio,
|
minAcceleratedSaleRatio,
|
||||||
insuranceFeePercent,
|
insuranceFeePercent,
|
||||||
|
calculatedInsuranceFee,
|
||||||
|
calculatedTakeHomePrice,
|
||||||
virtualAssetSaleAgreementChecked,
|
virtualAssetSaleAgreementChecked,
|
||||||
sellerAgreementChecked,
|
sellerAgreementChecked,
|
||||||
passwordAndDeviceConfirmed,
|
passwordAndDeviceConfirmed,
|
||||||
@@ -766,6 +768,32 @@ function selectDailyLoss(value: string | number) {
|
|||||||
</em>
|
</em>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="take-home-preview"
|
||||||
|
:class="{ 'has-insurance': form.compensation_plan === 'insurance' }"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span>卖家发布价格</span>
|
||||||
|
<strong>¥{{ formatMoney(calculatedSellerPrice) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span v-if="form.compensation_plan === 'insurance'">
|
||||||
|
无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)
|
||||||
|
</span>
|
||||||
|
<span v-else>普通赔付</span>
|
||||||
|
<strong :class="{ 'is-deduction': form.compensation_plan === 'insurance' }">
|
||||||
|
{{
|
||||||
|
form.compensation_plan === 'insurance'
|
||||||
|
? `-¥${formatMoney(calculatedInsuranceFee)}`
|
||||||
|
: '¥0.0'
|
||||||
|
}}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
<div class="take-home-net">
|
||||||
|
<span>预计到手价格</span>
|
||||||
|
<strong>¥{{ formatMoney(calculatedTakeHomePrice) }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p class="coverage-note">
|
<p class="coverage-note">
|
||||||
普通赔付覆盖封禁、追回、洗号等情况;无忧租号保险在此基础上增加账号市场价值保障。
|
普通赔付覆盖封禁、追回、洗号等情况;无忧租号保险在此基础上增加账号市场价值保障。
|
||||||
</p>
|
</p>
|
||||||
@@ -786,13 +814,17 @@ function selectDailyLoss(value: string | number) {
|
|||||||
<aside class="summary-column">
|
<aside class="summary-column">
|
||||||
<div class="summary-panel">
|
<div class="summary-panel">
|
||||||
<div class="summary-main">
|
<div class="summary-main">
|
||||||
<span>卖家发布价格</span>
|
<span>预计到手价格</span>
|
||||||
<strong>{{
|
<strong>{{
|
||||||
calculatedSellerPrice ? `¥${formatMoney(calculatedSellerPrice)}` : '--'
|
calculatedTakeHomePrice ? `¥${formatMoney(calculatedTakeHomePrice)}` : '--'
|
||||||
}}</strong>
|
}}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-breakdown">
|
<div class="summary-breakdown">
|
||||||
<div class="summary-breakdown-title">价格明细</div>
|
<div class="summary-breakdown-title">价格明细</div>
|
||||||
|
<div>
|
||||||
|
<span>卖家发布价格</span>
|
||||||
|
<strong>¥{{ formatMoney(calculatedSellerPrice) }}</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>纯币价格</span>
|
<span>纯币价格</span>
|
||||||
<strong>{{
|
<strong>{{
|
||||||
@@ -803,6 +835,10 @@ function selectDailyLoss(value: string | number) {
|
|||||||
<span>额外物品价格</span>
|
<span>额外物品价格</span>
|
||||||
<strong>¥{{ formatMoney(calculatedConsumablePrice) }}</strong>
|
<strong>¥{{ formatMoney(calculatedConsumablePrice) }}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="form.compensation_plan === 'insurance'">
|
||||||
|
<span>无忧租号保险({{ insuranceFeePercent.toFixed(1) }}%)</span>
|
||||||
|
<strong class="is-deduction">-¥{{ formatMoney(calculatedInsuranceFee) }}</strong>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>押金价格</span>
|
<span>押金价格</span>
|
||||||
<strong>{{
|
<strong>{{
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
import { calculateInsuranceFeeAmount } from '../pricing'
|
||||||
|
|
||||||
|
describe('calculateInsuranceFeeAmount', () => {
|
||||||
|
it('普通赔付不收取保险服务费', () => {
|
||||||
|
expect(calculateInsuranceFeeAmount(238.1, 0)).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('按普通保险费率计算并四舍五入到分', () => {
|
||||||
|
expect(calculateInsuranceFeeAmount(238.1, 800)).toBe(19.05)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('按极品专区专享费率计算', () => {
|
||||||
|
expect(calculateInsuranceFeeAmount(238.1, 400)).toBe(9.52)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('保险服务费不超过卖家租金', () => {
|
||||||
|
expect(calculateInsuranceFeeAmount(1, 10_000)).toBe(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -26,6 +26,13 @@ export function roundMoney(value: number) {
|
|||||||
return Math.round(value * 10) / 10
|
return Math.round(value * 10) / 10
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateInsuranceFeeAmount(ownerIncomeYuan: number, feeBps: number) {
|
||||||
|
const ownerIncomeCent = Math.max(Math.round(Number(ownerIncomeYuan || 0) * 100), 0)
|
||||||
|
const safeFeeBps = Math.max(Math.round(Number(feeBps || 0)), 0)
|
||||||
|
if (ownerIncomeCent <= 0 || safeFeeBps <= 0) return 0
|
||||||
|
return Math.min(Math.round((ownerIncomeCent * safeFeeBps) / 10_000), ownerIncomeCent) / 100
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将比例四舍五入到一位小数
|
* 将比例四舍五入到一位小数
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user