feat: 价格试算模块优化

This commit is contained in:
R524809
2026-08-27 10:35:07 +08:00
parent ac4ab22ac2
commit 7e1192f2ef
5 changed files with 120 additions and 105 deletions
+10 -7
View File
@@ -55,9 +55,12 @@
## 4. 02 价格试算(公式从 web/js/app.js 移植,单位口径不变)
输入参数(存 `product.pricing` JSON):`purchasePrice` 进货价¥(默认30)、`profitRate` 净利率%(默认100)、
`tdPrice` 贴单费¥(默认3)、`logisticsLevel` low/high/high2(默认 low)、`discountReserve` 预留折扣%(默认50,≤95)、
`tdPrice` 贴单费¥(默认3)、`logisticsLevel` low/high/high2(默认 low)、`lineMultiplier` 划线价倍数%(默认1000-500)、
`fxRate` 汇率(`product.fx_rate` 快照,无则 `GET /api/fx` 拉取)。重量/尺寸取自 01 区块。
> 划线价口径(v2.1 调整):原 v1 的「预留折扣空间」(划线价 = 销售价 ÷ (1-折扣%))在 >95% 区间发散且 ≥100% 无解,
> 改为「划线价倍数」:`划线价 = 销售价 × (1 + 倍数%)`。倍数 100% → 划线价 = 2× 销售价;50% → 1.5×。线性、全程可算。
**物流费**`baseLogisticsFee` 已在 `studio/src/pricing/pricing.ts`):
| 等级 | 条件 | 公式 | 规则文案 |
@@ -81,8 +84,8 @@
平台佣金 = 销售价 × 12%(low)/ 18%(其余) ← 展示用
平台总抽成 = 销售价 × (1 - 净得率) = 15.5% / 21.5% ← 含佣金及约3.5%其它费
完全成本 = 进货价 + 物流费 + 平台总抽成
预留后¥ = 销售价 / (1 - 预留折扣%);差额 = 预留后 - 销售价
销售价₽ = 销售价¥ × 汇率;预留₽ = 预留后¥ × 汇率
预留后¥ = 销售价 × (1 + 划线价倍数%)(即划线价);差额 = 划线价 - 销售价
销售价₽ = 销售价¥ × 汇率;划线₽ = 划线价¥ × 汇率
```
**校验**v2.1 新增移植到 `pricing.ts``validateDimensions` / `validateLogisticsLevelCny` / `validatePriceRange`,注意等级/区间建议用的销售价单位是 **CNY**,与 v1 一致):
@@ -99,14 +102,14 @@
// product.pricingPriceInfoPanel 字段的超集,向后兼容)
{
"purchasePrice": 30, "profitRate": 100, "logisticsLevel": "low", "tdPrice": 3,
"discountReserve": 50, "fxRate": 11.5, "weightG": 600, "dims": { "l": 20, "w": 15, "h": 10 },
"lineMultiplier": 100, "fxRate": 11.5, "weightG": 600, "dims": { "l": 20, "w": 15, "h": 10 },
"logisticsFee": 22.12, "receivedPrice": 60, "profitPrice": 30, "commission": 11.63,
"fullCommission": 15.02, "totalCost": 67.14,
"sellingPriceCny": 77.51, "reservedPriceCny": 155.02,
"sellingPriceRub": 891.4, "reservedPriceRub": 1782.7,
"sellingPriceCny": 77.51, "linePriceCny": 155.02,
"sellingPriceRub": 891.4, "linePriceRub": 1782.7,
"calculatedAt": "2026-08-26T12:00:00Z"
}
// 同时回填:price=销售价CNYold_price=预留后CNYcurrency_code=CNYfx_rate=汇率
// 同时回填:price=销售价CNYold_price=划线价CNYcurrency_code=CNYfx_rate=汇率
```
## 5. 03 俄文文案
+14 -14
View File
@@ -33,16 +33,16 @@ function dimsCm(product: ProductDetail): { l: number; w: number; h: number } {
}
/**
* 售价信息:售价/划线价(CNY)+ 定价参数(进货价/净利率/物流等级/汇率/预留折扣)。
* 售价信息:售价/划线价(CNY)+ 定价参数(进货价/净利率/物流等级/汇率/划线价倍数)。
* 重量、尺寸直接读「主要信息」的包装字段,不重复填写。
* 任一定价参数变化即重算并回填售价(= 销售价)与划线价(= 预留折扣前价格)。
* 任一定价参数变化即重算并回填售价(= 销售价)与划线价(= 销售价 × (1 + 倍数%))。
*/
export default function PriceInfoPanel({ product, onSave }: Props) {
const pricing = (product.pricing ?? {}) as PricingPayload;
const [purchasePrice, setPurchasePrice] = useState(pricing.purchasePrice ?? 30);
const [profitRate, setProfitRate] = useState(pricing.profitRate ?? 100);
const [level, setLevel] = useState<LogisticsLevel>((pricing.logisticsLevel as LogisticsLevel) ?? 'low');
const [reserve, setReserve] = useState(pricing.discountReserve ?? 50);
const [multiplier, setMultiplier] = useState(pricing.lineMultiplier ?? 100);
const [fxRate, setFxRate] = useState(product.fx_rate ?? pricing.fxRate ?? 0);
// 币种固定人民币:历史数据(默认 RUB)打开编辑页时纠正一次
@@ -67,7 +67,7 @@ export default function PriceInfoPanel({ product, onSave }: Props) {
const tdPrice = pricing.tdPrice ?? 3;
/** 用当前参数(可被 patch 覆盖)实时计算,不落库 */
const compute = (patch: Partial<{ purchasePrice: number; profitRate: number; level: LogisticsLevel; reserve: number; fxRate: number }> = {}) =>
const compute = (patch: Partial<{ purchasePrice: number; profitRate: number; level: LogisticsLevel; multiplier: number; fxRate: number }> = {}) =>
calculatePricing({
purchasePrice: patch.purchasePrice ?? purchasePrice,
profitRate: patch.profitRate ?? profitRate,
@@ -75,17 +75,17 @@ export default function PriceInfoPanel({ product, onSave }: Props) {
weightG: weightGrams(product),
dims: dimsCm(product),
tdPrice,
discountReserve: patch.reserve ?? reserve,
lineMultiplier: patch.multiplier ?? multiplier,
fxRate: patch.fxRate ?? fxRate,
});
/** 重算并落库:定价参数 + 售价/划线价一并保存 */
const recalc = (patch: Partial<{ purchasePrice: number; profitRate: number; level: LogisticsLevel; reserve: number; fxRate: number }> = {}) => {
const recalc = (patch: Partial<{ purchasePrice: number; profitRate: number; level: LogisticsLevel; multiplier: number; fxRate: number }> = {}) => {
const p = {
purchasePrice: patch.purchasePrice ?? purchasePrice,
profitRate: patch.profitRate ?? profitRate,
level: patch.level ?? level,
reserve: patch.reserve ?? reserve,
multiplier: patch.multiplier ?? multiplier,
fxRate: patch.fxRate ?? fxRate,
};
const r = compute(patch);
@@ -98,7 +98,7 @@ export default function PriceInfoPanel({ product, onSave }: Props) {
weightG: weightGrams(product),
dims: dimsCm(product),
tdPrice,
discountReserve: p.reserve,
lineMultiplier: p.multiplier,
fxRate: p.fxRate,
logisticsFee: r.logisticsFee,
fullCommission: r.fullCommission,
@@ -109,7 +109,7 @@ export default function PriceInfoPanel({ product, onSave }: Props) {
},
fx_rate: p.fxRate,
price: r.sellingPriceCny,
old_price: r.reservedPriceCny,
old_price: r.linePriceCny,
currency_code: 'CNY',
});
};
@@ -189,15 +189,15 @@ export default function PriceInfoPanel({ product, onSave }: Props) {
/>
</Col>
<Col span={5}>
<FieldLabel> %</FieldLabel>
<FieldLabel>线 %</FieldLabel>
<InputNumber
style={{ width: '100%' }}
min={0}
max={95}
value={reserve}
max={500}
value={multiplier}
onChange={(v) => {
setReserve(v ?? 0);
recalc({ reserve: v ?? 0 });
setMultiplier(v ?? 0);
recalc({ multiplier: v ?? 0 });
}}
/>
</Col>
+6 -6
View File
@@ -1,6 +1,6 @@
/**
* 05 入库与导出(docs/v2.1/trial-page.md §7):
* 入库即各区块自动落库;此处提供 本商品CSV / 批量CSV(16 列,对齐 v1 web)与 组合码(SKU 预留₽价)。
* 入库即各区块自动落库;此处提供 本商品CSV / 批量CSV(16 列,对齐 v1 web)与 组合码(SKU 划线₽价)。
* Phase A 由前端拼装(逐个拉详情),Phase D 可换 GET /api/export/trial-csv。
*/
import { useState } from 'react';
@@ -50,11 +50,11 @@ function buildRow(p: ProductDetail): Array<string | number | null> {
];
}
/** 组合码一行:`货号 预留₽价(两位)`,无预留价回退销售价₽ */
/** 组合码一行:`货号 划线₽价(两位)`,无划线价回退销售价₽ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function comboCode(p: ProductDetail): string {
const pr = (p.pricing ?? {}) as Record<string, any>;
const rub = pr.reservedPriceRub ?? pr.sellingPriceRub;
const rub = pr.linePriceRub ?? pr.reservedPriceRub ?? pr.sellingPriceRub;
if (!p.offer_id || rub == null) return '';
return `${p.offer_id} ${Number(rub).toFixed(2)}`;
}
@@ -127,7 +127,7 @@ export default function TrialExportPanel({ product }: Props) {
const copyCurrentCombo = async () => {
const code = comboCode(product);
if (!code) {
message.warning('缺少货号或计价结果(卢布预留价)');
message.warning('缺少货号或计价结果(卢布划线价)');
return;
}
const ok = await copyText(code);
@@ -141,7 +141,7 @@ export default function TrialExportPanel({ product }: Props) {
const list = await fetchAllPricedProducts();
const lines = list.map(comboCode).filter(Boolean);
if (lines.length === 0) {
message.warning('没有可用的组合码(需要货号 + 卢布预留价)');
message.warning('没有可用的组合码(需要货号 + 卢布划线价)');
return;
}
setComboText(lines.join('\n'));
@@ -188,7 +188,7 @@ export default function TrialExportPanel({ product }: Props) {
</Space>
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 12 }}>
`货号 预留₽价`退 Ozon
`货号 划线₽价`线退 Ozon
CSV 16 v1 UTF-8 BOM Excel
</Text>
+79 -68
View File
@@ -1,9 +1,10 @@
import { useEffect, useState, type CSSProperties } from 'react';
import { Alert, Button, Card, Col, InputNumber, Radio, Row, Space, Typography, message } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { Alert, Button, Card, Col, InputNumber, message, Radio, Row, Space, Typography } from 'antd';
import { CopyOutlined, ReloadOutlined } from '@ant-design/icons';
import { ProductDetail } from '@/services/product';
import { getFxRate } from '@/services/fx';
import { apiErrorMessage } from '@/services/api';
import { copyText } from '@/utils/file';
import {
calculatePricing,
logisticsFeeRule,
@@ -28,7 +29,7 @@ interface Props {
type PricingPayload = Record<string, any>;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type PricingState = Partial<{ purchasePrice: number; profitRate: number; tdPrice: number; level: LogisticsLevel; reserve: number; fxRate: number }>;
type PricingState = Partial<{ purchasePrice: number; profitRate: number; tdPrice: number; level: LogisticsLevel; multiplier: number; fxRate: number }>;
function weightGrams(product: ProductDetail): number {
const w = product.weight ?? 0;
@@ -55,22 +56,24 @@ function Metric({ label, value, rule, color }: { label: string; value: string; r
);
}
/** 销售价格四宫格:人民币/卢布 × 销售价/划线价,价格居中,划线价带删除线 */
/** 销售价格四宫格:人民币/卢布 × 销售价/划线价,价格居中,划线价带删除线;人民币销售价可一键复制 */
function SalePriceGrid({
cnySelling,
cnySellingNumber,
cnyLine,
rubSelling,
rubLine,
reserve,
multiplier,
fxRate,
cnyGap,
rubGap,
}: {
cnySelling: string;
cnySellingNumber?: string;
cnyLine: string;
rubSelling: string;
rubLine: string;
reserve: number;
multiplier: number;
fxRate: number;
cnyGap: number | null;
rubGap: number | null;
@@ -88,11 +91,37 @@ function SalePriceGrid({
lineHeight: '30px',
};
const noteStyle: CSSProperties = { fontSize: 11, color: 'rgba(0,0,0,0.35)' };
const multiple = (1 + multiplier / 100).toFixed(2);
const copyCnySelling = async () => {
if (!cnySellingNumber) return;
const ok = await copyText(cnySellingNumber);
ok ? message.success(`已复制:${cnySellingNumber}`) : message.error('复制失败');
};
return (
<div style={{ borderTop: '1px dashed #e5e7eb', paddingTop: 14, marginTop: 14 }}>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
<div style={cellStyle}>
<div style={{ fontSize: 13, fontWeight: 500 }}>-</div>
<div
style={{
fontSize: 13,
fontWeight: 500,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: 4,
}}
>
-
{cnySellingNumber && (
<CopyOutlined
title="复制人民币销售价"
style={{ color: 'rgba(0,0,0,0.45)', cursor: 'pointer' }}
onClick={copyCnySelling}
/>
)}
</div>
<div style={priceStyle}>{cnySelling}</div>
<div style={noteStyle}></div>
</div>
@@ -107,14 +136,16 @@ function SalePriceGrid({
<div style={{ fontSize: 13, fontWeight: 500 }}>-线</div>
<div style={{ ...priceStyle, textDecoration: 'line-through' }}>{cnyLine}</div>
<div style={noteStyle}>
{reserve}% {cnyGap != null ? ` · 差额 ¥ ${cnyGap.toFixed(2)}` : ''}
× {multiple}
{cnyGap != null ? ` · 差额 ¥ ${cnyGap.toFixed(2)}` : ''}
</div>
</div>
<div style={cellStyle}>
<div style={{ fontSize: 13, fontWeight: 500 }}>-线</div>
<div style={{ ...priceStyle, textDecoration: 'line-through' }}>{rubLine}</div>
<div style={noteStyle}>
{reserve}% {rubGap != null ? ` · 差额 ₽ ${rubGap.toFixed(0)}` : ''}
× {multiple}
{rubGap != null ? ` · 差额 ₽ ${rubGap.toFixed(0)}` : ''}
</div>
</div>
</div>
@@ -125,9 +156,10 @@ function SalePriceGrid({
/**
* 02 价格试算:公式与告警完整移植 v1 web/js/app.jstrial-page.md §4)。
* 布局:左侧操作项(进货价→物流等级),右侧展示项(物流费→销售价)。
* 划线价为「倍数」口径:划线价 = 销售价 × (1 + 倍数%),0-500% 线性可算(v2.1 调整)。
* 颜色对齐 v1:完全成本/平台总抽成红色,人民币/卢布销售价绿色。
* 参数变化即重算并落库(pricing JSON + price/old_price/fx_rate);
* 尺寸不合规时不落计价结果;预留折扣空间 ≥100% 时划线价无法计算(显示 --,只落销售价)
* 尺寸不合规时不落计价结果。
*/
export default function TrialPricingPanel({ product, onSave }: Props) {
const pricing = (product.pricing ?? {}) as PricingPayload;
@@ -135,7 +167,7 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
const [profitRate, setProfitRate] = useState(pricing.profitRate ?? 100);
const [tdPrice, setTdPrice] = useState(pricing.tdPrice ?? 3);
const [level, setLevel] = useState<LogisticsLevel>((pricing.logisticsLevel as LogisticsLevel) ?? 'low');
const [reserve, setReserve] = useState(pricing.discountReserve ?? 50);
const [multiplier, setMultiplier] = useState(pricing.lineMultiplier ?? 100);
const [fxRate, setFxRate] = useState(product.fx_rate ?? pricing.fxRate ?? 0);
const [fxSource, setFxSource] = useState('');
@@ -178,7 +210,7 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
weightG: weightGrams(product),
dims: dimsCm(product),
tdPrice: patch.tdPrice ?? tdPrice,
discountReserve: patch.reserve ?? reserve,
lineMultiplier: patch.multiplier ?? multiplier,
fxRate: patch.fxRate ?? fxRate,
});
@@ -189,7 +221,7 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
profitRate: patch.profitRate ?? profitRate,
tdPrice: patch.tdPrice ?? tdPrice,
level: patch.level ?? level,
reserve: patch.reserve ?? reserve,
multiplier: patch.multiplier ?? multiplier,
fxRate: patch.fxRate ?? fxRate,
};
const dims = dimsCm(product);
@@ -201,40 +233,9 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
weightG: weightGrams(product),
dims,
tdPrice: p.tdPrice,
discountReserve: p.reserve,
lineMultiplier: p.multiplier,
fxRate: p.fxRate,
});
// 预留折扣空间 ≥100% 时划线价无数学意义(除数 ≤0):只保存销售价,并清空划线价字段
const lineValid = p.reserve < 100;
const lineFields = r && lineValid
? {
logisticsFee: r.logisticsFee,
receivedPrice: r.receivedPrice,
profitPrice: r.profitPrice,
commission: r.commission,
fullCommission: r.fullCommission,
totalCost: r.totalCost,
sellingPriceCny: r.sellingPriceCny,
reservedPriceCny: r.reservedPriceCny,
sellingPriceRub: r.sellingPriceRub,
reservedPriceRub: r.reservedPriceRub,
calculatedAt: new Date().toISOString(),
}
: r
? {
logisticsFee: r.logisticsFee,
receivedPrice: r.receivedPrice,
profitPrice: r.profitPrice,
commission: r.commission,
fullCommission: r.fullCommission,
totalCost: r.totalCost,
sellingPriceCny: r.sellingPriceCny,
sellingPriceRub: r.sellingPriceRub,
reservedPriceCny: null,
reservedPriceRub: null,
calculatedAt: new Date().toISOString(),
}
: {};
onSave({
pricing: {
...pricing,
@@ -242,18 +243,28 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
profitRate: p.profitRate,
tdPrice: p.tdPrice,
logisticsLevel: p.level,
discountReserve: p.reserve,
lineMultiplier: p.multiplier,
fxRate: p.fxRate,
weightG: weightGrams(product),
dims,
...lineFields,
...(r
? {
logisticsFee: r.logisticsFee,
receivedPrice: r.receivedPrice,
profitPrice: r.profitPrice,
commission: r.commission,
fullCommission: r.fullCommission,
totalCost: r.totalCost,
sellingPriceCny: r.sellingPriceCny,
linePriceCny: r.linePriceCny,
sellingPriceRub: r.sellingPriceRub,
linePriceRub: r.linePriceRub,
calculatedAt: new Date().toISOString(),
}
: {}),
},
fx_rate: p.fxRate,
...(r && lineValid
? { price: r.sellingPriceCny, old_price: r.reservedPriceCny }
: r
? { price: r.sellingPriceCny, old_price: null }
: {}),
...(r ? { price: r.sellingPriceCny, old_price: r.linePriceCny } : {}),
currency_code: 'CNY',
});
};
@@ -262,8 +273,7 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
const preview = fxRate && !dimError ? compute() : null;
const levelHint = preview ? validateLogisticsLevelCny(preview.sellingPriceCny, level) : '';
const rangeHint = preview ? validatePriceRange(preview.sellingPriceCny) : '';
const lineValid = reserve < 100;
const discountGap = preview && lineValid ? preview.reservedPriceCny - preview.sellingPriceCny : 0;
const discountGap = preview ? preview.linePriceCny - preview.sellingPriceCny : 0;
const num = (v: number | undefined | null, digits = 2) => (v == null ? '--' : v.toFixed(digits));
@@ -314,17 +324,17 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
/>
</Col>
<Col span={12}>
<FieldLabel> %</FieldLabel>
<FieldLabel>线 %</FieldLabel>
<InputNumber
style={{ width: '100%' }}
min={0}
max={500}
step={5}
value={reserve}
value={multiplier}
onChange={(v) => {
const n = v ?? 0;
setReserve(n);
recalc({ reserve: n });
setMultiplier(n);
recalc({ multiplier: n });
}}
/>
</Col>
@@ -384,19 +394,19 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
{dimError && <Alert type="error" showIcon message="尺寸不符合物流要求" description={dimError} />}
<Text type="secondary" style={{ fontSize: 12, display: 'block', marginTop: 12 }}>
/
线 = × (1 + %)/
</Text>
</Col>
{/* ── 右:展示项(物流费 → 销售价) ── */}
<Col span={14}>
{(levelHint || rangeHint) && (
<div style={{ marginBottom: 12 }}>
<div style={{ marginTop: 12 }}>
{levelHint && <Alert type="warning" showIcon message={levelHint} style={{ marginBottom: 8 }} />}
{rangeHint && <Alert type="warning" showIcon message={rangeHint} />}
</div>
)}
</Col>
{/* ── 右:展示项(物流费 → 销售价) ── */}
<Col span={14}>
<Card size="small" style={{ background: '#fafafa' }}>
{/* 第一行:物流费 / 平台佣金 / 提现费及其他 */}
<Row gutter={[16, 20]}>
@@ -445,13 +455,14 @@ export default function TrialPricingPanel({ product, onSave }: Props) {
{/* 第三、四行:人民币/卢布 销售价 + 划线价 四宫格 */}
<SalePriceGrid
cnySelling={preview ? `¥ ${num(preview.sellingPriceCny)}` : '--'}
cnyLine={preview && lineValid ? `¥ ${num(preview.reservedPriceCny)}` : '--'}
cnySellingNumber={preview ? preview.sellingPriceCny.toFixed(2) : undefined}
cnyLine={preview ? `¥ ${num(preview.linePriceCny)}` : '--'}
rubSelling={preview ? `${num(preview.sellingPriceRub, 0)}` : '--'}
rubLine={preview && lineValid ? `${num(preview.reservedPriceRub, 0)}` : '--'}
reserve={reserve}
rubLine={preview ? `${num(preview.linePriceRub, 0)}` : '--'}
multiplier={multiplier}
fxRate={fxRate}
cnyGap={preview && lineValid ? discountGap : null}
rubGap={preview && lineValid ? preview.reservedPriceRub - preview.sellingPriceRub : null}
cnyGap={preview ? discountGap : null}
rubGap={preview ? preview.linePriceRub - preview.sellingPriceRub : null}
/>
</Card>
</Col>
+11 -10
View File
@@ -1,7 +1,8 @@
/**
* 计价纯函数 —— 从 v1 web/js/app.js 抄录(只抄不改,见 docs/v2/migration.md §3)。
* 输入:进货价/净利率/物流等级/重量/尺寸/贴单费/预留折扣/汇率
* 输出:物流费/佣金/完全成本/销售价(¥/₽)/预留折扣价
* v2.1 调整:划线价改为「倍数」口径(划线价 = 销售价 × (1 + 倍数%)),替代 v1 的 ÷(1-预留折扣)。
* 输入:进货价/净利率/物流等级/重量/尺寸/贴单费/划线价倍数/汇率
* 输出:物流费/佣金/完全成本/销售价(¥/₽)/划线价(¥/₽)
*/
export type LogisticsLevel = 'low' | 'high' | 'high2';
@@ -13,7 +14,7 @@ export interface PricingInput {
weightG: number; // 重量 g
dims: { l: number; w: number; h: number }; // cm
tdPrice: number; // 贴单费用 ¥
discountReserve: number; // 预留折扣 %
lineMultiplier: number; // 划线价倍数 %(划线价 = 销售价 × (1 + 倍数/100)
fxRate: number; // 汇率 CNY→RUB
}
@@ -27,8 +28,8 @@ export interface PricingResult {
commission: number; // 展示用平台佣金
totalCost: number; // 完全成本
sellingPriceRub: number; // 销售价 ₽
reservedPriceCny: number;// 预留折扣后 ¥
reservedPriceRub: number;// 预留折扣后
linePriceCny: number; // 划线价 ¥(销售价 × (1 + 倍数))
linePriceRub: number; // 划线价
}
/** 物流费(不含贴单费) */
@@ -44,7 +45,7 @@ export function baseLogisticsFee(weightG: number, level: LogisticsLevel): number
}
export function calculatePricing(input: PricingInput): PricingResult {
const { purchasePrice, profitRate, logisticsLevel, weightG, tdPrice, discountReserve, fxRate } = input;
const { purchasePrice, profitRate, logisticsLevel, weightG, tdPrice, lineMultiplier, fxRate } = input;
const logisticsFee = baseLogisticsFee(weightG, logisticsLevel) + tdPrice;
const profitDecimal = profitRate / 100;
@@ -57,8 +58,8 @@ export function calculatePricing(input: PricingInput): PricingResult {
const totalCost = purchasePrice + logisticsFee + fullCommission;
const sellingPriceRub = sellingPriceCny * fxRate;
const reservedPriceCny = sellingPriceCny / (1 - discountReserve / 100);
const reservedPriceRub = reservedPriceCny * fxRate;
const linePriceCny = sellingPriceCny * (1 + lineMultiplier / 100);
const linePriceRub = linePriceCny * fxRate;
return {
logisticsFee,
@@ -70,8 +71,8 @@ export function calculatePricing(input: PricingInput): PricingResult {
commission,
totalCost,
sellingPriceRub,
reservedPriceCny,
reservedPriceRub,
linePriceCny,
linePriceRub,
};
}