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:
zyc
2026-07-07 10:04:00 +08:00
co-authored by Claude Sonnet 5
parent c1420316c2
commit bf20de956c
46 changed files with 1490 additions and 150 deletions
+23 -15
View File
@@ -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