Files
Genarrative/apps/admin-web/src/pages/adminEditorGenerationPricing.ts
T
k88936 ad2b37c6d3 后台模型定价保存去掉版本比对:保存即整段覆盖
- shared-contracts:删掉 `EDITOR_GENERATION_PRICING_VERSION_CONFLICT` 常量
- spacetime-module:`EditorGenerationPricingConfigUpsertInput` 去掉期望版本字段,删掉版本比对方法与它的用例,入库只按请求内容整段覆盖
- spacetime-client:重新生成定价 upsert input 绑定(少一个字段),记录映射同步去掉版本
- api-server:进程内缓存去掉版本字段与 409 冲突错误,`editor_generation_pricing()` / `save_editor_generation_pricing()` 只返回配置本身,`EditorGenerationPricingStore::replace` 注释写明「只是缓存刷新、不做版本校验、不是拦截点」
- api-server admin/app:后台 GET 不再返回 `updatedAtMicros`,POST 不再读 `expectedUpdatedAtMicros`,删掉 409 映射与「旧版本被拒」用例,新增「连续保存两次以后一次为准」
- admin-web:定价类型去掉版本字段,保存请求不再回传版本,删掉 409 分支与「重新读取」提示及其用例
- docs:主规范新增「保存语义」段(整段覆盖、同时只有一个管理员在操作、缓存替换不是拦截点),后端数据契约、ADR 0005、里程碑 / 实施计划、决策记录与 pitfalls 同步为删除版本比对
2026-09-24 19:33:41 +08:00

116 lines
4.1 KiB
TypeScript

import type {
AdminUpsertEditorGenerationPricingRequest,
EditorGenerationModel3dEndpointPricingPayload,
EditorGenerationModel3dPricingPayload,
EditorGenerationPricingConfigPayload,
} from '../api/adminApiTypes';
/** 3D 端点在两个并列段里的字段名与后台展示名。 */
export const EDITOR_MODEL3D_ENDPOINT_SECTIONS = [
{ key: 'textToModelPricing', label: '文生 3D' },
{ key: 'imageToModelPricing', label: '图生 3D' },
] as const satisfies ReadonlyArray<{
key: keyof EditorGenerationModel3dPricingPayload;
label: string;
}>;
/**
* 3D 加价项键到中文标签。键来自接口返回,这里只负责展示;遇到未登记的键回退显示原键名,
* 不因为标签缺失就把该项漏掉。
*/
const EDITOR_MODEL3D_ADD_ON_LABELS: Record<string, string> = {
hdTexture: '高清贴图',
ultraTexture: '超高清贴图',
hdGeometry: '高清几何',
quadMesh: '四边形网格',
smartLowPoly: '智能低面',
generateParts: '分件生成',
};
export function editorModel3dAddOnLabel(addOn: string) {
return EDITOR_MODEL3D_ADD_ON_LABELS[addOn] ?? addOn;
}
export function isPositiveMudPoints(value: number | null | undefined) {
return typeof value === 'number' && Number.isInteger(value) && value >= 1;
}
/**
* 提交前校验:所有泥点数值都必须是不小于 1 的整数,返回不合法项的展示名。
*
* 后端仍会独立校验一遍;这里只是不把明显非法的 payload 发出去,避免保存后才报错。
*/
export function collectInvalidEditorGenerationPricingLabels(
pricing: EditorGenerationPricingConfigPayload,
) {
const labels: string[] = [];
for (const [model, modelPricing] of Object.entries(pricing.models)) {
const tieredPrices = modelPricing.prices;
// `prices: {}` 没有档位可校验,不能算「这一项走分层计价」——否则底价会被顺手跳过校验。
if (tieredPrices && Object.keys(tieredPrices).length > 0) {
for (const [tier, price] of Object.entries(tieredPrices)) {
if (!isPositiveMudPoints(price)) {
labels.push(`${model} ${tier}`);
}
}
continue;
}
if (!isPositiveMudPoints(modelPricing.price)) {
labels.push(`${model} 泥点`);
}
}
const model3d = pricing.model3d;
if (model3d) {
for (const { key, label } of EDITOR_MODEL3D_ENDPOINT_SECTIONS) {
// 段与子表都来自接口返回:缺段或缺子表时只跳过这一段,不能让整页校验崩掉。
const endpointPricing = model3d[key];
if (!endpointPricing) {
continue;
}
for (const [version, price] of Object.entries(
endpointPricing.versionPrices ?? {},
)) {
if (!isPositiveMudPoints(price.noTexture)) {
labels.push(`${label} ${version} 无贴图`);
}
if (!isPositiveMudPoints(price.texture)) {
labels.push(`${label} ${version} 带贴图`);
}
}
for (const [addOn, price] of Object.entries(
endpointPricing.addOnPrices ?? {},
)) {
if (!isPositiveMudPoints(price)) {
labels.push(`${label} ${editorModel3dAddOnLabel(addOn)}`);
}
}
}
}
return labels;
}
/**
* 组装保存请求:`models` 与 3D 两段整段回传,不带定价版本(保存即覆盖,服务端不比对版本)。
* 3D 段缺失时回传 `null`——服务端会以 400 拒绝,后台不允许在这里静默新建或丢弃整段。
*/
export function buildAdminEditorGenerationPricingRequest(
pricing: EditorGenerationPricingConfigPayload,
): AdminUpsertEditorGenerationPricingRequest {
return {
models: pricing.models,
model3d: pricing.model3d ?? null,
};
}
/** 单个端点段的不可变更新:底价与加价项各改一处,其余原样保留。 */
export function updateEditorModel3dEndpointPricing(
model3d: EditorGenerationModel3dPricingPayload,
section: keyof EditorGenerationModel3dPricingPayload,
updater: (
current: EditorGenerationModel3dEndpointPricingPayload,
) => EditorGenerationModel3dEndpointPricingPayload,
): EditorGenerationModel3dPricingPayload {
return { ...model3d, [section]: updater(model3d[section]) };
}