feat(billing): 计费商业化重构(积分制)+ 团队差异化调价
统一计价引擎 apps/billing/pricing.py:1积分=¥0.1,平台成本(¥)与用户价(积分)双记账, 视频按火山真实 usage.total_tokens 结算(true-up,终结¥1/段倒贴)+ 首发×1.5毛利系数。 Team.price_multiplier 差异化调价(jimeng同款):挂牌价×系数两步HALF_UP取整,视频类 按下单时的价格/汇率快照结算(中途改配不影响在途任务)。 BillingConfig 单例(汇率/毛利系数/预留buffer,admin可调即刻生效)。存量数据 ×10 rescale 迁移(RunPython+atomic,MySQL 迁移不可中断重跑)。开户赠送归零(DEFAULT_TRIAL_CREDITS=0, 商业决策)。audit_billing 加 I9(卖亏审计)。271 条测试 + tsc/build 全绿。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -121,21 +121,21 @@ export function AssetFactoryPage({ navigate }: { navigate: (page: Page) => void
|
||||
tag: "[ MODEL · TRY-ON ]",
|
||||
title: "模特上身图",
|
||||
desc: "选择模特,AI 生成商品模特上身效果图",
|
||||
cost: "≈ ¥0.30 / 张"
|
||||
cost: "≈ 20 积分 / 张"
|
||||
},
|
||||
{
|
||||
page: "platformCover" as Page,
|
||||
tag: "[ PLATFORM · KIT ]",
|
||||
title: "平台套图",
|
||||
desc: "选择平台模板,AI 生成电商平台套图",
|
||||
cost: "≈ ¥0.50 / 张"
|
||||
cost: "≈ 20 积分 / 张"
|
||||
},
|
||||
{
|
||||
page: "imageOptimize" as Page,
|
||||
tag: "[ IMAGE · STUDIO ]",
|
||||
title: "图片创作",
|
||||
desc: "自由创作 AI 图片,适用于详情图 / 海报 / 灵感速写",
|
||||
cost: "≈ ¥0.40 / 组"
|
||||
cost: "≈ 20 积分 / 张"
|
||||
}
|
||||
];
|
||||
|
||||
@@ -729,15 +729,23 @@ export function ImageWorkbenchPage({
|
||||
}
|
||||
|
||||
const imageModels = modelConfigs.filter((model) => model.capability.includes("image"));
|
||||
// 团队价格系数(差异化调价):预估所见即所扣;拉不到按标准价 1
|
||||
const [priceMultiplier, setPriceMultiplier] = useState(1);
|
||||
useEffect(() => {
|
||||
void api.billingConfig().then((cfg) => setPriceMultiplier(Number(cfg.team_price_multiplier) || 1)).catch(() => undefined);
|
||||
}, []);
|
||||
// 每张图实扣单价:取「当前选的生图模型」的 unit_price(火山/gpt-image),没匹配上用第一个图像模型,
|
||||
// 再兜底 ¥2(后端 estimate_cost=unit_price,默认模型 gpt-image-2=¥2)。前端预估据此算,和后端实扣一致(PMC#20)。
|
||||
// 再兜底 20 积分(后端 quote_flat=unit_price 积分/张,默认模型 gpt-image-2=20 积分)。前端预估据此算,和后端实扣一致(PMC#20)。
|
||||
const perImagePrice = (() => {
|
||||
const want = genModel === "gpt-image" ? "gpt-image" : genModel === "volcano" ? "seedream" : genModel;
|
||||
const m = imageModels.find((x) => x.name.toLowerCase().includes(want)) || imageModels[0];
|
||||
const p = Number(m?.unit_price);
|
||||
return Number.isFinite(p) && p > 0 ? p : 2;
|
||||
return Number.isFinite(p) && p > 0 ? p : 20;
|
||||
})();
|
||||
const estimateFee = (n: number) => `¥${(n * perImagePrice).toFixed(2)}`;
|
||||
// 与后端逐张对齐:每张 = 挂牌单价取整 × 系数 → HALF_UP 最低 1,总价 = 张数 × 单张
|
||||
// (不能先乘张数再取整:0.85 系数 × 3 张会比后端逐张各取整少 1-2 积分,review 确认)
|
||||
const perImageFinal = Math.max(1, Math.round(Number((Math.round(perImagePrice) * priceMultiplier).toFixed(6))));
|
||||
const estimateFee = (n: number) => `${n * perImageFinal} 积分`;
|
||||
|
||||
/* 模特卡数据来源:期2 改为「模特库」(顶级实体,引用其形象图当上身图参考)。
|
||||
映射 ModelEntity→Asset 形:id=形象图资产 id(选中即作 model_id 参考图),metadata 记 model_entity_id 溯源。
|
||||
@@ -2477,8 +2485,8 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
|
||||
<div className="dm-form-cta">
|
||||
<div className="dm-cost">
|
||||
<span>预估扣费 <span className="v">≈ ¥1.20</span></span>
|
||||
<span>余额 ¥327.40</span>
|
||||
<span>预估扣费 <span className="v">≈ 20 积分/张</span></span>
|
||||
<span>余额以消费页为准</span>
|
||||
</div>
|
||||
<button className="dm-gen" type="button" onClick={toRealTool}>
|
||||
<Sparkles size={15} />
|
||||
@@ -2502,7 +2510,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="nm">Ava × 4 张</div>
|
||||
<div className="info">
|
||||
{active.title} <span className="sep">·</span> 3:4 <span className="sep">·</span> 3 分钟前{" "}
|
||||
<span className="sep">·</span> ¥1.20
|
||||
<span className="sep">·</span> 20 积分/张
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
@@ -2527,7 +2535,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="nm">Zoe × 4 张</div>
|
||||
<div className="info">
|
||||
{active.title} <span className="sep">·</span> 3:4 <span className="sep">·</span> 12 分钟前{" "}
|
||||
<span className="sep">·</span> ¥1.20
|
||||
<span className="sep">·</span> 20 积分/张
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
@@ -2624,7 +2632,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="info">
|
||||
<span>3:4</span><span className="sep">·</span>
|
||||
<span>3 分钟前</span><span className="sep">·</span>
|
||||
<span>¥1.20</span>
|
||||
<span>20 积分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
@@ -2647,7 +2655,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="info">
|
||||
<span>3:4</span><span className="sep">·</span>
|
||||
<span>12 分钟前</span><span className="sep">·</span>
|
||||
<span>¥1.20</span>
|
||||
<span>20 积分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
@@ -2670,7 +2678,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="info">
|
||||
<span>3:4</span><span className="sep">·</span>
|
||||
<span>刚刚</span><span className="sep">·</span>
|
||||
<span>¥0.60</span>
|
||||
<span>20 积分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
@@ -2697,7 +2705,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<div className="info">
|
||||
<span>3:4</span><span className="sep">·</span>
|
||||
<span>昨天 18:24</span><span className="sep">·</span>
|
||||
<span>¥1.20</span>
|
||||
<span>20 积分</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops">
|
||||
@@ -2762,7 +2770,7 @@ export function ModelPhotoDemoPage({ variant, products, onBack, navigate }: { va
|
||||
<span className="muted">+ 添加</span>
|
||||
</button>
|
||||
<span className="spacer" />
|
||||
<span className="meta-right">预估 <span className="v">¥1.20</span> · 余额 <span className="v">¥327.40</span></span>
|
||||
<span className="meta-right">预估 <span className="v">20 积分/张</span></span>
|
||||
<button className="gen-btn" type="button" onClick={toRealTool}>
|
||||
<Sparkles size={14} />
|
||||
生成 · {active.title} × Ava
|
||||
|
||||
Reference in New Issue
Block a user