后台模型定价保存去掉版本比对:保存即整段覆盖

- 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 同步为删除版本比对
This commit is contained in:
2026-09-24 19:33:41 +08:00
parent 3900aa9157
commit ad2b37c6d3
20 changed files with 149 additions and 358 deletions
+5 -4
View File
@@ -458,15 +458,16 @@ export interface EditorGenerationPricingConfigPayload {
models: Record<string, EditorGenerationModelPricingPayload>;
/** 后端尚未配置 3D 段时缺省;缺失即 3D 生成不可提交,后台也不能新建整段。 */
model3d?: EditorGenerationModel3dPricingPayload | null;
/** 当前定价版本:保存时原样回传做乐观锁,冲突时后端返回 409。 */
updatedAtMicros: number;
}
/** 后台保存定价的请求体:`models` 与 3D 两段都必须显式给出,外加读取到的定价版本。 */
/**
* 后台保存定价的请求体:`models` 与 3D 两段都必须显式给出。
*
* 不带定价版本:保存即整段覆盖,服务端不比对版本(后台同时只有一个管理员在操作)。
*/
export interface AdminUpsertEditorGenerationPricingRequest {
models: Record<string, EditorGenerationModelPricingPayload>;
model3d: EditorGenerationModel3dPricingPayload | null;
expectedUpdatedAtMicros: number;
}
export interface AdminEditorAssetListQuery {
@@ -5,7 +5,6 @@ import userEvent from '@testing-library/user-event';
import { beforeEach, expect, test, vi } from 'vitest';
import {
AdminApiError,
getAdminEditorGenerationPricing,
upsertAdminEditorGenerationPricing,
} from '../api/adminApiClient';
@@ -70,7 +69,6 @@ const pricing: EditorGenerationPricingConfigPayload = {
},
},
},
updatedAtMicros: 1_700_000_000_000_000,
};
function savedPricing(
@@ -78,7 +76,6 @@ function savedPricing(
) {
return {
...pricing,
updatedAtMicros: pricing.updatedAtMicros + 1,
...overrides,
};
}
@@ -163,7 +160,6 @@ test('3D 区块按端点渲染版本底价与加价项,并随保存回传整
expect(upsertAdminEditorGenerationPricing).toHaveBeenCalledWith(
'admin-token',
expect.objectContaining({
expectedUpdatedAtMicros: pricing.updatedAtMicros,
model3d: {
textToModelPricing: expect.objectContaining({
versionPrices: expect.objectContaining({
@@ -199,22 +195,6 @@ test('数值小于 1 时禁用保存并给出中文提示', async () => {
expect(upsertAdminEditorGenerationPricing).not.toHaveBeenCalled();
});
test('保存遇到 409 时提示重新读取且不覆盖本地改动', async () => {
const user = userEvent.setup();
vi.mocked(upsertAdminEditorGenerationPricing).mockRejectedValue(
new AdminApiError({ message: '定价冲突', status: 409 }),
);
await renderPage();
await user.click(screen.getByRole('button', { name: '保存定价' }));
await user.click(screen.getByRole('button', { name: '确认' }));
expect(
await screen.findByText('定价已被其他人更新,请重新读取后再编辑保存'),
).toBeTruthy();
expect(screen.getByRole('button', { name: '重新读取' })).toBeTruthy();
});
test('后端未配置 3D 段时给出提示并禁止保存', async () => {
vi.mocked(getAdminEditorGenerationPricing).mockResolvedValue({
...pricing,
@@ -3,7 +3,6 @@ import { FormEvent, useEffect, useState } from 'react';
import {
getAdminEditorGenerationPricing,
isAdminApiError,
upsertAdminEditorGenerationPricing,
} from '../api/adminApiClient';
import type {
@@ -28,7 +27,6 @@ interface AdminEditorGenerationPricingPageProps {
const emptyPricing: EditorGenerationPricingConfigPayload = {
models: {},
model3d: null,
updatedAtMicros: 0,
};
const unitLabels: Record<EditorGenerationPricingUnitPayload, string> = {
@@ -36,9 +34,6 @@ const unitLabels: Record<EditorGenerationPricingUnitPayload, string> = {
perSecond: '按秒',
};
const PRICING_VERSION_CONFLICT_MESSAGE =
'定价已被其他人更新,请重新读取后再编辑保存';
export function AdminEditorGenerationPricingPage({
token,
onUnauthorized,
@@ -48,7 +43,6 @@ export function AdminEditorGenerationPricingPage({
const [isLoading, setIsLoading] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [isPricingVersionStale, setIsPricingVersionStale] = useState(false);
const { confirmWrite, confirmDialog } = useAdminWriteConfirm();
useEffect(() => {
@@ -59,7 +53,6 @@ export function AdminEditorGenerationPricingPage({
async function refreshPricing() {
setIsLoading(true);
setErrorMessage('');
setIsPricingVersionStale(false);
try {
setPricing(await getAdminEditorGenerationPricing(token));
} catch (error: unknown) {
@@ -75,7 +68,6 @@ export function AdminEditorGenerationPricingPage({
return;
}
setErrorMessage('');
setIsPricingVersionStale(false);
const confirmed = await confirmWrite({
action: '保存模型定价',
target: '图片画布生成',
@@ -93,11 +85,6 @@ export function AdminEditorGenerationPricingPage({
),
);
} catch (error: unknown) {
if (isAdminApiError(error) && error.status === 409) {
setErrorMessage(PRICING_VERSION_CONFLICT_MESSAGE);
setIsPricingVersionStale(true);
return;
}
handlePageError(error, onUnauthorized, setErrorMessage);
} finally {
setIsSaving(false);
@@ -175,17 +162,6 @@ export function AdminEditorGenerationPricingPage({
{errorMessage ? (
<div className="admin-alert" role="status">
{errorMessage}
{isPricingVersionStale ? (
<button
className="admin-secondary-button"
disabled={isLoading}
type="button"
onClick={refreshPricing}
>
<RefreshCcw size={17} aria-hidden="true" />
<span>重新读取</span>
</button>
) : null}
</div>
) : null}
@@ -24,7 +24,6 @@ function pricingFixture(): EditorGenerationPricingConfigPayload {
addOnPrices: { hdTexture: 8 },
},
},
updatedAtMicros: 42,
};
}
@@ -73,18 +72,16 @@ test('校验覆盖模型档位与 3D 两段的全部数值', () => {
]);
});
test('保存请求整段回传两段与定价版本,3D 缺失时回传 null', () => {
test('保存请求整段回传两段且不带定价版本,3D 缺失时回传 null', () => {
const pricing = pricingFixture();
expect(buildAdminEditorGenerationPricingRequest(pricing)).toEqual({
models: pricing.models,
model3d: pricing.model3d,
expectedUpdatedAtMicros: 42,
});
expect(
buildAdminEditorGenerationPricingRequest({ ...pricing, model3d: null }),
).toEqual({
models: pricing.models,
model3d: null,
expectedUpdatedAtMicros: 42,
});
});
@@ -91,7 +91,7 @@ export function collectInvalidEditorGenerationPricingLabels(
}
/**
* 组装保存请求:`models` 与 3D 两段整段回传,并带上读取时拿到的定价版本做乐观锁。
* 组装保存请求:`models` 与 3D 两段整段回传,不带定价版本(保存即覆盖,服务端不比对版本)。
* 3D 段缺失时回传 `null`——服务端会以 400 拒绝,后台不允许在这里静默新建或丢弃整段。
*/
export function buildAdminEditorGenerationPricingRequest(
@@ -100,7 +100,6 @@ export function buildAdminEditorGenerationPricingRequest(
return {
models: pricing.models,
model3d: pricing.model3d ?? null,
expectedUpdatedAtMicros: pricing.updatedAtMicros,
};
}