后台模型定价保存去掉版本比对:保存即整段覆盖
- 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:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user