diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index c08b03d85..4c014e6fe 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -11,6 +11,14 @@ import type { AdminDatabaseTableListResponse, AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, + AdminEditorAssetListQuery, + AdminEditorAssetListResponse, + AdminEditorShowcaseAssetResponse, + AdminEditorShowcaseCampaignResponse, + AdminEditorShowcaseDisplayRequest, + AdminEditorShowcaseListQuery, + AdminEditorShowcaseListResponse, + AdminEditorShowcaseReviewRequest, AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, @@ -19,6 +27,7 @@ import type { AdminTrackingEventListResponse, AdminUpdateWorkVisibilityRequest, AdminUpdateWorkVisibilityResponse, + AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertProfileInviteCodeRequest, AdminUpsertProfileRechargeProductRequest, AdminUpsertProfileRedeemCodeRequest, @@ -54,6 +63,23 @@ interface AdminRequestOptions { signal?: AbortSignal; } +interface AdminAssetReadUrlQuery { + objectKey?: string | null; + legacyPublicPath?: string | null; + expireSeconds?: number | null; +} + +export interface AdminAssetReadUrlResponse { + read?: { + objectKey?: string; + signedUrl?: string; + expiresAt?: string; + }; + signedUrl?: string; + objectKey?: string; + expiresAt?: string; +} + export class AdminApiError extends Error { status: number; code: string; @@ -293,6 +319,81 @@ export function updateAdminWorkVisibility( ); } +export function getAdminAssetReadUrl(query: AdminAssetReadUrlQuery) { + return request( + `/api/assets/read-url${buildAssetReadUrlQuery(query)}`, + ); +} + +export function listAdminEditorAssets( + token: string, + query: AdminEditorAssetListQuery = {}, +) { + return request( + `/admin/api/editor-assets${buildEditorAssetListQuery(query)}`, + { token }, + ); +} + +export function listAdminEditorShowcaseAssets( + token: string, + query: AdminEditorShowcaseListQuery = {}, +) { + return request( + `/admin/api/editor-showcase/assets${buildEditorShowcaseListQuery(query)}`, + { token }, + ); +} + +export function reviewAdminEditorShowcaseAsset( + token: string, + payload: AdminEditorShowcaseReviewRequest, +) { + return request( + '/admin/api/editor-showcase/assets/review', + { + method: 'POST', + token, + body: payload, + }, + ); +} + +export function updateAdminEditorShowcaseDisplay( + token: string, + payload: AdminEditorShowcaseDisplayRequest, +) { + return request( + '/admin/api/editor-showcase/assets/display', + { + method: 'POST', + token, + body: payload, + }, + ); +} + +export function getAdminEditorShowcaseCampaign(token: string) { + return request( + '/admin/api/editor-showcase/campaign', + { token }, + ); +} + +export function upsertAdminEditorShowcaseCampaign( + token: string, + payload: AdminUpsertEditorShowcaseCampaignRequest, +) { + return request( + '/admin/api/editor-showcase/campaign', + { + method: 'POST', + token, + body: payload, + }, + ); +} + export function listProfileRedeemCodes(token: string) { return request( '/admin/api/profile/redeem-codes', @@ -432,6 +533,29 @@ function buildRequestUrl(path: string) { return `${ADMIN_API_BASE_URL}${normalizedPath}`; } +function buildAssetReadUrlQuery(query: AdminAssetReadUrlQuery) { + const params = new URLSearchParams(); + const objectKey = query.objectKey?.trim().replace(/^\/+/u, '') ?? ''; + const legacyPublicPath = query.legacyPublicPath?.trim() ?? ''; + if (objectKey) { + params.set('objectKey', objectKey); + } else if (legacyPublicPath) { + params.set( + 'legacyPublicPath', + `/${legacyPublicPath.replace(/^\/+/u, '')}`, + ); + } + if ( + typeof query.expireSeconds === 'number' && + Number.isFinite(query.expireSeconds) && + query.expireSeconds > 0 + ) { + params.set('expireSeconds', String(Math.floor(query.expireSeconds))); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + function buildQueryString(query: AdminTrackingEventListQuery) { const params = new URLSearchParams(); appendQueryParam(params, 'eventKey', query.eventKey); @@ -471,6 +595,35 @@ function buildDatabaseTableRowsQuery(query: AdminDatabaseTableRowsQuery) { return queryString ? `?${queryString}` : ''; } +function buildEditorAssetListQuery(query: AdminEditorAssetListQuery) { + const params = new URLSearchParams(); + appendQueryParam(params, 'cursor', query.cursor); + appendQueryParam(params, 'ownerUserId', query.ownerUserId); + appendQueryParam(params, 'assetKind', query.assetKind); + appendQueryParam(params, 'createdAfter', query.createdAfter); + appendQueryParam(params, 'createdBefore', query.createdBefore); + if (typeof query.limit === 'number' && Number.isFinite(query.limit)) { + params.set('limit', String(query.limit)); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + +function buildEditorShowcaseListQuery(query: AdminEditorShowcaseListQuery) { + const params = new URLSearchParams(); + appendQueryParam(params, 'cursor', query.cursor); + appendQueryParam(params, 'ownerUserId', query.ownerUserId); + appendQueryParam(params, 'assetKind', query.assetKind); + appendQueryParam(params, 'reviewStatus', query.reviewStatus); + appendQueryParam(params, 'submittedAfter', query.submittedAfter); + appendQueryParam(params, 'submittedBefore', query.submittedBefore); + if (typeof query.limit === 'number' && Number.isFinite(query.limit)) { + params.set('limit', String(query.limit)); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + function appendQueryParam( params: URLSearchParams, key: string, diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 5d408ff65..2fed262fd 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -346,6 +346,135 @@ export interface AdminUpdateWorkVisibilityResponse { entry: AdminWorkVisibilityEntryPayload; } +export interface AdminEditorAssetListQuery { + cursor?: string | null; + ownerUserId?: string | null; + assetKind?: string | null; + createdAfter?: string | null; + createdBefore?: string | null; + limit?: number | null; +} + +export interface AdminEditorAssetPayload { + assetId: string; + ownerUserId: string; + authorDisplayName?: string | null; + authorPublicUserCode?: string | null; + folderId: string; + label: string; + assetObjectId?: string | null; + imageSrc: string; + objectKey?: string | null; + width: number; + height: number; + sourceType: string; + prompt?: string | null; + actualPrompt?: string | null; + model?: string | null; + provider?: string | null; + taskId?: string | null; + assetKind?: string | null; + generationInputs?: Record | null; + sourceResourceId?: string | null; + thumbnailSrc?: string | null; + generationCostMudPoints: number; + createdAt: string; + updatedAt: string; +} + +export interface AdminEditorAssetListResponse { + entries: AdminEditorAssetPayload[]; + nextCursor?: string | null; +} + +export interface AdminEditorShowcaseListQuery { + cursor?: string | null; + ownerUserId?: string | null; + assetKind?: string | null; + reviewStatus?: string | null; + submittedAfter?: string | null; + submittedBefore?: string | null; + limit?: number | null; +} + +export interface AdminEditorShowcaseAssetPayload { + showcaseId: string; + assetId: string; + ownerUserId: string; + authorDisplayName?: string | null; + authorPublicUserCode?: string | null; + label: string; + imageSrc: string; + objectKey?: string | null; + width: number; + height: number; + prompt?: string | null; + actualPrompt?: string | null; + model?: string | null; + provider?: string | null; + taskId?: string | null; + assetKind?: string | null; + generationInputs?: Record | null; + generationCostMudPoints: number; + refundMudPoints: number; + reviewStatus: 'pending' | 'approved' | 'rejected' | string; + displayEnabled: boolean; + likeCount: number; + assetDeletedWhilePending: boolean; + reviewedByAdminUserId?: string | null; + reviewNote?: string | null; + refundLedgerId?: string | null; + refundCompletedAt?: string | null; + submittedAt: string; + reviewedAt?: string | null; + approvedAt?: string | null; + rejectedAt?: string | null; + updatedAt: string; +} + +export interface AdminEditorShowcaseListResponse { + entries: AdminEditorShowcaseAssetPayload[]; + nextCursor?: string | null; +} + +export interface AdminEditorShowcaseReviewRequest { + showcaseId: string; + reviewStatus: 'approved' | 'rejected'; + reviewNote?: string | null; +} + +export interface AdminEditorShowcaseDisplayRequest { + showcaseId: string; + displayEnabled: boolean; +} + +export interface AdminEditorShowcaseAssetResponse { + entry: AdminEditorShowcaseAssetPayload; +} + +export interface AdminEditorShowcaseCampaignPayload { + enabled: boolean; + title: string; + imageSrc: string; + prompt: string; + author: string; + costText: string; + updatedAt?: string; +} + +export interface AdminEditorShowcaseCampaignResponse { + campaign?: AdminEditorShowcaseCampaignPayload | null; +} + +export interface AdminUpsertEditorShowcaseCampaignRequest { + enabled: boolean; + title: string; + imageSrc: string; + prompt: string; + author: string; + costText: string; +} + export interface AdminUpsertProfileRedeemCodeRequest { code: string; mode: ProfileRedeemCodeMode; diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index 6a09d347c..65cc5e99e 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -24,6 +24,7 @@ import {AdminDatabaseTablesPage} from '../pages/AdminDatabaseTablesPage'; import {AdminInviteCodePage} from '../pages/AdminInviteCodePage'; import {AdminLoginPage} from '../pages/AdminLoginPage'; import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage'; +import {AdminEditorAssetQueryPage} from '../pages/AdminEditorAssetQueryPage'; import {AdminOverviewPage} from '../pages/AdminOverviewPage'; import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage'; import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage'; @@ -245,6 +246,12 @@ export function AdminApp() { onUnauthorized={handleUnauthorized} /> ) : null} + {routeId === 'editor-showcase' ? ( + + ) : null} ); } diff --git a/apps/admin-web/src/app/AdminShell.tsx b/apps/admin-web/src/app/AdminShell.tsx index 708b9dda5..8b9fd9dd0 100644 --- a/apps/admin-web/src/app/AdminShell.tsx +++ b/apps/admin-web/src/app/AdminShell.tsx @@ -7,6 +7,7 @@ import { LogOut, Megaphone, Eye, + Images, WalletCards, ShieldCheck, ListChecks, @@ -42,6 +43,7 @@ const routeIcons = { tasks: ListChecks, 'recharge-products': BadgeDollarSign, 'editor-generation-pricing': Coins, + 'editor-showcase': Images, 'creation-announcement': Megaphone, 'creation-entry': SlidersHorizontal, 'work-visibility': Eye, diff --git a/apps/admin-web/src/app/adminRoutes.test.ts b/apps/admin-web/src/app/adminRoutes.test.ts index cd26f5405..5270e89dd 100644 --- a/apps/admin-web/src/app/adminRoutes.test.ts +++ b/apps/admin-web/src/app/adminRoutes.test.ts @@ -39,3 +39,13 @@ test('后台模型定价路由可通过导航和 hash 访问', () => { '#editor-generation-pricing', ); }); + +test('后台素材查询路由可通过导航和 hash 访问', () => { + expect(adminRoutes).toContainEqual({ + id: 'editor-showcase', + label: '素材查询', + hash: '#editor-showcase', + }); + expect(resolveAdminRoute('#editor-showcase')).toBe('editor-showcase'); + expect(routeHash('editor-showcase')).toBe('#editor-showcase'); +}); diff --git a/apps/admin-web/src/app/adminRoutes.ts b/apps/admin-web/src/app/adminRoutes.ts index 870e31a19..61fe082c4 100644 --- a/apps/admin-web/src/app/adminRoutes.ts +++ b/apps/admin-web/src/app/adminRoutes.ts @@ -11,6 +11,7 @@ export type AdminRouteId = | 'tasks' | 'recharge-products' | 'editor-generation-pricing' + | 'editor-showcase' | 'creation-announcement' | 'creation-entry' | 'work-visibility'; @@ -34,6 +35,7 @@ export const adminRoutes: AdminRouteDefinition[] = [ {id: 'tasks', label: '任务配置', hash: '#tasks'}, {id: 'recharge-products', label: '充值商品', hash: '#recharge-products'}, {id: 'editor-generation-pricing', label: '模型定价', hash: '#editor-generation-pricing'}, + {id: 'editor-showcase', label: '素材查询', hash: '#editor-showcase'}, {id: 'creation-announcement', label: '入口公告', hash: '#creation-announcement'}, {id: 'creation-entry', label: '入口开关', hash: '#creation-entry'}, {id: 'work-visibility', label: '作品可见性', hash: '#work-visibility'}, diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx new file mode 100644 index 000000000..4e7839b9c --- /dev/null +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx @@ -0,0 +1,164 @@ +/* @vitest-environment jsdom */ + +import {fireEvent, render, screen, waitFor, within} from '@testing-library/react'; +import {beforeEach, expect, test, vi} from 'vitest'; + +import { + getAdminAssetReadUrl, + listAdminEditorAssets, +} from '../api/adminApiClient'; +import type {AdminEditorAssetPayload} from '../api/adminApiTypes'; +import {AdminEditorAssetQueryPage} from './AdminEditorAssetQueryPage'; + +vi.mock('../api/adminApiClient', () => ({ + getAdminAssetReadUrl: vi.fn(), + listAdminEditorAssets: vi.fn(), +})); + +const generatedAsset: AdminEditorAssetPayload = { + assetId: 'asset-1', + ownerUserId: 'user-1', + authorDisplayName: '作者昵称', + authorPublicUserCode: 'SY-00000042', + folderId: 'folder-1', + label: '角色形象 1', + assetObjectId: 'asset-object-1', + imageSrc: '/generated-character-drafts/editor/spec.png', + objectKey: 'generated-character-drafts/editor/spec.png', + width: 1024, + height: 1024, + sourceType: 'generated', + prompt: '完整提示词内容', + actualPrompt: null, + model: 'gpt-image-2', + provider: 'character', + taskId: 'task-1', + assetKind: 'character', + generationInputs: {style: 'clay'}, + sourceResourceId: 'resource-1', + thumbnailSrc: null, + generationCostMudPoints: 12, + createdAt: '2026-07-04T10:00:00Z', + updatedAt: '2026-07-04T10:00:00Z', +}; + +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(listAdminEditorAssets).mockResolvedValue({ + entries: [generatedAsset], + nextCursor: null, + }); + vi.mocked(getAdminAssetReadUrl).mockResolvedValue({ + read: { + objectKey: 'generated-character-drafts/editor/spec.png', + signedUrl: + 'https://signed.example.com/generated-character-drafts/editor/spec.png', + expiresAt: '2026-07-04T11:00:00Z', + }, + }); +}); + +test('后台素材查询展示作者昵称和陶泥号', async () => { + render( + , + ); + + expect(await screen.findByText('作者昵称')).toBeTruthy(); + expect(screen.getByText('SY-00000042')).toBeTruthy(); + expect(screen.queryByText('user-1')).toBeNull(); +}); + +test('后台素材查询按用户、分类和时间调用查询接口', async () => { + render( + , + ); + + await screen.findByRole('img', {name: '素材:角色形象 1'}); + + fireEvent.change(screen.getByLabelText('用户 ID'), { + target: {value: 'user-1'}, + }); + fireEvent.change(screen.getByLabelText('分类'), { + target: {value: 'character'}, + }); + fireEvent.change(screen.getByLabelText('开始时间'), { + target: {value: '2026-07-01'}, + }); + fireEvent.change(screen.getByLabelText('结束时间'), { + target: {value: '2026-07-04'}, + }); + + await waitFor(() => { + expect(listAdminEditorAssets).toHaveBeenLastCalledWith('admin-token', { + ownerUserId: 'user-1', + assetKind: 'character', + createdAfter: '2026-07-01T00:00:00+08:00', + createdBefore: '2026-07-04T23:59:59.999+08:00', + limit: 80, + }); + }); +}); + +test('后台素材查询缩略图使用 objectKey 换签后展示', async () => { + render( + , + ); + + const image = await screen.findByRole('img', {name: '素材:角色形象 1'}); + + await waitFor(() => { + expect(image.getAttribute('src')).toBe( + 'https://signed.example.com/generated-character-drafts/editor/spec.png', + ); + }); + expect(getAdminAssetReadUrl).toHaveBeenCalledWith({ + objectKey: 'generated-character-drafts/editor/spec.png', + expireSeconds: 300, + }); +}); + +test('后台素材查询可查看素材详情', async () => { + render( + , + ); + + fireEvent.click(await screen.findByRole('button', {name: '详情'})); + + const dialog = screen.getByRole('dialog', {name: '素材详情'}); + expect(within(dialog).getByText('asset-1')).toBeTruthy(); + expect(within(dialog).getByText('1024 x 1024')).toBeTruthy(); + expect(within(dialog).getByText('12 泥点')).toBeTruthy(); + expect(within(dialog).getByText(/"style": "clay"/u)).toBeTruthy(); +}); + +test('后台素材查询可打开弹窗查看完整提示词', async () => { + render( + , + ); + + fireEvent.click(await screen.findByRole('button', {name: '完整提示词内容'})); + + const dialog = screen.getByRole('dialog', {name: '完整提示词'}); + expect(within(dialog).getByText('完整提示词内容')).toBeTruthy(); + + fireEvent.click(within(dialog).getByRole('button', {name: '关闭完整提示词'})); + + await waitFor(() => { + expect(screen.queryByRole('dialog', {name: '完整提示词'})).toBeNull(); + }); +}); diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx new file mode 100644 index 000000000..0799490ef --- /dev/null +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx @@ -0,0 +1,565 @@ +import {Eye, FileText, RefreshCcw, X} from 'lucide-react'; +import type {ReactNode} from 'react'; +import {useEffect, useMemo, useState} from 'react'; + +import { + getAdminAssetReadUrl, + listAdminEditorAssets, +} from '../api/adminApiClient'; +import type {AdminAssetReadUrlResponse} from '../api/adminApiClient'; +import type { + AdminEditorAssetListQuery, + AdminEditorAssetPayload, +} from '../api/adminApiTypes'; +import {handlePageError} from './pageUtils'; + +interface AdminEditorAssetQueryPageProps { + token: string; + onUnauthorized: (message?: string) => void; +} + +const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300; + +const assetKindOptions = [ + {value: '', label: '全部'}, + {value: 'character', label: '角色'}, + {value: 'ui-design', label: 'UI'}, + {value: 'icon', label: '图标'}, + {value: 'icon-spritesheet', label: '图标图集'}, + {value: 'spec', label: '规范'}, + {value: 'publication-material', label: '宣发素材'}, +]; + +export function AdminEditorAssetQueryPage({ + token, + onUnauthorized, +}: AdminEditorAssetQueryPageProps) { + const [entries, setEntries] = useState([]); + const [keyword, setKeyword] = useState(''); + const [ownerUserId, setOwnerUserId] = useState(''); + const [assetKind, setAssetKind] = useState(''); + const [createdAfter, setCreatedAfter] = useState(''); + const [createdBefore, setCreatedBefore] = useState(''); + const [nextCursor, setNextCursor] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const [errorMessage, setErrorMessage] = useState(''); + const [detailEntry, setDetailEntry] = + useState(null); + const [promptPreview, setPromptPreview] = useState<{ + title: string; + prompt: string; + } | null>(null); + + useEffect(() => { + void refreshPage(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token, ownerUserId, assetKind, createdAfter, createdBefore]); + + const filteredEntries = useMemo(() => { + const normalizedKeyword = keyword.trim().toLowerCase(); + if (!normalizedKeyword) { + return entries; + } + return entries.filter((entry) => + [ + entry.assetId, + entry.ownerUserId, + entry.authorDisplayName ?? '', + entry.authorPublicUserCode ?? '', + entry.label, + entry.assetKind ?? '', + entry.prompt ?? '', + entry.actualPrompt ?? '', + entry.provider ?? '', + entry.model ?? '', + ] + .join(' ') + .toLowerCase() + .includes(normalizedKeyword), + ); + }, [entries, keyword]); + + async function refreshPage() { + setIsLoading(true); + setErrorMessage(''); + try { + const response = await listAdminEditorAssets(token, buildListQuery()); + setEntries(response.entries); + setNextCursor(response.nextCursor ?? null); + } catch (error: unknown) { + handlePageError(error, onUnauthorized, setErrorMessage); + } finally { + setIsLoading(false); + } + } + + async function loadMore() { + if (!nextCursor || isLoadingMore) { + return; + } + setIsLoadingMore(true); + setErrorMessage(''); + try { + const response = await listAdminEditorAssets(token, { + ...buildListQuery(), + cursor: nextCursor, + }); + setEntries((current) => mergeAssetEntries(current, response.entries)); + setNextCursor(response.nextCursor ?? null); + } catch (error: unknown) { + handlePageError(error, onUnauthorized, setErrorMessage); + } finally { + setIsLoadingMore(false); + } + } + + function buildListQuery(): AdminEditorAssetListQuery { + return { + ownerUserId: ownerUserId || null, + assetKind: assetKind || null, + createdAfter: dateInputToStartRfc3339(createdAfter), + createdBefore: dateInputToEndRfc3339(createdBefore), + limit: 80, + }; + } + + return ( +
+
+
+

素材查询

+
+ +
+ + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} + +
+
+ + + + + +
+ +
+ + + + + + + + + + + + + + {filteredEntries.map((entry) => { + const promptText = entry.prompt || entry.actualPrompt || '-'; + return ( + + + + + + + + + + ); + })} + +
资源图创建时间用户分类提示词生成成本详情
+ + + {formatDateTime(entry.createdAt)} + {entry.assetId} + + {entry.authorDisplayName?.trim() || entry.ownerUserId} + {entry.authorPublicUserCode?.trim() || '-'} + + {assetKindLabel(entry.assetKind || entry.provider || '-')} + {entry.provider || '-'} + + + {entry.generationCostMudPoints} 泥点 + +
+
+ + {!isLoading && filteredEntries.length === 0 ? ( +
暂无生成素材
+ ) : null} + {nextCursor ? ( + + ) : null} +
+ + {detailEntry ? ( + setDetailEntry(null)} + onPromptPreview={(entry, prompt) => + setPromptPreview({ + title: entry.label || entry.assetId, + prompt, + }) + } + /> + ) : null} + + {promptPreview ? ( +
+
+
+
+

完整提示词

+ {promptPreview.title} +
+ +
+
+              {promptPreview.prompt}
+            
+
+
+ ) : null} +
+ ); +} + +function AdminAssetThumbnail({entry}: {entry: AdminEditorAssetPayload}) { + const imageSrc = useAdminResolvedAssetImageSrc( + entry.thumbnailSrc || entry.imageSrc, + entry.objectKey, + ); + const alt = `素材:${entry.label || entry.assetId}`; + + return imageSrc ? ( + {alt} + ) : ( +
+ ); +} + +function AdminAssetDetailDialog({ + entry, + onClose, + onPromptPreview, +}: { + entry: AdminEditorAssetPayload; + onClose: () => void; + onPromptPreview: (entry: AdminEditorAssetPayload, prompt: string) => void; +}) { + const promptText = entry.prompt || entry.actualPrompt || ''; + return ( +
+
+
+
+

{entry.label || entry.assetId}

+ {entry.assetId} +
+ +
+
+ +
+ + {entry.authorDisplayName?.trim() || entry.ownerUserId} + {entry.authorPublicUserCode?.trim() || '-'} + + {entry.ownerUserId} + + {assetKindLabel(entry.assetKind || '-')} + + + {entry.width} x {entry.height} + + + {entry.generationCostMudPoints} 泥点 + + {entry.model || '-'} + {entry.provider || '-'} + {entry.taskId || '-'} + + {entry.objectKey || '-'} + + + {formatDateTime(entry.createdAt)} + + + {formatDateTime(entry.updatedAt)} + + + {promptText ? ( + + ) : ( + '-' + )} + + +
+                {formatGenerationInputs(entry.generationInputs)}
+              
+
+
+
+
+
+ ); +} + +function AdminInfoItem({ + label, + children, +}: { + label: string; + children: ReactNode; +}) { + return ( +
+
{label}
+
{children}
+
+ ); +} + +function useAdminResolvedAssetImageSrc( + imageSrc: string | null | undefined, + objectKey: string | null | undefined, +) { + const normalizedImageSrc = imageSrc?.trim() ?? ''; + const normalizedObjectKey = normalizeAdminObjectKey(objectKey); + const shouldResolve = + Boolean(normalizedObjectKey) || isGeneratedLegacyPath(normalizedImageSrc); + const [resolvedImageSrc, setResolvedImageSrc] = useState( + shouldResolve ? '' : normalizedImageSrc, + ); + + useEffect(() => { + if (!normalizedImageSrc && !normalizedObjectKey) { + setResolvedImageSrc(''); + return; + } + if (!shouldResolve) { + setResolvedImageSrc(normalizedImageSrc); + return; + } + + let cancelled = false; + setResolvedImageSrc(''); + + void getAdminAssetReadUrl( + normalizedObjectKey + ? { + objectKey: normalizedObjectKey, + expireSeconds: ADMIN_ASSET_READ_EXPIRE_SECONDS, + } + : { + legacyPublicPath: normalizedImageSrc, + expireSeconds: ADMIN_ASSET_READ_EXPIRE_SECONDS, + }, + ) + .then(resolveAdminAssetReadSignedUrl) + .then((signedUrl) => { + if (!cancelled) { + setResolvedImageSrc(signedUrl); + } + }) + .catch(() => { + if (!cancelled) { + setResolvedImageSrc(''); + } + }); + + return () => { + cancelled = true; + }; + }, [normalizedImageSrc, normalizedObjectKey, shouldResolve]); + + return resolvedImageSrc; +} + +function normalizeAdminObjectKey(value: string | null | undefined) { + return value?.trim().replace(/^\/+/u, '') ?? ''; +} + +function isGeneratedLegacyPath(value: string) { + return /^\/?generated-[^/?#]+\/.+/u.test(value.trim()); +} + +function resolveAdminAssetReadSignedUrl(response: AdminAssetReadUrlResponse) { + const read = response.read ?? response; + return typeof read.signedUrl === 'string' ? read.signedUrl.trim() : ''; +} + +function mergeAssetEntries( + current: AdminEditorAssetPayload[], + incoming: AdminEditorAssetPayload[], +) { + const byId = new Map(); + [...current, ...incoming].forEach((entry) => byId.set(entry.assetId, entry)); + return [...byId.values()].sort( + (left, right) => + Date.parse(right.createdAt) - Date.parse(left.createdAt) || + right.assetId.localeCompare(left.assetId), + ); +} + +function dateInputToStartRfc3339(value: string) { + return value ? `${value}T00:00:00+08:00` : null; +} + +function dateInputToEndRfc3339(value: string) { + return value ? `${value}T23:59:59.999+08:00` : null; +} + +function formatDateTime(value: string) { + const timestamp = Date.parse(value); + if (!Number.isFinite(timestamp)) { + return value || '-'; + } + return new Intl.DateTimeFormat('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }).format(timestamp); +} + +function assetKindLabel(value: string) { + const normalizedValue = value.trim(); + return ( + assetKindOptions.find((option) => option.value === normalizedValue)?.label || + normalizedValue || + '-' + ); +} + +function formatGenerationInputs(value: Record | null | undefined) { + if (!value) { + return '-'; + } + return JSON.stringify(value, null, 2); +} diff --git a/apps/admin-web/src/styles/admin.css b/apps/admin-web/src/styles/admin.css index b71b17acc..7da14c4fc 100644 --- a/apps/admin-web/src/styles/admin.css +++ b/apps/admin-web/src/styles/admin.css @@ -553,6 +553,46 @@ button:disabled { gap: 10px; } +.admin-asset-query-filter-row { + align-items: end; +} + +.admin-asset-query-filter-row .admin-field { + min-width: 140px; +} + +.admin-asset-query-filter-row .admin-field:last-child { + min-width: 240px; +} + +.admin-asset-query-thumb-button { + display: inline-flex; + border: 0; + background: transparent; + padding: 0; +} + +.admin-asset-query-thumb { + border: 1px solid #eaded2; + border-radius: 8px; + width: 72px; + height: 72px; + background: #fffdf9; + object-fit: cover; +} + +.admin-asset-query-thumb-placeholder { + display: block; +} + +.admin-asset-query-prompt-text { + display: block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .admin-query-action-row { justify-content: space-between; } @@ -905,6 +945,17 @@ button:disabled { font-size: 12px; } +.admin-table textarea { + width: min(100%, 220px); + min-height: 64px; + border: 1px solid #dfc8b7; + border-radius: 8px; + color: #3d1f10; + background: #fffdf9; + padding: 8px 10px; + resize: vertical; +} + .admin-muted-text { color: #a38f80; } @@ -939,6 +990,78 @@ button:disabled { min-width: 1180px; } +.admin-asset-query-table { + table-layout: fixed; +} + +.admin-asset-query-table th:nth-child(1), +.admin-asset-query-table td:nth-child(1) { + width: 10%; +} + +.admin-asset-query-table th:nth-child(2), +.admin-asset-query-table td:nth-child(2), +.admin-asset-query-table th:nth-child(3), +.admin-asset-query-table td:nth-child(3) { + width: 14%; +} + +.admin-asset-query-table th:nth-child(4), +.admin-asset-query-table td:nth-child(4) { + width: 10%; +} + +.admin-asset-query-table th:nth-child(5), +.admin-asset-query-table td:nth-child(5) { + width: 30%; +} + +.admin-asset-query-table th:nth-child(6), +.admin-asset-query-table td:nth-child(6) { + width: 8%; +} + +.admin-asset-query-table th:nth-child(7), +.admin-asset-query-table td:nth-child(7) { + width: 10%; +} + +.admin-asset-query-detail-dialog, +.admin-asset-query-prompt-dialog { + width: min(100%, 860px); +} + +.admin-asset-query-prompt-dialog .admin-panel-heading > div, +.admin-asset-query-detail-dialog .admin-panel-heading > div { + min-width: 0; +} + +.admin-asset-query-prompt-dialog .admin-panel-heading span, +.admin-asset-query-detail-dialog .admin-panel-heading span { + display: block; + max-width: 100%; + margin-top: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.admin-asset-query-prompt-full { + max-height: min(68dvh, 520px); +} + +.admin-asset-query-detail-layout { + display: grid; + grid-template-columns: 220px minmax(0, 1fr); + gap: 16px; + align-items: start; +} + +.admin-asset-query-detail-layout > .admin-asset-query-thumb { + width: 220px; + height: 220px; +} + .admin-database-table { width: max-content; min-width: 100%; diff --git a/apps/admin-web/vite.config.ts b/apps/admin-web/vite.config.ts index 74f1567bd..f863f8630 100644 --- a/apps/admin-web/vite.config.ts +++ b/apps/admin-web/vite.config.ts @@ -40,6 +40,11 @@ export default defineConfig(({mode}) => { changeOrigin: true, secure: false, }, + '/api/assets': { + target: apiTarget, + changeOrigin: true, + secure: false, + }, '/healthz': { target: apiTarget, changeOrigin: true, diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 389432a1d..065692fec 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -24,6 +24,14 @@ - 验证方式:运行 `cargo fmt --manifest-path server-rs/Cargo.toml --all`、`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`、`npm run check:encoding` 和 `git diff --check`。 - 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`。 +## 2026-07-04 陶泥儿精选改为素材提交审核后公开 + +- 背景:`/creation` 的 `陶泥儿精选` 过去依赖 `editor_project_resource.public_showcase_enabled`,生成画布资源默认可公开,和“作品公开默认关闭、由用户主动投稿精选”的运营要求冲突,也无法在后台审核、返还泥点和配置固定活动卡。 +- 决策:`陶泥儿精选` 的公开事实改为独立 `editor_showcase_asset` 审核表。生成素材默认不公开;用户在账号级素材库对 `sourceType="generated"` 且有媒体内容的素材提交审核,后端快照素材信息并写入 `pending`。后台审核通过后写入 `approved`、默认 `display_enabled=true` 并按 `generation_cost_mud_points` 返还 50% 泥点;拒绝后写入 `rejected`。公开接口 `GET /api/editor/showcase/resources` 只返回已通过且展示开启的快照,按通过时间和 `showcaseId` 倒序分页,并可携带后台配置的固定活动卡。旧 `editor_project_resource.public_showcase_enabled` 和旧 PATCH 接口只保留兼容,不再驱动精选公开。 +- 影响范围:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`、`spacetime-client` 绑定与 mapper、`api-server` 编辑器和后台路由、admin-web 精选审核页、素材库右键菜单、`/creation` 精选瀑布流、图片画布文档和后端表目录。 +- 验证方式:运行 `npm run spacetime:generate`、`npm run check:spacetime-schema`、`cargo check --manifest-path server-rs/Cargo.toml -p spacetime-module -p spacetime-client -p api-server`、前端 / 后台 typecheck 与精选相关组件测试,确认默认不公开、提交后 pending、审核通过后展示和返还、展示开关与点赞生效。 +- 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + ## 2026-07-03 外部编辑器 API 生成默认写入画布与素材库 - 背景:外部 API 面向美术 Agent 使用时,需要从自然语言自动选路,并保证生成结果不会只停留在接口回包里;同时后续素材生成需要复用已抽象出的美术规范,避免每次重新追问风格要求。 @@ -92,7 +100,7 @@ ## 2026-06-22 创作主页精选展示全站公开画布生成资源 - 背景:`/creation` 的 `陶泥儿精选` 曾从账号级素材库读取,并在素材为空时用公开作品图片补充,导致新创作页出现不属于任何当前图片画布项目的素材。 -- 决策:`陶泥儿精选` 从全站 `editor_project_resource` 构建素材包和素材,读取路径固定为 `GET /api/editor/showcase/resources`;只保留 `sourceType="generated"`、图片源非空且 `public_showcase_enabled = true` 的画布生成资源,按创建时间和资源 ID 倒序 cursor 分页展示,每页最多 36 条,有 `nextCursor` 时前端滚动到底部继续追加。账号级 `editor_asset`、上传素材、公开作品图片和 `mock_generated` 都不作为精选来源。任意画布左侧素材列表中,单素材右键菜单承接文字 `删除` 和 `公开展示该素材` 勾选项;勾选项默认打开,修改时写回对应 `editor_project_resource.public_showcase_enabled`。 +- 决策:该历史决策已被 2026-07-04 的“素材提交审核后公开”取代。历史背景仍有效:公开作品图片和假数据不应回填精选;但精选事实源不再是 `editor_project_resource.public_showcase_enabled`,而是 `editor_showcase_asset` 审核快照。 - 影响范围:`/creation` 创作主页、`creationShowcaseModel`、公开精选 BFF、图片画布素材列表右键菜单、账号素材库快照、创作主页改版计划和精选素材相关测试。 - 验证方式:运行 `src/components/creation-home/creationShowcaseModel.test.ts`、`CreationLandingView.test.tsx`、`ImageCanvasAssetRowView.test.tsx`、`useImageCanvasAssetLibrary.test.tsx` 与 `src/services/image-editor/editorProjectClient.test.ts`,确认公开生成资源展示、上传素材过滤、公开开关隐藏资源、删除入口在右键菜单中、公开作品不再 fallback。 - 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md`、`docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md`。 diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index fc8fffb8f..4771beca1 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -46,7 +46,8 @@ - 新增 `editor_project` 表保存图片画布工程:`projectId`、`ownerUserId`、标题、创建时间和更新时间;历史 layout 字段暂保留为兼容列,不再作为权威画布数据。 - 新增 `editor_canvas` 表保存工程下的画布:`canvasId`、`projectId`、`ownerUserId`、标题、viewport、图层布局 JSON、创建时间和更新时间。当前编辑器使用项目默认画布,后续可扩展为一个 project 下多个 canvas。 - 新增 `editor_asset_folder` 表保存账号级素材文件夹:`folderId`、`ownerUserId`、名称、排序、折叠状态、系统默认标记、创建时间和更新时间。素材文件夹不归属于 project,同一个账号进入任一项目都能看到。 -- 新增 `editor_asset` 表保存账号级素材:`assetId`、`ownerUserId`、`folderId`、名称、图片读取地址、可选封面 `thumbnailSrc`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、`assetKind`、`generationInputs`、创建时间和更新时间。素材只跟账号走,不跟 project 走;角色、图标、UI 设计图、视频和音频等生成结果的用户可见输入快照随素材保存。 +- 新增 `editor_asset` 表保存账号级素材:`assetId`、`ownerUserId`、`folderId`、名称、图片读取地址、可选封面 `thumbnailSrc`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、`assetKind`、`generationInputs`、`generationCostMudPoints`、创建时间和更新时间。素材只跟账号走,不跟 project 走;角色、图标、UI 设计图、视频和音频等生成结果的用户可见输入快照随素材保存。 +- 新增 `editor_showcase_asset`、`editor_showcase_asset_like` 和 `editor_showcase_campaign_config` 表承接 `陶泥儿精选`:生成素材默认不公开,用户在素材菜单中提交精选审核后生成独立快照;后台审核通过才进入公开精选,并按生成成本返还 50% 泥点。公开列表不再读取 `editor_project_resource.public_showcase_enabled`,而是读取已通过且展示开启的精选快照,支持点赞数和首位活动卡。 - `editor_project_resource` 表保存工程画布引用过的资源快照:`resourceId`、`projectId`、`ownerUserId`、OSS / asset object 引用、图片尺寸、来源类型、prompt、actualPrompt、model、provider、taskId、sourceResourceId、`assetKind`、`generationInputs`、创建时间和更新时间。上传素材被拖入画布时会复制为 project resource,图层只引用 resourceId;图片、图标和 UI 素材生成 BFF 在请求携带 `projectId` 时由后端直接创建新 resource,并把 `resourceId` 随生成响应返回给前端。图片生成请求如果同时携带 `canvasCompletion`(生成器 `dialogId`、标题和占位框,或无 dialog 的右侧完成占位),BFF / worker 在生成成功后必须直接读取当前项目布局,优先使用最新 `generation-dialog` 占位框位置;只有当前布局仍存在对应 `generation-dialog` 时才插入轻量结果图层、把生成器标记为 `idle` 并写入 `generatedLayerId`,沿用后端当前 viewport 保存布局,再返回或刷新最新项目快照;前端只应用该快照刷新显示,不把生成完成态作为本地业务真相,也不在项目加载时根据资源行推断完成态。有项目上下文但后端没有返回项目快照时,前端不得本地补结果图层,只保留当前生成器交互状态等待下一次项目刷新。 - 项目封面图是画布当前视口栅格化后的静态快照资源,不在项目列表页临时重放 `layers + viewport`。前端在项目加载后和防抖保存 layout 时生成 320x240 PNG,走私有 OSS / asset object 上传,再创建 `editor_project_resource`,其中 `assetKind="project-cover-snapshot"`、`sourceType="uploaded"`。`/project` 与 `/creation` 最近项目卡只读取最新封面快照资源渲染;没有封面快照时显示普通项目占位,不回退为实时画布组合。 - 图片、音频、视频和角色动画帧文件本体继续走 OSS / asset object;浏览器读取私有 generated 对象统一经 `/api/assets/read-url` 换签,签名 URL 可在 session 内复用,但不得作为持久化真相。`/api/assets/read-url` 属于页面展示层高频后台请求,前端统一在 `assetReadUrlService` 内做同 key pending 去重、session 缓存和跨组件节流;UI 设计切片、角色动画帧或大量素材恢复时不得绕过该服务并发换签,否则单页可在同一秒内打满发布入口 `genarrative_api_rps` burst。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index e999cd218..602b4bc70 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -54,7 +54,7 @@ npm run check:server-rs-ddd 路由树由 `server-rs/crates/api-server/src/app.rs` 统一构造。当前主要分组: - 健康检查:`GET /healthz`。 -- 后台管理:`/admin/api/*`,包括登录、Dashboard 运营看板、概览、HTTP debug、埋点、表查询、创作入口开关、作品互动配置、作品可见性、兑换码、邀请码、任务配置和充值商品配置。Dashboard 指标口径见 [`docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md`](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)。 +- 后台管理:`/admin/api/*`,包括登录、Dashboard 运营看板、概览、HTTP debug、埋点、表查询、素材查询、创作入口开关、作品互动配置、作品可见性、兑换码、邀请码、任务配置和充值商品配置。Dashboard 指标口径见 [`docs/technical/【后台管理】Dashboard运营看板方案-2026-06-23.md`](./technical/【后台管理】Dashboard运营看板方案-2026-06-23.md)。 - 认证与账号:`/api/auth/*`、`/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。 - 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请和兑换等账号侧能力。 - 平台基础能力:`/api/llm/*`、`/api/speech/volcengine/*`,只保留通用 LLM 和语音代理。 @@ -62,6 +62,7 @@ npm run check:server-rs-ddd - 外部 OpenAPI:`/api/external/v1/openapi.json`、`/api/external/v1/assets/direct-upload-tickets`、`/api/external/v1/assets/objects/confirm`、`/api/external/v1/assets/read-url`、`/api/external/v1/editor/*`,使用 Bearer API Key 鉴权;API Key 管理仍在登录态 `/api/profile/api-keys`,不进入外部 OpenAPI JSON。 - 创作 / 游玩支撑能力:`/api/creation-entry/config`、`/api/ai/tasks*`、`/api/runtime/chat/*`、`/api/runtime/settings`、`/api/runtime/save/snapshot`、`/api/profile/browse-history`、`/api/profile/save-archives*`、`/api/profile/play-stats`、`/api/assets/history`、`/api/assets/character-visual/*`、`/api/assets/character-animation/*`、`/api/assets/character-workflow-cache*`、`/api/assets/hyper3d/*`、`/api/runtime/custom-world/asset-studio/*`、`/api/editor/projects*`。`/api/runtime/custom-world/asset-studio/*` 解析默认角色形象 / 动作提示词时可以在 OSS 缓存不可用或未配置时按无缓存返回默认提示;保存 workflow 缓存和真实素材读写仍必须要求 OSS 正常可用。 - 后台入口配置:`/admin/api/creation-entry/config`、`/admin/api/creation-entry/config/banners` 和 `/admin/api/creation-entry/config/interactions`。 +- 后台素材查询:`GET /admin/api/editor-assets` 读取账号级 `editor_asset` 中 `source_type = 'generated'` 的素材,支持 `ownerUserId`、`assetKind`、`createdAfter`、`createdBefore`、`cursor` 和 `limit`;返回缩略图 / Object Key、作者展示名、陶泥号、提示词、生成输入和生成成本,只用于查询,不执行精选审核、返还或展示状态修改。 - 自定义世界 / RPG:`/api/runtime/custom-world*`、`/api/story/*`、`/api/runtime/chat/*`。 - 拼图:`/api/runtime/puzzle/*`。 - 抓大鹅 Match3D:`/api/creation/match3d/*`、`/api/runtime/match3d/*`。 @@ -463,7 +464,7 @@ npm run check:server-rs-ddd - Rust 结构体:`EditorProjectResource` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` -- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复;`public_showcase_enabled` 控制该生成资源是否进入 `/creation` 的 `陶泥儿精选`,新增和旧行默认 `true`,但通过 `source_resource_id` 指向同一 owner 源资源且媒体引用相同的画布副本默认不公开。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot`、`source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。公开精选读取走 `GET /api/editor/showcase/resources`,只返回 `source_type = generated`、图片源非空且 `public_showcase_enabled = true` 的全站资源,并跳过同源同媒体副本,按 `created_at` / `resource_id` 倒序 cursor 分页;每页最多 36 条,响应返回 `nextCursor`,下一页通过 `?cursor=...` 继续读取。公开响应的作者展示字段优先提供 `authorDisplayName` / `display_name`,没有展示名时提供 `authorPublicUserCode` / 陶泥号作为展示兜底,前端公开展示绝不能兜底到内部 `ownerUserId` / `user_id`。公开开关修改走登录鉴权 `PATCH /api/editor/project-resources/{resource_id}/showcase`,只允许资源 owner 更新。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。 +- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind`、`generation_inputs_json` 和历史 `public_showcase_enabled`。`asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复。`public_showcase_enabled` 只保留旧接口兼容,不再作为 `/creation` 的 `陶泥儿精选` 事实源;精选公开改由账号级生成素材提交 `editor_showcase_asset` 审核决定。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot`、`source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。 - 索引:`by_editor_project_resource_project_id`、`by_editor_project_resource_owner_user_id`。 ### `editor_asset_folder` @@ -477,9 +478,30 @@ npm run check:server-rs-ddd - Rust 结构体:`EditorAsset` - 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` -- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、可选封面 `thumbnail_src`、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind`、`generation_inputs_json` 和可选 `source_resource_id`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。生成视频会抽取首帧封面写入 `thumbnail_src`,素材库和再次放入画布时用它作为 video poster。素材库快照通过 `source_resource_id` 回查对应 project resource 的 `public_showcase_enabled`,供左侧素材菜单展示和切换公开状态;公开开关本身仍只落在 `editor_project_resource`,不在账号素材表复制真相。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。 +- 说明:图片画布账号级素材表,保存用户上传 / 生成素材的名称、文件夹、图片读取地址、可选封面 `thumbnail_src`、OSS 引用、尺寸、来源类型、prompt、provider、task、`asset_kind`、`generation_inputs_json`、可选 `source_resource_id` 和 `generation_cost_mud_points`。素材在同一账号的所有项目中可见;图片 / 图标 / UI 提取等生成 BFF 在请求携带 `asset_folder_id` 时负责创建账号级生成素材并返回 asset 快照,若同次生成也创建了 `editor_project_resource`,则把该 `resource_id` 写入 `source_resource_id`。生成视频会抽取首帧封面写入 `thumbnail_src`,素材库和再次放入画布时用它作为 video poster。素材库快照通过 `asset_id` 回查对应 `editor_showcase_asset`,供左侧素材菜单展示 `pending` / `approved` / `rejected` 审核状态;公开事实不落在账号素材表,素材库只发起提交审核。素材放入画布时复制为 `editor_project_resource` 并由图层引用 resourceId,画布从 resource / asset 级元数据恢复素材类别和用户可见生成输入快照。 - 索引:`by_editor_asset_owner_user_id`、`by_editor_asset_folder_id`。 +### `editor_showcase_asset` + +- Rust 结构体:`EditorShowcaseAsset` +- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` +- 说明:`陶泥儿精选` 的独立审核与公开快照表。用户从账号级生成素材提交后,后端把素材媒体、提示词、生成输入、素材类型和 `generation_cost_mud_points` 快照到该表,初始 `review_status = pending` 且 `display_enabled = false`。后台审核通过后写入 `approved`、默认开启展示并生成确定性返还流水 `editor-showcase-refund:{showcase_id}`,BFF 按 50% 生成成本返还泥点后回写 `refund_completed_at`;拒绝后写入 `rejected`。公开精选 `GET /api/editor/showcase/resources` 只读取 `review_status = approved`、`display_enabled = true` 且媒体非空的记录,按通过时间 / `showcase_id` 倒序 cursor 分页。素材删除时,待审核记录标记 `asset_deleted_while_pending`,已拒绝记录删除,已通过记录保留快照继续展示。 +- 索引:`by_editor_showcase_asset_owner_user_id`、`by_editor_showcase_asset_review_status`。 + +### `editor_showcase_asset_like` + +- Rust 结构体:`EditorShowcaseAssetLike` +- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` +- 说明:`陶泥儿精选` 点赞去重表,主键由 `showcase_id:user_id` 组成。点赞 / 取消点赞通过登录接口更新该表,并同步回写 `editor_showcase_asset.like_count`;未通过审核或未展示的精选素材不能点赞。 +- 索引:`by_editor_showcase_asset_like_showcase_id`、`by_editor_showcase_asset_like_user_id`。 + +### `editor_showcase_campaign_config` + +- Rust 结构体:`EditorShowcaseCampaignConfig` +- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs` +- 说明:`陶泥儿精选` 首位固定活动卡配置表,当前使用固定 `config_id = default`。后台可配置启用状态、标题、图片 URL、提示词、作者和成本文案;固定活动卡不包含副标题或 Object Key,公开精选接口只在启用时返回该配置。 +- 索引:主键 `config_id`。 + ### `inventory_slot` - Rust 结构体:`InventorySlot` diff --git a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md index ec9714319..ce1fbf0d4 100644 --- a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md +++ b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md @@ -73,9 +73,9 @@ `陶泥儿精选` 是页面底部的全站公开画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/` 工作台使用,但不作为本页精选区内容。 -精选内容只使用 `editor_project_resource` 中 `sourceType="generated"` 且 `public_showcase_enabled = true` 的项目资源,代表用户通过图片画布生成、已经落入某个项目并允许公开展示的素材。账号级 `editor_asset` 只表示跨项目素材库,不单独作为精选来源;上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。从项目素材把同一个生成素材再次拖入画布时,只是新增图层实例或复用同源同媒体项目资源,不应额外生成新的精选候选;历史上已经产生的同源同媒体副本也需要在精选读取时跳过。公开 BFF 必须返回资源作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。 +精选内容只使用用户从账号级素材库主动提交、后台审核通过且展示状态开启的 `editor_showcase_asset` 快照。新生成素材不会默认公开,旧 `editor_project_resource.public_showcase_enabled` 只保留历史兼容,不再作为 `/creation` 精选事实源。账号级 `editor_asset` 仍是素材库私有事实;只有 `sourceType="generated"`、有媒体内容、提交审核并通过的素材才进入精选,上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。审核通过时按生成成本返还 50% 泥点,返还流水使用确定性 `editor-showcase-refund:{showcaseId}` 保证幂等。公开 BFF 必须返回作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。 -瀑布流通过 `GET /api/editor/showcase/resources` 按 `created_at` / `resource_id` 倒序 cursor 分页读取,每页最多 36 条;响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有瀑布流,而不是固定只展示首屏数量。 +瀑布流通过 `GET /api/editor/showcase/resources` 按通过审核时间 / `showcaseId` 倒序 cursor 分页读取,每页最多 36 条;响应有 `nextCursor` 时,页面滚动到底部继续请求 `?cursor=...` 并追加到现有瀑布流,而不是固定只展示首屏数量。响应可以额外携带后台配置的固定活动卡,用于在列表首位展示运营精选。 Tab: @@ -112,11 +112,12 @@ Tab: 数据来源: -- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_project_resource` 快照,快照包含 `authorDisplayName` / `display_name` 和 `authorPublicUserCode` / 陶泥号用于作者展示;前端只保留 `sourceType="generated"` 且 `publicShowcaseEnabled !== false` 的画布生成资源;若某条资源通过 `sourceResourceId` 指回同一 owner 的源资源且媒体引用相同,则视为拖拽画布副本,不进入精选。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`。 -- 素材包、角色、UI、音乐、音效、视频和美宣都必须来自已存在项目资源;素材库里尚未放入任何项目的账号级素材不展示。 +- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_showcase_asset` 快照,快照包含 `showcaseId`、`assetId`、审核状态、展示状态、点赞数、生成成本、返还泥点、`authorDisplayName` / `display_name` 和 `authorPublicUserCode` / 陶泥号用于展示;前端只展示 `reviewStatus="approved"` 且 `displayEnabled=true` 的后端结果。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`。 +- 素材包、角色、UI、音乐、音效、视频和美宣都必须来自用户素材库中已提交并通过审核的生成素材;不要求素材当前仍保留在某个项目资源中,已通过审核的快照在原素材删除后仍可保留公开展示。 - 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。 - 上传素材、公开作品图片、mock 资源和假组合都不进入精选。 -- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。菜单内 `公开展示该素材` 默认勾选,取消勾选后调用 `PATCH /api/editor/project-resources/{resourceId}/showcase` 写回 `public_showcase_enabled = false`,该资源从 `陶泥儿精选` 移除。 +- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。生成素材菜单内提供 `提交精选审核`,调用 `POST /api/editor/assets/{assetId}/showcase-submissions` 后进入 `pending` 状态;已提交、已通过或已拒绝的素材显示对应状态,不再显示默认打开的公开勾选项。 +- 后台新增纯素材查询页,读取账号级 `editor_asset` 中 `sourceType="generated"` 的素材,用于查看所有用户生成素材。该页只提供时间、用户 ID、分类和关键词查询,以及缩略图详情、提示词全文、生成成本、作者展示名和陶泥号查看;不承载审核、返还、展示开关或活动卡配置操作。 - 不为了填满展示区创建假素材、假作者、假泥点成本或假组合关系。 - 暂无真实数据的 Tab 保留 Tab 入口,但内容区显示简洁空态。 @@ -127,7 +128,7 @@ Tab: - 最近项目继续使用编辑器项目接口:`listEditorProjects`、`createEditorProject` 和 `/editor/canvas?projectid=xxx`。 - 最近项目和项目页打开画布时,浏览器 history 只保留带 `projectid` 的最终画布路由;新建画布引导使用 `guide=toolbar` 一次性 query,主站九宫格直达生成器使用 `tool=` 一次性 query,画布消费后通过 `history.replaceState` 清理这两个参数。 - 项目封面逻辑复用 `/project` 项目卡已有的画布中心缩略图算法。 -- `陶泥儿精选` 读取后端公开精选接口返回的全站公开画布生成资源;公开开关以后端 `editor_project_resource.public_showcase_enabled` 为准,前端只做展示过滤、同源同媒体副本兜底过滤和乐观交互回滚。 +- `陶泥儿精选` 读取后端公开精选接口返回的审核通过素材快照;公开事实以后端 `editor_showcase_asset.review_status` 和 `display_enabled` 为准,前端只做展示组合、点赞交互和提交审核的乐观状态回滚。 - `/creation/` 的玩法工作台、草稿、生成页、结果页、发布、运行态和作品架链路保持原状。 ## 路由与导航 diff --git a/server-rs/crates/api-server/src/admin.rs b/server-rs/crates/api-server/src/admin.rs index 37c1e5a4f..94f8522ec 100644 --- a/server-rs/crates/api-server/src/admin.rs +++ b/server-rs/crates/api-server/src/admin.rs @@ -25,25 +25,38 @@ use shared_contracts::admin::{ AdminDashboardResponse, AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload, AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload, AdminDebugHeaderInput, AdminDebugHttpRequest, - AdminDebugHttpResponse, AdminLoginRequest, AdminLoginResponse, AdminMeResponse, - AdminOverviewResponse, AdminServiceOverviewPayload, AdminSessionPayload, - AdminTrackingEventEntryPayload, AdminTrackingEventKeyListResponse, + AdminDebugHttpResponse, AdminEditorAssetListQuery, AdminEditorAssetListResponse, + AdminEditorAssetPayload, AdminEditorShowcaseAssetPayload, AdminEditorShowcaseAssetResponse, + AdminEditorShowcaseCampaignPayload, AdminEditorShowcaseCampaignResponse, + AdminEditorShowcaseDisplayRequest, AdminEditorShowcaseListQuery, + AdminEditorShowcaseListResponse, AdminEditorShowcaseReviewRequest, AdminLoginRequest, + AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, AdminServiceOverviewPayload, + AdminSessionPayload, AdminTrackingEventEntryPayload, AdminTrackingEventKeyListResponse, AdminTrackingEventKeyPayload, AdminTrackingEventListQuery, AdminTrackingEventListResponse, AdminUpdateWorkVisibilityRequest, AdminUpdateWorkVisibilityResponse, AdminUpsertCreationEntryEventBannersRequest, AdminUpsertCreationEntryTypeConfigRequest, - AdminUpsertPublicWorkInteractionConfigRequest, AdminWorkVisibilityListResponse, + AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertPublicWorkInteractionConfigRequest, + AdminWorkVisibilityListResponse, }; use shared_contracts::creation_entry_config::{ encode_unified_creation_spec_response, validate_unified_creation_spec_for_play, }; +use spacetime_client::{ + EditorShowcaseAssetAdminListRecordInput, EditorShowcaseAssetAdminReviewRecordInput, + EditorShowcaseAssetDisplayUpdateRecordInput, EditorShowcaseAssetRecord, + EditorShowcaseAssetRefundMarkRecordInput, EditorShowcaseCampaignConfigGetRecordInput, + EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput, +}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use crate::{ api_response::json_success_body, editor_generation_config::EditorGenerationPricingConfig, + editor_project::current_utc_micros, http_error::AppError, request_context::RequestContext, state::{AdminRuntime, AppState}, + work_author::resolve_work_author_by_user_id, }; // 首版调试台只允许有限大小的请求体,避免把后台当作通用代理大包转发器。 @@ -66,6 +79,9 @@ const ADMIN_DATABASE_TABLE_DEFAULT_LIMIT: u32 = 100; const ADMIN_DATABASE_TABLE_MAX_LIMIT: u32 = 500; const ADMIN_DASHBOARD_ROW_LIMIT: u32 = 50_000; const ADMIN_DASHBOARD_MAX_PERIOD_DAYS: i64 = 366; +const ADMIN_EDITOR_ASSET_DEFAULT_LIMIT: u32 = 80; +const ADMIN_EDITOR_ASSET_MAX_LIMIT: u32 = 200; +const ADMIN_EDITOR_ASSET_SCAN_LIMIT: u32 = 50_000; const ADMIN_DASHBOARD_ACTIVE_WINDOW_MICROS: i64 = 5 * 60 * 1_000_000; #[derive(Clone, Debug)] @@ -388,6 +404,211 @@ pub async fn admin_update_work_visibility( )) } +pub async fn admin_list_editor_assets( + State(state): State, + Extension(request_context): Extension, + Extension(_admin): Extension, + Query(query): Query, +) -> Result, AppError> { + let page_size = query + .limit + .unwrap_or(ADMIN_EDITOR_ASSET_DEFAULT_LIMIT) + .clamp(1, ADMIN_EDITOR_ASSET_MAX_LIMIT); + let date_range = resolve_admin_editor_asset_date_range(&query) + .map_err(|error| AppError::from_status(StatusCode::BAD_REQUEST).with_message(error))?; + let sql = build_admin_editor_assets_sql(&query); + let rows = fetch_admin_dashboard_rows(&state, &sql).await.map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY) + .with_message(format!("素材查询失败:{error}")) + })?; + let cursor = parse_admin_editor_asset_cursor(query.cursor.as_deref()); + let mut rows = rows + .into_iter() + .map(|row| admin_editor_asset_row_from_sql(&state, &row)) + .collect::, _>>() + .map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY) + .with_message(format!("素材查询解析失败:{error}")) + })?; + rows.retain(|row| { + date_range + .created_after_micros + .is_none_or(|value| row.created_at_micros >= value) + && date_range + .created_before_micros + .is_none_or(|value| row.created_at_micros <= value) + && is_after_admin_editor_asset_cursor( + row.created_at_micros, + row.payload.asset_id.as_str(), + cursor.as_ref(), + ) + }); + rows.sort_by(|left, right| { + right + .created_at_micros + .cmp(&left.created_at_micros) + .then_with(|| right.payload.asset_id.cmp(&left.payload.asset_id)) + }); + + let next_cursor = if rows.len() > page_size as usize { + rows.get(page_size.saturating_sub(1) as usize) + .map(admin_editor_asset_cursor) + } else { + None + }; + rows.truncate(page_size as usize); + Ok(json_success_body( + Some(&request_context), + AdminEditorAssetListResponse { + entries: rows.into_iter().map(|row| row.payload).collect(), + next_cursor, + }, + )) +} + +pub async fn admin_list_editor_showcase_assets( + State(state): State, + Extension(request_context): Extension, + Extension(_admin): Extension, + Query(query): Query, +) -> Result, AppError> { + let page_size = query.limit.unwrap_or(80).clamp(1, 200); + let mut records = state + .spacetime_client() + .admin_list_editor_showcase_assets(EditorShowcaseAssetAdminListRecordInput { + limit: page_size + 1, + cursor: normalize_admin_optional(query.cursor), + owner_user_id: normalize_admin_optional(query.owner_user_id), + asset_kind: normalize_admin_optional(query.asset_kind), + review_status: normalize_admin_optional(query.review_status), + submitted_after_micros: query + .submitted_after + .as_deref() + .and_then(parse_timestamp_text_to_micros), + submitted_before_micros: query + .submitted_before + .as_deref() + .and_then(parse_timestamp_text_to_micros), + }) + .await + .map_err(map_admin_spacetime_error)?; + let next_cursor = if records.len() > page_size as usize { + records + .get(page_size.saturating_sub(1) as usize) + .and_then(editor_showcase_admin_cursor) + } else { + None + }; + records.truncate(page_size as usize); + Ok(json_success_body( + Some(&request_context), + AdminEditorShowcaseListResponse { + entries: records + .into_iter() + .map(|record| admin_editor_showcase_asset_payload_from_record(&state, record)) + .collect(), + next_cursor, + }, + )) +} + +pub async fn admin_review_editor_showcase_asset( + State(state): State, + Extension(request_context): Extension, + Extension(admin): Extension, + Json(payload): Json, +) -> Result, AppError> { + let admin_user_id = admin.session().subject.clone(); + let record = state + .spacetime_client() + .admin_review_editor_showcase_asset(EditorShowcaseAssetAdminReviewRecordInput { + showcase_id: payload.showcase_id, + admin_user_id, + review_status: payload.review_status, + review_note: payload.review_note, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_admin_spacetime_error)?; + let record = refund_approved_editor_showcase_asset_if_needed(&state, record).await?; + Ok(json_success_body( + Some(&request_context), + AdminEditorShowcaseAssetResponse { + entry: admin_editor_showcase_asset_payload_from_record(&state, record), + }, + )) +} + +pub async fn admin_update_editor_showcase_asset_display( + State(state): State, + Extension(request_context): Extension, + Extension(admin): Extension, + Json(payload): Json, +) -> Result, AppError> { + let record = state + .spacetime_client() + .update_editor_showcase_asset_display(EditorShowcaseAssetDisplayUpdateRecordInput { + showcase_id: payload.showcase_id, + admin_user_id: admin.session().subject.clone(), + display_enabled: payload.display_enabled, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_admin_spacetime_error)?; + Ok(json_success_body( + Some(&request_context), + AdminEditorShowcaseAssetResponse { + entry: admin_editor_showcase_asset_payload_from_record(&state, record), + }, + )) +} + +pub async fn admin_get_editor_showcase_campaign( + State(state): State, + Extension(request_context): Extension, + Extension(_admin): Extension, +) -> Result, AppError> { + let campaign = state + .spacetime_client() + .get_editor_showcase_campaign_config(EditorShowcaseCampaignConfigGetRecordInput { + include_disabled: true, + }) + .await + .map_err(map_admin_spacetime_error)? + .map(admin_editor_showcase_campaign_payload_from_record); + Ok(json_success_body( + Some(&request_context), + AdminEditorShowcaseCampaignResponse { campaign }, + )) +} + +pub async fn admin_upsert_editor_showcase_campaign( + State(state): State, + Extension(request_context): Extension, + Extension(admin): Extension, + Json(payload): Json, +) -> Result, AppError> { + let campaign = state + .spacetime_client() + .upsert_editor_showcase_campaign_config(EditorShowcaseCampaignConfigUpsertRecordInput { + admin_user_id: admin.session().subject.clone(), + enabled: payload.enabled, + title: payload.title, + image_src: payload.image_src, + prompt: payload.prompt, + author: payload.author, + cost_text: payload.cost_text, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_admin_spacetime_error)? + .map(admin_editor_showcase_campaign_payload_from_record); + Ok(json_success_body( + Some(&request_context), + AdminEditorShowcaseCampaignResponse { campaign }, + )) +} + fn build_admin_creation_entry_config_response( config: shared_contracts::creation_entry_config::CreationEntryConfigResponse, ) -> AdminCreationEntryConfigResponse { @@ -422,6 +643,286 @@ fn map_admin_creation_entry_type_config( } } +async fn refund_approved_editor_showcase_asset_if_needed( + state: &AppState, + record: EditorShowcaseAssetRecord, +) -> Result { + if record.review_status != "approved" + || record.refund_mud_points == 0 + || record.refund_completed_at.is_some() + { + return Ok(record); + } + let Some(ledger_id) = record.refund_ledger_id.clone() else { + return Ok(record); + }; + state + .spacetime_client() + .refund_profile_wallet_points_with_metadata( + record.owner_user_id.clone(), + record.refund_mud_points, + ledger_id.clone(), + current_utc_micros(), + serde_json::json!({ + "source": "editor_showcase_approval", + "showcaseId": record.showcase_id, + "assetId": record.asset_id, + "generationCostMudPoints": record.generation_cost_mud_points, + }) + .to_string(), + ) + .await + .map_err(map_admin_spacetime_error)?; + state + .spacetime_client() + .mark_editor_showcase_asset_refunded(EditorShowcaseAssetRefundMarkRecordInput { + showcase_id: record.showcase_id, + refund_ledger_id: ledger_id, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_admin_spacetime_error) +} + +fn admin_editor_showcase_asset_payload_from_record( + state: &AppState, + record: EditorShowcaseAssetRecord, +) -> AdminEditorShowcaseAssetPayload { + let author = resolve_work_author_by_user_id(state, &record.owner_user_id, None, None); + AdminEditorShowcaseAssetPayload { + showcase_id: record.showcase_id, + asset_id: record.asset_id, + owner_user_id: record.owner_user_id, + author_display_name: Some(author.display_name), + author_public_user_code: author.public_user_code, + label: record.label, + image_src: record.image_src, + object_key: record.object_key, + width: record.width, + height: record.height, + prompt: record.prompt, + actual_prompt: record.actual_prompt, + model: record.model, + provider: record.provider, + task_id: record.task_id, + asset_kind: record.asset_kind, + generation_inputs: record.generation_inputs, + generation_cost_mud_points: record.generation_cost_mud_points, + refund_mud_points: record.refund_mud_points, + review_status: record.review_status, + display_enabled: record.display_enabled, + like_count: record.like_count, + asset_deleted_while_pending: record.asset_deleted_while_pending, + reviewed_by_admin_user_id: record.reviewed_by_admin_user_id, + review_note: record.review_note, + refund_ledger_id: record.refund_ledger_id, + refund_completed_at: record.refund_completed_at, + submitted_at: record.submitted_at, + reviewed_at: record.reviewed_at, + approved_at: record.approved_at, + rejected_at: record.rejected_at, + updated_at: record.updated_at, + } +} + +fn admin_editor_showcase_campaign_payload_from_record( + record: EditorShowcaseCampaignConfigRecord, +) -> AdminEditorShowcaseCampaignPayload { + AdminEditorShowcaseCampaignPayload { + enabled: record.enabled, + title: record.title, + image_src: record.image_src, + prompt: record.prompt, + author: record.author, + cost_text: record.cost_text, + updated_at: record.updated_at, + } +} + +fn editor_showcase_admin_cursor(record: &EditorShowcaseAssetRecord) -> Option { + let submitted_at_micros = parse_timestamp_text_to_micros(record.submitted_at.as_str())?; + Some(format!("{submitted_at_micros}:{}", record.showcase_id)) +} + +struct AdminEditorAssetDateRange { + created_after_micros: Option, + created_before_micros: Option, +} + +struct AdminEditorAssetQueryRow { + payload: AdminEditorAssetPayload, + created_at_micros: i64, +} + +fn resolve_admin_editor_asset_date_range( + query: &AdminEditorAssetListQuery, +) -> Result { + let created_after_micros = normalize_admin_optional(query.created_after.clone()) + .map(|value| { + parse_timestamp_text_to_micros(&value) + .ok_or_else(|| "createdAfter 格式非法".to_string()) + }) + .transpose()?; + let created_before_micros = normalize_admin_optional(query.created_before.clone()) + .map(|value| { + parse_timestamp_text_to_micros(&value) + .ok_or_else(|| "createdBefore 格式非法".to_string()) + }) + .transpose()?; + if matches!( + (created_after_micros, created_before_micros), + (Some(created_after_micros), Some(created_before_micros)) + if created_after_micros > created_before_micros + ) { + return Err("createdAfter 不能晚于 createdBefore".to_string()); + } + Ok(AdminEditorAssetDateRange { + created_after_micros, + created_before_micros, + }) +} + +fn build_admin_editor_assets_sql(query: &AdminEditorAssetListQuery) -> String { + let mut conditions = vec!["source_type = 'generated'".to_string()]; + if let Some(value) = normalize_admin_optional(query.owner_user_id.clone()) { + conditions.push(format!("owner_user_id = {}", quote_sql_string(&value))); + } + if let Some(value) = normalize_admin_optional(query.asset_kind.clone()) { + conditions.push(format!("asset_kind = {}", quote_sql_string(&value))); + } + format!( + "SELECT asset_id, owner_user_id, folder_id, label, asset_object_id, image_src, object_key, width, height, source_type, prompt, actual_prompt, model, provider, task_id, asset_kind, generation_inputs_json, source_resource_id, thumbnail_src, generation_cost_mud_points, created_at, updated_at FROM editor_asset WHERE {} LIMIT {}", + conditions.join(" AND "), + ADMIN_EDITOR_ASSET_SCAN_LIMIT + ) +} + +fn admin_editor_asset_row_from_sql( + state: &AppState, + row: &Value, +) -> Result { + let columns = row + .as_array() + .ok_or_else(|| "素材查询行格式非法".to_string())?; + let owner_user_id = required_editor_asset_string_column(columns, 1, "owner_user_id")?; + let author = resolve_work_author_by_user_id(state, &owner_user_id, None, None); + let created_at_micros = columns + .get(20) + .and_then(timestamp_value_to_micros) + .ok_or_else(|| "素材查询行 created_at 格式非法".to_string())?; + let updated_at_micros = columns + .get(21) + .and_then(timestamp_value_to_micros) + .ok_or_else(|| "素材查询行 updated_at 格式非法".to_string())?; + let generation_cost_mud_points = columns + .get(19) + .and_then(value_to_u64) + .ok_or_else(|| "素材查询行 generation_cost_mud_points 格式非法".to_string())?; + Ok(AdminEditorAssetQueryRow { + payload: AdminEditorAssetPayload { + asset_id: required_editor_asset_string_column(columns, 0, "asset_id")?, + owner_user_id, + author_display_name: Some(author.display_name), + author_public_user_code: author.public_user_code, + folder_id: required_editor_asset_string_column(columns, 2, "folder_id")?, + label: required_editor_asset_string_column(columns, 3, "label")?, + asset_object_id: optional_string_column(columns, 4), + image_src: required_editor_asset_string_column(columns, 5, "image_src")?, + object_key: optional_string_column(columns, 6), + width: required_u32_column(columns, 7, "width")?, + height: required_u32_column(columns, 8, "height")?, + source_type: required_editor_asset_string_column(columns, 9, "source_type")?, + prompt: optional_string_column(columns, 10), + actual_prompt: optional_string_column(columns, 11), + model: optional_string_column(columns, 12), + provider: optional_string_column(columns, 13), + task_id: optional_string_column(columns, 14), + asset_kind: optional_string_column(columns, 15), + generation_inputs: optional_json_column(columns, 16, "generation_inputs_json")?, + source_resource_id: optional_string_column(columns, 17), + thumbnail_src: optional_string_column(columns, 18), + generation_cost_mud_points, + created_at: timestamp_micros_to_rfc3339(created_at_micros), + updated_at: timestamp_micros_to_rfc3339(updated_at_micros), + }, + created_at_micros, + }) +} + +fn required_editor_asset_string_column( + columns: &[Value], + index: usize, + field_name: &str, +) -> Result { + value_to_string( + columns + .get(index) + .ok_or_else(|| format!("素材查询行缺少 {field_name}"))?, + ) + .ok_or_else(|| format!("素材查询行 {field_name} 不是字符串")) +} + +fn required_u32_column(columns: &[Value], index: usize, field_name: &str) -> Result { + columns + .get(index) + .and_then(value_to_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| format!("素材查询行 {field_name} 格式非法")) +} + +fn optional_json_column( + columns: &[Value], + index: usize, + field_name: &str, +) -> Result, String> { + let Some(text) = optional_string_column(columns, index) else { + return Ok(None); + }; + serde_json::from_str(&text) + .map(Some) + .map_err(|error| format!("素材查询行 {field_name} 解析失败:{error}")) +} + +fn timestamp_micros_to_rfc3339(value: i64) -> String { + OffsetDateTime::from_unix_timestamp_nanos(i128::from(value) * 1_000) + .ok() + .and_then(|timestamp| timestamp.format(&Rfc3339).ok()) + .unwrap_or_else(|| "1970-01-01T00:00:00Z".to_string()) +} + +fn parse_admin_editor_asset_cursor(cursor: Option<&str>) -> Option<(i64, String)> { + let (created_at_micros, asset_id) = cursor?.trim().split_once(':')?; + let created_at_micros = created_at_micros.parse::().ok()?; + let asset_id = asset_id.trim(); + if asset_id.is_empty() { + return None; + } + Some((created_at_micros, asset_id.to_string())) +} + +fn is_after_admin_editor_asset_cursor( + created_at_micros: i64, + asset_id: &str, + cursor: Option<&(i64, String)>, +) -> bool { + let Some((cursor_created_at_micros, cursor_asset_id)) = cursor else { + return true; + }; + created_at_micros < *cursor_created_at_micros + || (created_at_micros == *cursor_created_at_micros + && asset_id < cursor_asset_id.as_str()) +} + +fn admin_editor_asset_cursor(row: &AdminEditorAssetQueryRow) -> String { + format!("{}:{}", row.created_at_micros, row.payload.asset_id) +} + +fn normalize_admin_optional(value: Option) -> Option { + value + .map(|item| item.trim().to_string()) + .filter(|item| !item.is_empty()) +} + fn validate_admin_creation_entry_config( payload: AdminUpsertCreationEntryTypeConfigRequest, ) -> Result { @@ -2628,6 +3129,7 @@ fn build_admin_session_payload(session: crate::state::AdminSession) -> AdminSess mod tests { use super::{ AdminDashboardGranularity, AdminDashboardRegistrationStats, AdminDashboardSeries, + EditorShowcaseAssetRecord, admin_editor_showcase_asset_payload_from_record, apply_admin_dashboard_registration_rows, apply_admin_database_table_filters, build_admin_database_table_row, build_admin_tracking_event_keys_sql, build_admin_tracking_events_sql, build_body_preview, build_debug_base_url, @@ -2644,6 +3146,65 @@ mod tests { AdminDashboardQuery, AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery, }; + use crate::{config::AppConfig, state::AppState}; + + fn test_admin_editor_showcase_asset_record(owner_user_id: &str) -> EditorShowcaseAssetRecord { + EditorShowcaseAssetRecord { + showcase_id: "showcase-1".to_string(), + asset_id: "asset-1".to_string(), + owner_user_id: owner_user_id.to_string(), + label: "精选素材".to_string(), + asset_object_id: None, + image_src: "/generated-character-drafts/editor/spec.png".to_string(), + object_key: Some("generated-character-drafts/editor/spec.png".to_string()), + width: 1024, + height: 1024, + source_type: "generated".to_string(), + prompt: None, + actual_prompt: None, + model: None, + provider: None, + task_id: None, + source_resource_id: None, + asset_kind: Some("character".to_string()), + generation_inputs: None, + thumbnail_src: None, + generation_cost_mud_points: 12, + refund_mud_points: 6, + review_status: "pending".to_string(), + display_enabled: false, + like_count: 0, + asset_deleted_while_pending: false, + reviewed_by_admin_user_id: None, + review_note: None, + refund_ledger_id: None, + refund_completed_at: None, + submitted_at: "2026-07-04T10:00:00.000Z".to_string(), + reviewed_at: None, + approved_at: None, + rejected_at: None, + updated_at: "2026-07-04T10:00:00.000Z".to_string(), + } + } + + #[test] + fn admin_editor_showcase_asset_payload_resolves_author_profile() { + let state = AppState::new(AppConfig::default()).expect("state should build"); + state + .auth_user_service() + .ensure_orphan_work_owner_user("user-author-1", "author-1", "作者昵称", "SY-00000042") + .expect("author should be inserted"); + + let payload = admin_editor_showcase_asset_payload_from_record( + &state, + test_admin_editor_showcase_asset_record("user-author-1"), + ); + + assert_eq!(payload.owner_user_id, "user-author-1"); + assert_eq!(payload.author_display_name.as_deref(), Some("作者昵称")); + assert_eq!(payload.author_public_user_code.as_deref(), Some("SY-00000042")); + } + #[test] fn normalize_debug_path_rejects_absolute_url() { let error = diff --git a/server-rs/crates/api-server/src/character_animation_assets.rs b/server-rs/crates/api-server/src/character_animation_assets.rs index fb2430fb8..1032c3db1 100644 --- a/server-rs/crates/api-server/src/character_animation_assets.rs +++ b/server-rs/crates/api-server/src/character_animation_assets.rs @@ -904,6 +904,7 @@ pub(crate) async fn generate_editor_video_for_owner( asset_kind: Some(asset_kind.clone()), generation_inputs: generation_inputs.clone(), thumbnail_src: generated.thumbnail_src.clone(), + generation_cost_mud_points: u64::from(normalized.price_mud_points), }, ) .await diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 41f8ef617..1a5876a1b 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -33,8 +33,11 @@ use spacetime_client::{ EditorProjectDeleteRecordInput, EditorProjectGetRecordInput, EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput, - EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord, - EditorProjectResourceShowcaseUpdateRecordInput, SpacetimeClientError, + EditorProjectResourceRecord, EditorProjectResourceShowcaseUpdateRecordInput, + EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput, + EditorShowcaseAssetRecord, EditorShowcaseAssetSubmitRecordInput, + EditorShowcaseCampaignConfigGetRecordInput, EditorShowcaseCampaignConfigRecord, + SpacetimeClientError, }; use crate::{ @@ -337,6 +340,8 @@ pub struct EditorProjectResourceResponse { pub struct EditorProjectResourceListResponse { resources: Vec, next_cursor: Option, + #[serde(skip_serializing_if = "Option::is_none")] + campaign: Option, } #[derive(Debug, Deserialize)] @@ -369,6 +374,18 @@ pub struct EditorAssetResponse { asset: EditorAssetPayload, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorShowcaseAssetResponse { + showcase_asset: EditorProjectResourcePayload, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorShowcaseAssetLikeRequest { + liked: bool, +} + #[derive(Debug)] pub(crate) struct EditorGeneratedAssetRecord { pub(crate) resource: Option, @@ -400,6 +417,7 @@ pub(crate) struct PersistEditorGeneratedAssetInput { pub(crate) asset_kind: Option, pub(crate) generation_inputs: Option, pub(crate) thumbnail_src: Option, + pub(crate) generation_cost_mud_points: u64, } pub(crate) struct PersistEditorGeneratedAssetRequest { @@ -421,6 +439,7 @@ pub(crate) struct PersistEditorGeneratedAssetRequest { pub(crate) asset_kind: Option, pub(crate) generation_inputs: Option, pub(crate) thumbnail_src: Option, + pub(crate) generation_cost_mud_points: u64, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -550,6 +569,12 @@ pub struct EditorCanvasPayload { #[serde(rename_all = "camelCase")] pub struct EditorProjectResourcePayload { pub(crate) resource_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + showcase_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + asset_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + label: Option, project_id: String, owner_user_id: String, #[serde(skip_serializing_if = "Option::is_none")] @@ -571,10 +596,31 @@ pub struct EditorProjectResourcePayload { asset_kind: Option, generation_inputs: Option, public_showcase_enabled: bool, + #[serde(skip_serializing_if = "Option::is_none")] + review_status: Option, + #[serde(skip_serializing_if = "Option::is_none")] + display_enabled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + like_count: Option, + generation_cost_mud_points: u64, + #[serde(skip_serializing_if = "Option::is_none")] + refund_mud_points: Option, created_at: String, updated_at: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditorShowcaseCampaignPayload { + enabled: bool, + title: String, + image_src: String, + prompt: String, + author: String, + cost_text: String, + updated_at: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct EditorAssetLibraryPayload { @@ -616,6 +662,11 @@ pub struct EditorAssetPayload { source_resource_id: Option, public_showcase_enabled: Option, thumbnail_src: Option, + generation_cost_mud_points: u64, + showcase_id: Option, + showcase_review_status: Option, + showcase_display_enabled: Option, + showcase_like_count: Option, created_at: String, updated_at: String, } @@ -689,7 +740,7 @@ pub async fn list_public_editor_project_resources( ) -> Result, AppError> { let mut resource_records = state .spacetime_client() - .list_public_editor_project_resources(EditorProjectResourcePublicShowcaseListRecordInput { + .list_public_editor_showcase_assets(EditorShowcaseAssetPublicListRecordInput { limit: (EDITOR_SHOWCASE_RESOURCE_PAGE_SIZE + 1) as u32, cursor: normalize_optional_string(query.cursor), }) @@ -708,19 +759,32 @@ pub async fn list_public_editor_project_resources( .into_iter() .map(|record| editor_project_resource_public_payload_from_record(&state, record)) .collect(); + let campaign = state + .spacetime_client() + .get_editor_showcase_campaign_config(EditorShowcaseCampaignConfigGetRecordInput { + include_disabled: false, + }) + .await + .map_err(map_editor_project_error)? + .map(editor_showcase_campaign_payload_from_record); Ok(json_success_body( Some(&request_context), EditorProjectResourceListResponse { resources, next_cursor, + campaign, }, )) } -fn editor_project_resource_showcase_cursor(record: &EditorProjectResourceRecord) -> Option { - let created_at_micros = parse_editor_timestamp_micros(record.created_at.as_str())?; - Some(format!("{created_at_micros}:{}", record.resource_id)) +fn editor_project_resource_showcase_cursor(record: &EditorShowcaseAssetRecord) -> Option { + let created_at = record + .approved_at + .as_deref() + .unwrap_or(record.updated_at.as_str()); + let created_at_micros = parse_editor_timestamp_micros(created_at)?; + Some(format!("{created_at_micros}:{}", record.showcase_id)) } fn parse_editor_timestamp_micros(value: &str) -> Option { @@ -1069,6 +1133,7 @@ pub async fn create_editor_asset( source_resource_id: None, now_micros: current_utc_micros(), thumbnail_src: None, + generation_cost_mud_points: 0, }) .await .map_err(map_editor_project_error)?; @@ -1131,6 +1196,56 @@ pub async fn delete_editor_asset( )) } +pub async fn submit_editor_asset_showcase( + State(state): State, + Path(asset_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, +) -> Result, AppError> { + let record = state + .spacetime_client() + .submit_editor_showcase_asset(EditorShowcaseAssetSubmitRecordInput { + asset_id, + owner_user_id: current_owner_user_id(&authenticated), + now_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorShowcaseAssetResponse { + showcase_asset: editor_project_resource_payload_from_showcase_record(record), + }, + )) +} + +pub async fn toggle_editor_showcase_asset_like( + State(state): State, + Path(showcase_id): Path, + Extension(request_context): Extension, + Extension(authenticated): Extension, + Json(payload): Json, +) -> Result, AppError> { + let record = state + .spacetime_client() + .toggle_editor_showcase_asset_like(EditorShowcaseAssetLikeToggleRecordInput { + showcase_id, + user_id: current_owner_user_id(&authenticated), + liked: payload.liked, + updated_at_micros: current_utc_micros(), + }) + .await + .map_err(map_editor_project_error)?; + + Ok(json_success_body( + Some(&request_context), + EditorShowcaseAssetResponse { + showcase_asset: editor_project_resource_payload_from_showcase_record(record), + }, + )) +} + pub async fn generate_editor_image( State(state): State, Extension(request_context): Extension, @@ -1418,6 +1533,7 @@ pub(crate) async fn generate_editor_image_for_owner( asset_kind: payload.asset_kind, generation_inputs: payload.generation_inputs, thumbnail_src: None, + generation_cost_mud_points: u64::from(configured_price_mud_points), }, ) .await?; @@ -1826,6 +1942,7 @@ pub(crate) async fn edit_editor_image_for_owner( asset_kind: payload.asset_kind, generation_inputs: payload.generation_inputs, thumbnail_src: None, + generation_cost_mud_points: u64::from(expected_price_mud_points), }, ) .await?; @@ -1953,6 +2070,7 @@ pub(crate) async fn remove_editor_image_background_for_owner( asset_kind: payload.asset_kind, generation_inputs: payload.generation_inputs, thumbnail_src: None, + generation_cost_mud_points: 0, }, ) .await?; @@ -2420,6 +2538,7 @@ pub(crate) async fn generate_editor_icon_spritesheet_for_owner( asset_kind: Some("icon-spritesheet".to_string()), generation_inputs: generation_inputs.clone(), thumbnail_src: None, + generation_cost_mud_points: u64::from(expected_price_mud_points), }, ) .await?; @@ -2673,6 +2792,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( asset_kind: Some("icon-spritesheet".to_string()), generation_inputs: generation_inputs.clone(), thumbnail_src: None, + generation_cost_mud_points: u64::from(expected_price_mud_points), }, ) .await?; @@ -2724,6 +2844,7 @@ pub(crate) async fn extract_editor_ui_design_assets_for_owner( asset_kind: Some("icon".to_string()), generation_inputs: generation_inputs.clone(), thumbnail_src: None, + generation_cost_mud_points: 0, }, ) .await?; @@ -3045,6 +3166,9 @@ pub(crate) fn editor_project_resource_payload_from_record( normalize_editor_record_media_src(record.image_src, record.object_key.as_deref()); EditorProjectResourcePayload { resource_id: record.resource_id, + showcase_id: None, + asset_id: None, + label: None, project_id: record.project_id, owner_user_id: record.owner_user_id, author_display_name: None, @@ -3066,16 +3190,62 @@ pub(crate) fn editor_project_resource_payload_from_record( .generation_inputs .map(sanitize_editor_payload_inline_media), public_showcase_enabled: record.public_showcase_enabled, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: record.created_at, updated_at: record.updated_at, } } +fn editor_project_resource_payload_from_showcase_record( + record: EditorShowcaseAssetRecord, +) -> EditorProjectResourcePayload { + let image_src = + normalize_editor_record_media_src(record.image_src, record.object_key.as_deref()); + EditorProjectResourcePayload { + resource_id: record.showcase_id.clone(), + showcase_id: Some(record.showcase_id), + asset_id: Some(record.asset_id), + label: Some(record.label), + project_id: "editor-showcase".to_string(), + owner_user_id: record.owner_user_id, + author_display_name: None, + author_public_user_code: None, + image_src, + object_key: record.object_key, + asset_object_id: record.asset_object_id, + width: record.width, + height: record.height, + source_type: record.source_type, + prompt: record.prompt, + actual_prompt: record.actual_prompt, + model: record.model, + provider: record.provider, + task_id: record.task_id, + source_resource_id: record.source_resource_id, + asset_kind: record.asset_kind, + generation_inputs: record + .generation_inputs + .map(sanitize_editor_payload_inline_media), + public_showcase_enabled: record.display_enabled, + review_status: Some(record.review_status), + display_enabled: Some(record.display_enabled), + like_count: Some(record.like_count), + generation_cost_mud_points: record.generation_cost_mud_points, + refund_mud_points: Some(record.refund_mud_points), + created_at: record.submitted_at, + updated_at: record.updated_at, + } +} + fn editor_project_resource_public_payload_from_record( state: &AppState, - record: EditorProjectResourceRecord, + record: EditorShowcaseAssetRecord, ) -> EditorProjectResourcePayload { - let mut payload = editor_project_resource_payload_from_record(record); + let mut payload = editor_project_resource_payload_from_showcase_record(record); let (display_name, public_user_code) = resolve_editor_project_resource_public_author(state, payload.owner_user_id.as_str()); payload.author_display_name = display_name; @@ -3159,11 +3329,30 @@ pub(crate) fn editor_asset_payload_from_record(record: EditorAssetRecord) -> Edi source_resource_id: record.source_resource_id, public_showcase_enabled: record.public_showcase_enabled, thumbnail_src: normalize_optional_string(record.thumbnail_src), + generation_cost_mud_points: record.generation_cost_mud_points, + showcase_id: record.showcase_id, + showcase_review_status: record.showcase_review_status, + showcase_display_enabled: record.showcase_display_enabled, + showcase_like_count: record.showcase_like_count, created_at: record.created_at, updated_at: record.updated_at, } } +fn editor_showcase_campaign_payload_from_record( + record: EditorShowcaseCampaignConfigRecord, +) -> EditorShowcaseCampaignPayload { + EditorShowcaseCampaignPayload { + enabled: record.enabled, + title: record.title, + image_src: record.image_src, + prompt: record.prompt, + author: record.author, + cost_text: record.cost_text, + updated_at: record.updated_at, + } +} + #[derive(Clone, Debug)] struct EditorPayloadMediaReference { image_src: String, @@ -3348,6 +3537,7 @@ pub(crate) async fn persist_editor_generated_asset( asset_kind: normalize_optional_string(input.asset_kind), generation_inputs_json, source_resource_id: created_resource_id, + generation_cost_mud_points: input.generation_cost_mud_points, now_micros, thumbnail_src: normalize_optional_string(input.thumbnail_src), }) @@ -3392,6 +3582,7 @@ pub(crate) async fn persist_editor_generated_media_asset( asset_kind: input.asset_kind, generation_inputs: input.generation_inputs, thumbnail_src: input.thumbnail_src, + generation_cost_mud_points: input.generation_cost_mud_points, }, ) .await?; @@ -4991,6 +5182,45 @@ mod tests { ); } + fn test_editor_showcase_asset_record(owner_user_id: &str) -> EditorShowcaseAssetRecord { + EditorShowcaseAssetRecord { + showcase_id: "showcase-1".to_string(), + asset_id: "asset-1".to_string(), + owner_user_id: owner_user_id.to_string(), + label: "精选素材".to_string(), + asset_object_id: None, + image_src: "/generated-character-drafts/editor/spec.png".to_string(), + object_key: None, + width: 512, + height: 512, + source_type: "generated".to_string(), + prompt: None, + actual_prompt: None, + model: None, + provider: None, + task_id: None, + source_resource_id: None, + asset_kind: None, + generation_inputs: None, + thumbnail_src: None, + generation_cost_mud_points: 10, + refund_mud_points: 5, + review_status: "approved".to_string(), + display_enabled: true, + like_count: 0, + asset_deleted_while_pending: false, + reviewed_by_admin_user_id: Some("admin-1".to_string()), + review_note: None, + refund_ledger_id: Some("editor-showcase-refund:showcase-1".to_string()), + refund_completed_at: Some("2026-06-23T00:00:00.000Z".to_string()), + submitted_at: "2026-06-23T00:00:00.000Z".to_string(), + reviewed_at: Some("2026-06-23T00:00:00.000Z".to_string()), + approved_at: Some("2026-06-23T00:00:00.000Z".to_string()), + rejected_at: None, + updated_at: "2026-06-23T00:00:00.000Z".to_string(), + } + } + #[test] fn editor_project_resource_payload_includes_owner_user_id() { let payload = editor_project_resource_payload_from_record(EditorProjectResourceRecord { @@ -5023,27 +5253,11 @@ mod tests { #[test] fn editor_project_resource_showcase_cursor_uses_microsecond_timestamp() { - let record = EditorProjectResourceRecord { - resource_id: "resource-1".to_string(), - project_id: "project-1".to_string(), - owner_user_id: "user-author-1".to_string(), - asset_object_id: None, - image_src: "/generated-character-drafts/editor/spec.png".to_string(), - object_key: None, - width: 512, - height: 512, - source_type: "generated".to_string(), - prompt: None, - actual_prompt: None, - model: None, - provider: None, - task_id: None, - source_resource_id: None, - asset_kind: None, - generation_inputs: None, - public_showcase_enabled: true, - created_at: "1713686401.234567Z".to_string(), + let record = EditorShowcaseAssetRecord { + showcase_id: "resource-1".to_string(), + approved_at: Some("1713686401.234567Z".to_string()), updated_at: "1713686401.234567Z".to_string(), + ..test_editor_showcase_asset_record("user-author-1") }; assert_eq!( @@ -5063,28 +5277,7 @@ mod tests { let payload = editor_project_resource_public_payload_from_record( &state, - EditorProjectResourceRecord { - resource_id: "resource-1".to_string(), - project_id: "project-1".to_string(), - owner_user_id: "user-author-1".to_string(), - asset_object_id: None, - image_src: "/generated-character-drafts/editor/spec.png".to_string(), - object_key: None, - width: 512, - height: 512, - source_type: "generated".to_string(), - prompt: None, - actual_prompt: None, - model: None, - provider: None, - task_id: None, - source_resource_id: None, - asset_kind: None, - generation_inputs: None, - public_showcase_enabled: true, - created_at: "2026-06-23T00:00:00.000Z".to_string(), - updated_at: "2026-06-23T00:00:00.000Z".to_string(), - }, + test_editor_showcase_asset_record("user-author-1"), ); let value = serde_json::to_value(payload).expect("payload should serialize"); @@ -5099,28 +5292,7 @@ mod tests { let state = AppState::new(AppConfig::default()).expect("state should build"); let payload = editor_project_resource_public_payload_from_record( &state, - EditorProjectResourceRecord { - resource_id: "resource-1".to_string(), - project_id: "project-1".to_string(), - owner_user_id: "missing-user".to_string(), - asset_object_id: None, - image_src: "/generated-character-drafts/editor/spec.png".to_string(), - object_key: None, - width: 512, - height: 512, - source_type: "generated".to_string(), - prompt: None, - actual_prompt: None, - model: None, - provider: None, - task_id: None, - source_resource_id: None, - asset_kind: None, - generation_inputs: None, - public_showcase_enabled: true, - created_at: "2026-06-23T00:00:00.000Z".to_string(), - updated_at: "2026-06-23T00:00:00.000Z".to_string(), - }, + test_editor_showcase_asset_record("missing-user"), ); let value = serde_json::to_value(payload).expect("payload should serialize"); @@ -5134,6 +5306,9 @@ mod tests { fn editor_payload_media_sanitizer_replaces_inline_layer_src_from_resource() { let resources = vec![EditorProjectResourcePayload { resource_id: "resource-1".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5153,6 +5328,11 @@ mod tests { asset_kind: Some("spec".to_string()), generation_inputs: None, public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-23T00:00:00.000Z".to_string(), updated_at: "2026-06-23T00:00:00.000Z".to_string(), }]; @@ -5208,6 +5388,9 @@ mod tests { fn editor_canvas_generation_completion_saves_after_sanitizing_legacy_inline_layers() { let existing_resources = vec![EditorProjectResourcePayload { resource_id: "resource-spec".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5227,6 +5410,11 @@ mod tests { asset_kind: Some("spec".to_string()), generation_inputs: None, public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-23T00:00:00.000Z".to_string(), updated_at: "2026-06-23T00:00:00.000Z".to_string(), }]; @@ -5278,6 +5466,9 @@ mod tests { }; let generated_resource = EditorProjectResourcePayload { resource_id: "editor-resource-character".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "editor-project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5297,6 +5488,11 @@ mod tests { asset_kind: Some("character".to_string()), generation_inputs: None, public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-23T00:00:00.000Z".to_string(), updated_at: "2026-06-23T00:00:00.000Z".to_string(), }; @@ -5626,6 +5822,9 @@ mod tests { }; let resource = EditorProjectResourcePayload { resource_id: "editor-resource-character".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "editor-project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5648,6 +5847,11 @@ mod tests { "references": [] })), public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-21T12:16:22.000Z".to_string(), updated_at: "2026-06-21T12:16:22.000Z".to_string(), }; @@ -5719,6 +5923,9 @@ mod tests { ]); let resource = EditorProjectResourcePayload { resource_id: "editor-resource-cutout".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "editor-project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5744,6 +5951,11 @@ mod tests { "references": [] })), public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-29T00:00:00.000Z".to_string(), updated_at: "2026-06-29T00:00:00.000Z".to_string(), }; @@ -5810,6 +6022,9 @@ mod tests { }; let resource = EditorProjectResourcePayload { resource_id: "editor-resource-character".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "editor-project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5829,6 +6044,11 @@ mod tests { asset_kind: Some("character".to_string()), generation_inputs: None, public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-21T12:16:22.000Z".to_string(), updated_at: "2026-06-21T12:16:22.000Z".to_string(), }; @@ -5894,6 +6114,9 @@ mod tests { }; let resource = EditorProjectResourcePayload { resource_id: "editor-resource-character".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "editor-project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -5913,6 +6136,11 @@ mod tests { asset_kind: Some("character".to_string()), generation_inputs: None, public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-21T12:16:22.000Z".to_string(), updated_at: "2026-06-21T12:16:22.000Z".to_string(), }; @@ -6019,6 +6247,9 @@ mod tests { }; let resource = EditorProjectResourcePayload { resource_id: "editor-resource-character".to_string(), + showcase_id: None, + asset_id: None, + label: None, project_id: "editor-project-1".to_string(), owner_user_id: "user-1".to_string(), author_display_name: None, @@ -6038,6 +6269,11 @@ mod tests { asset_kind: Some("character".to_string()), generation_inputs: None, public_showcase_enabled: true, + review_status: None, + display_enabled: None, + like_count: None, + generation_cost_mud_points: 0, + refund_mud_points: None, created_at: "2026-06-21T12:16:22.000Z".to_string(), updated_at: "2026-06-21T12:16:22.000Z".to_string(), }; @@ -6165,6 +6401,11 @@ mod tests { generation_inputs: None, source_resource_id: None, public_showcase_enabled: Some(true), + generation_cost_mud_points: 0, + showcase_id: None, + showcase_review_status: None, + showcase_display_enabled: None, + showcase_like_count: None, created_at: "2026-06-23T00:00:00.000Z".to_string(), updated_at: "2026-06-23T00:00:00.000Z".to_string(), }; diff --git a/server-rs/crates/api-server/src/external_editor_api.rs b/server-rs/crates/api-server/src/external_editor_api.rs index 5c030afb6..eb5ac1a69 100644 --- a/server-rs/crates/api-server/src/external_editor_api.rs +++ b/server-rs/crates/api-server/src/external_editor_api.rs @@ -491,6 +491,7 @@ pub async fn create_external_editor_asset( asset_kind: normalize_optional_string(payload.asset_kind), generation_inputs_json, source_resource_id: None, + generation_cost_mud_points: 0, now_micros: current_utc_micros(), thumbnail_src: None, }) diff --git a/server-rs/crates/api-server/src/modules/admin.rs b/server-rs/crates/api-server/src/modules/admin.rs index 5f4c5558e..025500d85 100644 --- a/server-rs/crates/api-server/src/modules/admin.rs +++ b/server-rs/crates/api-server/src/modules/admin.rs @@ -6,11 +6,14 @@ use axum::{ use crate::{ admin::{ admin_dashboard, admin_debug_http, admin_get_creation_entry_config, - admin_get_editor_generation_pricing, admin_list_database_table_rows, - admin_list_database_tables, admin_list_tracking_event_keys, admin_list_tracking_events, - admin_list_work_visibility, admin_login, admin_me, admin_overview, - admin_update_work_visibility, admin_upsert_creation_entry_config, - admin_upsert_creation_entry_event_banners_config, admin_upsert_editor_generation_pricing, + admin_get_editor_generation_pricing, admin_get_editor_showcase_campaign, + admin_list_database_table_rows, admin_list_database_tables, + admin_list_editor_assets, admin_list_editor_showcase_assets, admin_list_tracking_event_keys, + admin_list_tracking_events, admin_list_work_visibility, admin_login, admin_me, + admin_overview, admin_review_editor_showcase_asset, + admin_update_editor_showcase_asset_display, admin_update_work_visibility, + admin_upsert_creation_entry_config, admin_upsert_creation_entry_event_banners_config, + admin_upsert_editor_generation_pricing, admin_upsert_editor_showcase_campaign, admin_upsert_public_work_interaction_config, require_admin_auth, }, runtime_profile::{ @@ -113,6 +116,42 @@ pub fn router(state: AppState) -> Router { require_admin_auth, )), ) + .route( + "/admin/api/editor-assets", + get(admin_list_editor_assets).route_layer(middleware::from_fn_with_state( + state.clone(), + require_admin_auth, + )), + ) + .route( + "/admin/api/editor-showcase/assets", + get(admin_list_editor_showcase_assets).route_layer(middleware::from_fn_with_state( + state.clone(), + require_admin_auth, + )), + ) + .route( + "/admin/api/editor-showcase/assets/review", + post(admin_review_editor_showcase_asset).route_layer(middleware::from_fn_with_state( + state.clone(), + require_admin_auth, + )), + ) + .route( + "/admin/api/editor-showcase/assets/display", + post(admin_update_editor_showcase_asset_display).route_layer( + middleware::from_fn_with_state(state.clone(), require_admin_auth), + ), + ) + .route( + "/admin/api/editor-showcase/campaign", + get(admin_get_editor_showcase_campaign) + .post(admin_upsert_editor_showcase_campaign) + .route_layer(middleware::from_fn_with_state( + state.clone(), + require_admin_auth, + )), + ) .route( "/admin/api/works/visibility", get(admin_list_work_visibility) diff --git a/server-rs/crates/api-server/src/modules/editor_project.rs b/server-rs/crates/api-server/src/modules/editor_project.rs index 977c27b4b..1a6a381a1 100644 --- a/server-rs/crates/api-server/src/modules/editor_project.rs +++ b/server-rs/crates/api-server/src/modules/editor_project.rs @@ -15,7 +15,8 @@ use crate::{ get_editor_generation_pricing, get_editor_project, list_editor_projects, list_public_editor_project_resources, load_recent_editor_project, remove_editor_image_background, rename_editor_project, save_editor_project_layout, - update_editor_asset, update_editor_asset_folder, update_editor_project_resource_showcase, + submit_editor_asset_showcase, toggle_editor_showcase_asset_like, update_editor_asset, + update_editor_asset_folder, update_editor_project_resource_showcase, }, state::AppState, }; @@ -117,6 +118,20 @@ pub fn router(state: AppState) -> Router { require_bearer_auth, )), ) + .route( + "/api/editor/assets/{asset_id}/showcase-submissions", + post(submit_editor_asset_showcase).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) + .route( + "/api/editor/showcase/assets/{showcase_id}/likes", + post(toggle_editor_showcase_asset_like).route_layer(middleware::from_fn_with_state( + state.clone(), + require_bearer_auth, + )), + ) .route( "/api/editor/images/generations", post(generate_editor_image) diff --git a/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs b/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs index 717cec731..0d3b9f5da 100644 --- a/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs +++ b/server-rs/crates/api-server/src/vector_engine_audio_generation/generation.rs @@ -268,6 +268,7 @@ pub(crate) async fn generate_editor_sound_effect_for_owner( asset_kind: Some("sound-effect".to_string()), generation_inputs: generation_inputs.clone(), thumbnail_src: None, + generation_cost_mud_points: u64::from(normalized.price_mud_points), }, ) .await @@ -486,6 +487,7 @@ pub(crate) async fn generate_editor_background_music_for_owner( asset_kind: Some("background-music".to_string()), generation_inputs: generation_inputs.clone(), thumbnail_src: None, + generation_cost_mud_points: u64::from(normalized.price_mud_points), }, ) .await diff --git a/server-rs/crates/shared-contracts/src/admin.rs b/server-rs/crates/shared-contracts/src/admin.rs index db2325df9..58e78829c 100644 --- a/server-rs/crates/shared-contracts/src/admin.rs +++ b/server-rs/crates/shared-contracts/src/admin.rs @@ -123,6 +123,159 @@ pub struct AdminUpdateWorkVisibilityResponse { pub entry: AdminWorkVisibilityEntryPayload, } +#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorAssetListQuery { + pub cursor: Option, + pub owner_user_id: Option, + pub asset_kind: Option, + pub created_after: Option, + pub created_before: Option, + pub limit: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorAssetPayload { + pub asset_id: String, + pub owner_user_id: String, + pub author_display_name: Option, + pub author_public_user_code: Option, + pub folder_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub asset_kind: Option, + pub generation_inputs: Option, + pub source_resource_id: Option, + pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorAssetListResponse { + pub entries: Vec, + pub next_cursor: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, Default, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseListQuery { + pub cursor: Option, + pub owner_user_id: Option, + pub asset_kind: Option, + pub review_status: Option, + pub submitted_after: Option, + pub submitted_before: Option, + pub limit: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseAssetPayload { + pub showcase_id: String, + pub asset_id: String, + pub owner_user_id: String, + pub author_display_name: Option, + pub author_public_user_code: Option, + pub label: String, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub asset_kind: Option, + pub generation_inputs: Option, + pub generation_cost_mud_points: u64, + pub refund_mud_points: u64, + pub review_status: String, + pub display_enabled: bool, + pub like_count: u64, + pub asset_deleted_while_pending: bool, + pub reviewed_by_admin_user_id: Option, + pub review_note: Option, + pub refund_ledger_id: Option, + pub refund_completed_at: Option, + pub submitted_at: String, + pub reviewed_at: Option, + pub approved_at: Option, + pub rejected_at: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseListResponse { + pub entries: Vec, + pub next_cursor: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseReviewRequest { + pub showcase_id: String, + pub review_status: String, + pub review_note: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseDisplayRequest { + pub showcase_id: String, + pub display_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseAssetResponse { + pub entry: AdminEditorShowcaseAssetPayload, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseCampaignPayload { + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminEditorShowcaseCampaignResponse { + pub campaign: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminUpsertEditorShowcaseCampaignRequest { + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct AdminLoginResponse { @@ -411,3 +564,55 @@ pub struct AdminTrackingEventKeyPayload { pub struct AdminTrackingEventKeyListResponse { pub event_keys: Vec, } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::AdminEditorShowcaseAssetPayload; + + #[test] + fn editor_showcase_asset_payload_serializes_author_fields_as_camel_case() { + let payload = AdminEditorShowcaseAssetPayload { + showcase_id: "showcase-1".to_string(), + asset_id: "asset-1".to_string(), + owner_user_id: "user-1".to_string(), + author_display_name: Some("作者昵称".to_string()), + author_public_user_code: Some("SY-00000042".to_string()), + label: "角色形象 1".to_string(), + image_src: "/generated-character-drafts/editor/spec.png".to_string(), + object_key: Some("generated-character-drafts/editor/spec.png".to_string()), + width: 1024, + height: 1024, + prompt: None, + actual_prompt: None, + model: None, + provider: None, + task_id: None, + asset_kind: Some("character".to_string()), + generation_inputs: None, + generation_cost_mud_points: 12, + refund_mud_points: 6, + review_status: "pending".to_string(), + display_enabled: false, + like_count: 0, + asset_deleted_while_pending: false, + reviewed_by_admin_user_id: None, + review_note: None, + refund_ledger_id: None, + refund_completed_at: None, + submitted_at: "2026-07-04T10:00:00.000Z".to_string(), + reviewed_at: None, + approved_at: None, + rejected_at: None, + updated_at: "2026-07-04T10:00:00.000Z".to_string(), + }; + + let value = serde_json::to_value(payload).expect("payload should serialize"); + + assert_eq!(value["authorDisplayName"], json!("作者昵称")); + assert_eq!(value["authorPublicUserCode"], json!("SY-00000042")); + assert!(value.get("author_display_name").is_none()); + assert!(value.get("author_public_user_code").is_none()); + } +} diff --git a/server-rs/crates/spacetime-client/src/editor_project.rs b/server-rs/crates/spacetime-client/src/editor_project.rs index a8a877600..9e451cea3 100644 --- a/server-rs/crates/spacetime-client/src/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/editor_project.rs @@ -479,4 +479,229 @@ impl SpacetimeClient { ) .await } + + pub async fn submit_editor_showcase_asset( + &self, + input: EditorShowcaseAssetSubmitRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "submit_editor_showcase_asset_and_return", + move |connection, sender| { + connection + .procedures() + .submit_editor_showcase_asset_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn list_public_editor_showcase_assets( + &self, + input: EditorShowcaseAssetPublicListRecordInput, + ) -> Result, SpacetimeClientError> { + let procedure_input = input.into(); + + self.call_after_connect( + "list_public_editor_showcase_assets_and_return", + move |connection, sender| { + connection + .procedures() + .list_public_editor_showcase_assets_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_list_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn admin_list_editor_showcase_assets( + &self, + input: EditorShowcaseAssetAdminListRecordInput, + ) -> Result, SpacetimeClientError> { + let procedure_input = input.into(); + + self.call_after_connect( + "admin_list_editor_showcase_assets_and_return", + move |connection, sender| { + connection + .procedures() + .admin_list_editor_showcase_assets_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_list_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn admin_review_editor_showcase_asset( + &self, + input: EditorShowcaseAssetAdminReviewRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "admin_review_editor_showcase_asset_and_return", + move |connection, sender| { + connection + .procedures() + .admin_review_editor_showcase_asset_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn update_editor_showcase_asset_display( + &self, + input: EditorShowcaseAssetDisplayUpdateRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "update_editor_showcase_asset_display_and_return", + move |connection, sender| { + connection + .procedures() + .update_editor_showcase_asset_display_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn mark_editor_showcase_asset_refunded( + &self, + input: EditorShowcaseAssetRefundMarkRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "mark_editor_showcase_asset_refunded_and_return", + move |connection, sender| { + connection + .procedures() + .mark_editor_showcase_asset_refunded_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn toggle_editor_showcase_asset_like( + &self, + input: EditorShowcaseAssetLikeToggleRecordInput, + ) -> Result { + let procedure_input = input.into(); + + self.call_after_connect( + "toggle_editor_showcase_asset_like_and_return", + move |connection, sender| { + connection + .procedures() + .toggle_editor_showcase_asset_like_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_asset_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn get_editor_showcase_campaign_config( + &self, + input: EditorShowcaseCampaignConfigGetRecordInput, + ) -> Result, SpacetimeClientError> { + let procedure_input = input.into(); + + self.call_after_connect( + "get_editor_showcase_campaign_config_and_return", + move |connection, sender| { + connection + .procedures() + .get_editor_showcase_campaign_config_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_campaign_config_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } + + pub async fn upsert_editor_showcase_campaign_config( + &self, + input: EditorShowcaseCampaignConfigUpsertRecordInput, + ) -> Result, SpacetimeClientError> { + let procedure_input = input.into(); + + self.call_after_connect( + "upsert_editor_showcase_campaign_config_and_return", + move |connection, sender| { + connection + .procedures() + .upsert_editor_showcase_campaign_config_and_return_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_editor_showcase_campaign_config_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await + } } diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 1ad8c2924..911b0d24e 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -39,19 +39,25 @@ pub use mapper::{ EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput, EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord, - EditorProjectResourceShowcaseUpdateRecordInput, ExternalApiKeyAuthenticateRecordInput, - ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, ExternalApiKeyRevokeRecordInput, - ExternalGenerationJobAcknowledgeRecordInput, ExternalGenerationJobClaimRecordInput, - ExternalGenerationJobCompleteRecordInput, ExternalGenerationJobEnqueueRecordInput, - ExternalGenerationJobFailRecordInput, ExternalGenerationJobGetRecordInput, - ExternalGenerationJobListRecord, ExternalGenerationJobListRecordInput, - ExternalGenerationJobRecord, ExternalGenerationJobRenewLeaseRecordInput, - ExternalGenerationQueueStatsRecord, JumpHopActionRequest, JumpHopActionResponse, - JumpHopActionType, JumpHopCharacterAsset, JumpHopDifficulty, JumpHopDraftResponse, - JumpHopGalleryCardResponse, JumpHopGalleryDetailResponse, JumpHopGalleryResponse, - JumpHopGenerationStatus, JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, - JumpHopLastJump, JumpHopPath, JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, - JumpHopRunStatus, JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse, + EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput, + EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput, + EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput, + EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput, + EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput, + EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput, + ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, + ExternalApiKeyRevokeRecordInput, ExternalGenerationJobAcknowledgeRecordInput, + ExternalGenerationJobClaimRecordInput, ExternalGenerationJobCompleteRecordInput, + ExternalGenerationJobEnqueueRecordInput, ExternalGenerationJobFailRecordInput, + ExternalGenerationJobGetRecordInput, ExternalGenerationJobListRecord, + ExternalGenerationJobListRecordInput, ExternalGenerationJobRecord, + ExternalGenerationJobRenewLeaseRecordInput, ExternalGenerationQueueStatsRecord, + JumpHopActionRequest, JumpHopActionResponse, JumpHopActionType, JumpHopCharacterAsset, + JumpHopDifficulty, JumpHopDraftResponse, JumpHopGalleryCardResponse, + JumpHopGalleryDetailResponse, JumpHopGalleryResponse, JumpHopGenerationStatus, + JumpHopJumpRequest, JumpHopJumpResponse, JumpHopJumpResult, JumpHopLastJump, JumpHopPath, + JumpHopPlatform, JumpHopRestartRunRequest, JumpHopRunResponse, JumpHopRunStatus, + JumpHopRuntimeRunSnapshotResponse, JumpHopScoring, JumpHopSessionResponse, JumpHopSessionSnapshotResponse, JumpHopStartRunRequest, JumpHopStylePreset, JumpHopTileAsset, JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse, JumpHopWorkProfileResponse, JumpHopWorkSummaryResponse, JumpHopWorksResponse, @@ -202,8 +208,7 @@ use module_runtime::{ RuntimeProfileFeedbackSubmissionRecord, RuntimeProfileInviteCodeAdminListRecord, RuntimeProfileInviteCodeRecord, RuntimeProfilePlayStatsRecord, RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord, - RuntimeProfileRechargeProductConfigRecord, - RuntimeProfileRedeemCodeAdminListRecord, + RuntimeProfileRechargeProductConfigRecord, RuntimeProfileRedeemCodeAdminListRecord, RuntimeProfileRedeemCodeMode as DomainRuntimeProfileRedeemCodeMode, RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, @@ -214,9 +219,8 @@ use module_runtime::{ RuntimeTrackingScopeKind as DomainRuntimeTrackingScopeKind, build_analytics_metric_query_input, build_runtime_browse_history_clear_input, build_runtime_browse_history_list_input, build_runtime_browse_history_record, build_runtime_browse_history_sync_input, - build_runtime_profile_dashboard_get_input, build_runtime_profile_dashboard_record, - build_runtime_profile_code_operation_record, - build_runtime_profile_feedback_submission_input, + build_runtime_profile_code_operation_record, build_runtime_profile_dashboard_get_input, + build_runtime_profile_dashboard_record, build_runtime_profile_feedback_submission_input, build_runtime_profile_feedback_submission_record, build_runtime_profile_invite_code_admin_list_input, build_runtime_profile_invite_code_admin_upsert_input, build_runtime_profile_invite_code_record, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 10247312e..193666bf4 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -82,7 +82,12 @@ pub use self::editor_project::{ EditorProjectLayoutSaveRecordInput, EditorProjectRecord, EditorProjectRenameRecordInput, EditorProjectResourceCreateRecordInput, EditorProjectResourceMediaRepairRecordInput, EditorProjectResourcePublicShowcaseListRecordInput, EditorProjectResourceRecord, - EditorProjectResourceShowcaseUpdateRecordInput, + EditorProjectResourceShowcaseUpdateRecordInput, EditorShowcaseAssetAdminListRecordInput, + EditorShowcaseAssetAdminReviewRecordInput, EditorShowcaseAssetDisplayUpdateRecordInput, + EditorShowcaseAssetLikeToggleRecordInput, EditorShowcaseAssetPublicListRecordInput, + EditorShowcaseAssetRecord, EditorShowcaseAssetRefundMarkRecordInput, + EditorShowcaseAssetSubmitRecordInput, EditorShowcaseCampaignConfigGetRecordInput, + EditorShowcaseCampaignConfigRecord, EditorShowcaseCampaignConfigUpsertRecordInput, }; pub use self::external_api_key::{ ExternalApiKeyAuthenticateRecordInput, ExternalApiKeyCreateRecordInput, ExternalApiKeyRecord, @@ -214,7 +219,9 @@ pub(crate) use self::editor_project::{ map_editor_project_list_procedure_result, map_editor_project_optional_procedure_result, map_editor_project_required_procedure_result, map_editor_project_resource_list_procedure_result, - map_editor_project_resource_procedure_result, + map_editor_project_resource_procedure_result, map_editor_showcase_asset_list_procedure_result, + map_editor_showcase_asset_procedure_result, + map_editor_showcase_campaign_config_procedure_result, }; pub(crate) use self::external_api_key::{ map_external_api_key_list_procedure_result, map_external_api_key_single_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs index ce92aa49e..45ec8909b 100644 --- a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs @@ -95,6 +95,11 @@ pub struct EditorAssetRecord { pub source_resource_id: Option, pub public_showcase_enabled: Option, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub showcase_id: Option, + pub showcase_review_status: Option, + pub showcase_display_enabled: Option, + pub showcase_like_count: Option, pub created_at: String, pub updated_at: String, } @@ -105,6 +110,56 @@ pub struct EditorAssetLibraryRecord { pub assets: Vec, } +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorShowcaseAssetRecord { + pub showcase_id: String, + pub asset_id: String, + pub owner_user_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub source_resource_id: Option, + pub asset_kind: Option, + pub generation_inputs: Option, + pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub refund_mud_points: u64, + pub review_status: String, + pub display_enabled: bool, + pub like_count: u64, + pub asset_deleted_while_pending: bool, + pub reviewed_by_admin_user_id: Option, + pub review_note: Option, + pub refund_ledger_id: Option, + pub refund_completed_at: Option, + pub submitted_at: String, + pub reviewed_at: Option, + pub approved_at: Option, + pub rejected_at: Option, + pub updated_at: String, +} + +#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct EditorShowcaseCampaignConfigRecord { + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_by_admin_user_id: Option, + pub updated_at: String, +} + #[derive(Clone, Debug, PartialEq)] pub struct EditorProjectCreateRecordInput { pub project_id: String, @@ -235,6 +290,7 @@ pub struct EditorAssetCreateRecordInput { pub source_resource_id: Option, pub now_micros: i64, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -262,6 +318,79 @@ pub struct EditorAssetDeleteRecordInput { pub owner_user_id: String, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetSubmitRecordInput { + pub asset_id: String, + pub owner_user_id: String, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetPublicListRecordInput { + pub limit: u32, + pub cursor: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetAdminListRecordInput { + pub limit: u32, + pub cursor: Option, + pub owner_user_id: Option, + pub asset_kind: Option, + pub review_status: Option, + pub submitted_after_micros: Option, + pub submitted_before_micros: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetAdminReviewRecordInput { + pub showcase_id: String, + pub admin_user_id: String, + pub review_status: String, + pub review_note: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetDisplayUpdateRecordInput { + pub showcase_id: String, + pub admin_user_id: String, + pub display_enabled: bool, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetRefundMarkRecordInput { + pub showcase_id: String, + pub refund_ledger_id: String, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseAssetLikeToggleRecordInput { + pub showcase_id: String, + pub user_id: String, + pub liked: bool, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseCampaignConfigGetRecordInput { + pub include_disabled: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EditorShowcaseCampaignConfigUpsertRecordInput { + pub admin_user_id: String, + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_at_micros: i64, +} + impl From for crate::module_bindings::EditorProjectCreateInput { fn from(input: EditorProjectCreateRecordInput) -> Self { Self { @@ -449,6 +578,7 @@ impl From for crate::module_bindings::EditorAssetC source_resource_id: input.source_resource_id, now_micros: input.now_micros, thumbnail_src: input.thumbnail_src, + generation_cost_mud_points: input.generation_cost_mud_points, } } } @@ -489,6 +619,124 @@ impl From for crate::module_bindings::EditorAssetD } } +impl From + for crate::module_bindings::EditorShowcaseAssetSubmitInput +{ + fn from(input: EditorShowcaseAssetSubmitRecordInput) -> Self { + Self { + asset_id: input.asset_id, + owner_user_id: input.owner_user_id, + now_micros: input.now_micros, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseAssetPublicListInput +{ + fn from(input: EditorShowcaseAssetPublicListRecordInput) -> Self { + Self { + limit: input.limit, + cursor: input.cursor, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseAssetAdminListInput +{ + fn from(input: EditorShowcaseAssetAdminListRecordInput) -> Self { + Self { + limit: input.limit, + cursor: input.cursor, + owner_user_id: input.owner_user_id, + asset_kind: input.asset_kind, + review_status: input.review_status, + submitted_after_micros: input.submitted_after_micros, + submitted_before_micros: input.submitted_before_micros, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseAssetAdminReviewInput +{ + fn from(input: EditorShowcaseAssetAdminReviewRecordInput) -> Self { + Self { + showcase_id: input.showcase_id, + admin_user_id: input.admin_user_id, + review_status: input.review_status, + review_note: input.review_note, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseAssetDisplayUpdateInput +{ + fn from(input: EditorShowcaseAssetDisplayUpdateRecordInput) -> Self { + Self { + showcase_id: input.showcase_id, + admin_user_id: input.admin_user_id, + display_enabled: input.display_enabled, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseAssetRefundMarkInput +{ + fn from(input: EditorShowcaseAssetRefundMarkRecordInput) -> Self { + Self { + showcase_id: input.showcase_id, + refund_ledger_id: input.refund_ledger_id, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseAssetLikeToggleInput +{ + fn from(input: EditorShowcaseAssetLikeToggleRecordInput) -> Self { + Self { + showcase_id: input.showcase_id, + user_id: input.user_id, + liked: input.liked, + updated_at_micros: input.updated_at_micros, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseCampaignConfigGetInput +{ + fn from(input: EditorShowcaseCampaignConfigGetRecordInput) -> Self { + Self { + include_disabled: input.include_disabled, + } + } +} + +impl From + for crate::module_bindings::EditorShowcaseCampaignConfigUpsertInput +{ + fn from(input: EditorShowcaseCampaignConfigUpsertRecordInput) -> Self { + Self { + admin_user_id: input.admin_user_id, + enabled: input.enabled, + title: input.title, + image_src: input.image_src, + prompt: input.prompt, + author: input.author, + cost_text: input.cost_text, + updated_at_micros: input.updated_at_micros, + } + } +} + pub(crate) fn map_editor_project_optional_procedure_result( result: EditorProjectProcedureResult, ) -> Result, SpacetimeClientError> { @@ -634,6 +882,46 @@ pub(crate) fn map_editor_asset_procedure_result( .ok_or_else(|| SpacetimeClientError::missing_snapshot("图片画布素材快照")) } +pub(crate) fn map_editor_showcase_asset_procedure_result( + result: EditorShowcaseAssetProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + result + .asset + .map(map_editor_showcase_asset_snapshot) + .transpose()? + .ok_or_else(|| SpacetimeClientError::missing_snapshot("陶泥儿精选素材快照")) +} + +pub(crate) fn map_editor_showcase_asset_list_procedure_result( + result: EditorShowcaseAssetListProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + result + .assets + .into_iter() + .map(map_editor_showcase_asset_snapshot) + .collect() +} + +pub(crate) fn map_editor_showcase_campaign_config_procedure_result( + result: EditorShowcaseCampaignConfigProcedureResult, +) -> Result, SpacetimeClientError> { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + Ok(result + .config + .map(map_editor_showcase_campaign_config_snapshot)) +} + fn map_editor_project_snapshot( snapshot: EditorProjectSnapshot, ) -> Result { @@ -764,11 +1052,78 @@ fn map_editor_asset_snapshot( source_resource_id: snapshot.source_resource_id, public_showcase_enabled: snapshot.public_showcase_enabled, thumbnail_src: snapshot.thumbnail_src, + generation_cost_mud_points: snapshot.generation_cost_mud_points, + showcase_id: snapshot.showcase_id, + showcase_review_status: snapshot.showcase_review_status, + showcase_display_enabled: snapshot.showcase_display_enabled, + showcase_like_count: snapshot.showcase_like_count, created_at: format_timestamp_micros(snapshot.created_at_micros), updated_at: format_timestamp_micros(snapshot.updated_at_micros), }) } +fn map_editor_showcase_asset_snapshot( + snapshot: EditorShowcaseAssetSnapshot, +) -> Result { + let generation_inputs = parse_optional_generation_inputs_json( + snapshot.generation_inputs_json.as_deref(), + "陶泥儿精选素材生成输入 JSON", + )?; + Ok(EditorShowcaseAssetRecord { + showcase_id: snapshot.showcase_id, + asset_id: snapshot.asset_id, + owner_user_id: snapshot.owner_user_id, + label: snapshot.label, + asset_object_id: snapshot.asset_object_id, + image_src: snapshot.image_src, + object_key: snapshot.object_key, + width: snapshot.width, + height: snapshot.height, + source_type: snapshot.source_type, + prompt: snapshot.prompt, + actual_prompt: snapshot.actual_prompt, + model: snapshot.model, + provider: snapshot.provider, + task_id: snapshot.task_id, + source_resource_id: snapshot.source_resource_id, + asset_kind: snapshot.asset_kind, + generation_inputs, + thumbnail_src: snapshot.thumbnail_src, + generation_cost_mud_points: snapshot.generation_cost_mud_points, + refund_mud_points: snapshot.refund_mud_points, + review_status: snapshot.review_status, + display_enabled: snapshot.display_enabled, + like_count: snapshot.like_count, + asset_deleted_while_pending: snapshot.asset_deleted_while_pending, + reviewed_by_admin_user_id: snapshot.reviewed_by_admin_user_id, + review_note: snapshot.review_note, + refund_ledger_id: snapshot.refund_ledger_id, + refund_completed_at: snapshot + .refund_completed_at_micros + .map(format_timestamp_micros), + submitted_at: format_timestamp_micros(snapshot.submitted_at_micros), + reviewed_at: snapshot.reviewed_at_micros.map(format_timestamp_micros), + approved_at: snapshot.approved_at_micros.map(format_timestamp_micros), + rejected_at: snapshot.rejected_at_micros.map(format_timestamp_micros), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + }) +} + +fn map_editor_showcase_campaign_config_snapshot( + snapshot: EditorShowcaseCampaignConfigSnapshot, +) -> EditorShowcaseCampaignConfigRecord { + EditorShowcaseCampaignConfigRecord { + enabled: snapshot.enabled, + title: snapshot.title, + image_src: snapshot.image_src, + prompt: snapshot.prompt, + author: snapshot.author, + cost_text: snapshot.cost_text, + updated_by_admin_user_id: snapshot.updated_by_admin_user_id, + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + } +} + fn parse_optional_generation_inputs_json( raw_json: Option<&str>, label: &str, diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index a19453595..097430587 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -12,11 +12,13 @@ pub mod acknowledge_quest_completion_reducer; pub mod admin_disable_profile_redeem_code_procedure; pub mod admin_disable_profile_task_config_procedure; pub mod admin_get_profile_wallet_config_procedure; +pub mod admin_list_editor_showcase_assets_and_return_procedure; pub mod admin_list_profile_invite_codes_procedure; pub mod admin_list_profile_recharge_products_procedure; pub mod admin_list_profile_redeem_codes_procedure; pub mod admin_list_profile_task_configs_procedure; pub mod admin_list_work_visibility_procedure; +pub mod admin_review_editor_showcase_asset_and_return_procedure; pub mod admin_update_work_visibility_procedure; pub mod admin_upsert_profile_invite_code_procedure; pub mod admin_upsert_profile_recharge_product_procedure; @@ -403,6 +405,26 @@ pub mod editor_project_snapshot_type; pub mod editor_project_table; pub mod editor_project_type; pub mod editor_project_viewport_snapshot_type; +pub mod editor_showcase_asset_admin_list_input_type; +pub mod editor_showcase_asset_admin_review_input_type; +pub mod editor_showcase_asset_display_update_input_type; +pub mod editor_showcase_asset_like_table; +pub mod editor_showcase_asset_like_toggle_input_type; +pub mod editor_showcase_asset_like_type; +pub mod editor_showcase_asset_list_procedure_result_type; +pub mod editor_showcase_asset_procedure_result_type; +pub mod editor_showcase_asset_public_list_input_type; +pub mod editor_showcase_asset_refund_mark_input_type; +pub mod editor_showcase_asset_snapshot_type; +pub mod editor_showcase_asset_submit_input_type; +pub mod editor_showcase_asset_table; +pub mod editor_showcase_asset_type; +pub mod editor_showcase_campaign_config_get_input_type; +pub mod editor_showcase_campaign_config_procedure_result_type; +pub mod editor_showcase_campaign_config_snapshot_type; +pub mod editor_showcase_campaign_config_table; +pub mod editor_showcase_campaign_config_type; +pub mod editor_showcase_campaign_config_upsert_input_type; pub mod enqueue_external_generation_job_and_return_procedure; pub mod ensure_analytics_date_dimension_for_date_reducer; pub mod equip_inventory_item_input_type; @@ -461,6 +483,7 @@ pub mod get_custom_world_gallery_detail_procedure; pub mod get_custom_world_library_detail_procedure; pub mod get_editor_asset_library_and_return_procedure; pub mod get_editor_project_and_return_procedure; +pub mod get_editor_showcase_campaign_config_and_return_procedure; pub mod get_external_generation_job_and_return_procedure; pub mod get_external_generation_queue_stats_and_return_procedure; pub mod get_jump_hop_agent_session_procedure; @@ -581,6 +604,7 @@ pub mod list_platform_browse_history_procedure; pub mod list_profile_save_archives_procedure; pub mod list_profile_wallet_ledger_procedure; pub mod list_public_editor_project_resources_and_return_procedure; +pub mod list_public_editor_showcase_assets_and_return_procedure; pub mod list_puzzle_clear_works_procedure; pub mod list_puzzle_gallery_procedure; pub mod list_puzzle_works_procedure; @@ -588,6 +612,7 @@ pub mod list_square_hole_works_procedure; pub mod list_visual_novel_runtime_history_procedure; pub mod list_visual_novel_works_procedure; pub mod list_wooden_fish_works_procedure; +pub mod mark_editor_showcase_asset_refunded_and_return_procedure; pub mod mark_profile_recharge_order_paid_and_return_procedure; pub mod mark_puzzle_clear_level_time_up_procedure; pub mod mark_puzzle_draft_generation_failed_procedure; @@ -1101,6 +1126,7 @@ pub mod story_session_type; pub mod submit_big_fish_input_procedure; pub mod submit_big_fish_message_procedure; pub mod submit_custom_world_agent_message_procedure; +pub mod submit_editor_showcase_asset_and_return_procedure; pub mod submit_match_3_d_agent_message_procedure; pub mod submit_profile_feedback_and_return_procedure; pub mod submit_puzzle_agent_message_procedure; @@ -1110,6 +1136,7 @@ pub mod submit_visual_novel_agent_message_procedure; pub mod swap_puzzle_clear_cards_procedure; pub mod swap_puzzle_pieces_procedure; pub mod sync_auth_store_projection_procedure; +pub mod toggle_editor_showcase_asset_like_and_return_procedure; pub mod tracking_daily_stat_table; pub mod tracking_daily_stat_type; pub mod tracking_event_table; @@ -1128,6 +1155,7 @@ pub mod update_bark_battle_draft_config_procedure; pub mod update_editor_asset_and_return_procedure; pub mod update_editor_asset_folder_and_return_procedure; pub mod update_editor_project_resource_showcase_and_return_procedure; +pub mod update_editor_showcase_asset_display_and_return_procedure; pub mod update_jump_hop_work_procedure; pub mod update_match_3_d_work_procedure; pub mod update_puzzle_clear_work_procedure; @@ -1143,6 +1171,7 @@ pub mod upsert_creation_entry_type_config_procedure; pub mod upsert_custom_world_agent_operation_progress_procedure; pub mod upsert_custom_world_profile_and_return_procedure; pub mod upsert_custom_world_profile_reducer; +pub mod upsert_editor_showcase_campaign_config_and_return_procedure; pub mod upsert_npc_state_and_return_procedure; pub mod upsert_npc_state_reducer; pub mod upsert_platform_browse_history_and_return_procedure; @@ -1243,11 +1272,13 @@ pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion; pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code; pub use admin_disable_profile_task_config_procedure::admin_disable_profile_task_config; pub use admin_get_profile_wallet_config_procedure::admin_get_profile_wallet_config; +pub use admin_list_editor_showcase_assets_and_return_procedure::admin_list_editor_showcase_assets_and_return; pub use admin_list_profile_invite_codes_procedure::admin_list_profile_invite_codes; pub use admin_list_profile_recharge_products_procedure::admin_list_profile_recharge_products; pub use admin_list_profile_redeem_codes_procedure::admin_list_profile_redeem_codes; pub use admin_list_profile_task_configs_procedure::admin_list_profile_task_configs; pub use admin_list_work_visibility_procedure::admin_list_work_visibility; +pub use admin_review_editor_showcase_asset_and_return_procedure::admin_review_editor_showcase_asset_and_return; pub use admin_update_work_visibility_procedure::admin_update_work_visibility; pub use admin_upsert_profile_invite_code_procedure::admin_upsert_profile_invite_code; pub use admin_upsert_profile_recharge_product_procedure::admin_upsert_profile_recharge_product; @@ -1634,6 +1665,26 @@ pub use editor_project_snapshot_type::EditorProjectSnapshot; pub use editor_project_table::*; pub use editor_project_type::EditorProject; pub use editor_project_viewport_snapshot_type::EditorProjectViewportSnapshot; +pub use editor_showcase_asset_admin_list_input_type::EditorShowcaseAssetAdminListInput; +pub use editor_showcase_asset_admin_review_input_type::EditorShowcaseAssetAdminReviewInput; +pub use editor_showcase_asset_display_update_input_type::EditorShowcaseAssetDisplayUpdateInput; +pub use editor_showcase_asset_like_table::*; +pub use editor_showcase_asset_like_toggle_input_type::EditorShowcaseAssetLikeToggleInput; +pub use editor_showcase_asset_like_type::EditorShowcaseAssetLike; +pub use editor_showcase_asset_list_procedure_result_type::EditorShowcaseAssetListProcedureResult; +pub use editor_showcase_asset_procedure_result_type::EditorShowcaseAssetProcedureResult; +pub use editor_showcase_asset_public_list_input_type::EditorShowcaseAssetPublicListInput; +pub use editor_showcase_asset_refund_mark_input_type::EditorShowcaseAssetRefundMarkInput; +pub use editor_showcase_asset_snapshot_type::EditorShowcaseAssetSnapshot; +pub use editor_showcase_asset_submit_input_type::EditorShowcaseAssetSubmitInput; +pub use editor_showcase_asset_table::*; +pub use editor_showcase_asset_type::EditorShowcaseAsset; +pub use editor_showcase_campaign_config_get_input_type::EditorShowcaseCampaignConfigGetInput; +pub use editor_showcase_campaign_config_procedure_result_type::EditorShowcaseCampaignConfigProcedureResult; +pub use editor_showcase_campaign_config_snapshot_type::EditorShowcaseCampaignConfigSnapshot; +pub use editor_showcase_campaign_config_table::*; +pub use editor_showcase_campaign_config_type::EditorShowcaseCampaignConfig; +pub use editor_showcase_campaign_config_upsert_input_type::EditorShowcaseCampaignConfigUpsertInput; pub use enqueue_external_generation_job_and_return_procedure::enqueue_external_generation_job_and_return; pub use ensure_analytics_date_dimension_for_date_reducer::ensure_analytics_date_dimension_for_date; pub use equip_inventory_item_input_type::EquipInventoryItemInput; @@ -1692,6 +1743,7 @@ pub use get_custom_world_gallery_detail_procedure::get_custom_world_gallery_deta pub use get_custom_world_library_detail_procedure::get_custom_world_library_detail; pub use get_editor_asset_library_and_return_procedure::get_editor_asset_library_and_return; pub use get_editor_project_and_return_procedure::get_editor_project_and_return; +pub use get_editor_showcase_campaign_config_and_return_procedure::get_editor_showcase_campaign_config_and_return; pub use get_external_generation_job_and_return_procedure::get_external_generation_job_and_return; pub use get_external_generation_queue_stats_and_return_procedure::get_external_generation_queue_stats_and_return; pub use get_jump_hop_agent_session_procedure::get_jump_hop_agent_session; @@ -1812,6 +1864,7 @@ pub use list_platform_browse_history_procedure::list_platform_browse_history; pub use list_profile_save_archives_procedure::list_profile_save_archives; pub use list_profile_wallet_ledger_procedure::list_profile_wallet_ledger; pub use list_public_editor_project_resources_and_return_procedure::list_public_editor_project_resources_and_return; +pub use list_public_editor_showcase_assets_and_return_procedure::list_public_editor_showcase_assets_and_return; pub use list_puzzle_clear_works_procedure::list_puzzle_clear_works; pub use list_puzzle_gallery_procedure::list_puzzle_gallery; pub use list_puzzle_works_procedure::list_puzzle_works; @@ -1819,6 +1872,7 @@ pub use list_square_hole_works_procedure::list_square_hole_works; pub use list_visual_novel_runtime_history_procedure::list_visual_novel_runtime_history; pub use list_visual_novel_works_procedure::list_visual_novel_works; pub use list_wooden_fish_works_procedure::list_wooden_fish_works; +pub use mark_editor_showcase_asset_refunded_and_return_procedure::mark_editor_showcase_asset_refunded_and_return; pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return; pub use mark_puzzle_clear_level_time_up_procedure::mark_puzzle_clear_level_time_up; pub use mark_puzzle_draft_generation_failed_procedure::mark_puzzle_draft_generation_failed; @@ -2332,6 +2386,7 @@ pub use story_session_type::StorySession; pub use submit_big_fish_input_procedure::submit_big_fish_input; pub use submit_big_fish_message_procedure::submit_big_fish_message; pub use submit_custom_world_agent_message_procedure::submit_custom_world_agent_message; +pub use submit_editor_showcase_asset_and_return_procedure::submit_editor_showcase_asset_and_return; pub use submit_match_3_d_agent_message_procedure::submit_match_3_d_agent_message; pub use submit_profile_feedback_and_return_procedure::submit_profile_feedback_and_return; pub use submit_puzzle_agent_message_procedure::submit_puzzle_agent_message; @@ -2341,6 +2396,7 @@ pub use submit_visual_novel_agent_message_procedure::submit_visual_novel_agent_m pub use swap_puzzle_clear_cards_procedure::swap_puzzle_clear_cards; pub use swap_puzzle_pieces_procedure::swap_puzzle_pieces; pub use sync_auth_store_projection_procedure::sync_auth_store_projection; +pub use toggle_editor_showcase_asset_like_and_return_procedure::toggle_editor_showcase_asset_like_and_return; pub use tracking_daily_stat_table::*; pub use tracking_daily_stat_type::TrackingDailyStat; pub use tracking_event_table::*; @@ -2359,6 +2415,7 @@ pub use update_bark_battle_draft_config_procedure::update_bark_battle_draft_conf pub use update_editor_asset_and_return_procedure::update_editor_asset_and_return; pub use update_editor_asset_folder_and_return_procedure::update_editor_asset_folder_and_return; pub use update_editor_project_resource_showcase_and_return_procedure::update_editor_project_resource_showcase_and_return; +pub use update_editor_showcase_asset_display_and_return_procedure::update_editor_showcase_asset_display_and_return; pub use update_jump_hop_work_procedure::update_jump_hop_work; pub use update_match_3_d_work_procedure::update_match_3_d_work; pub use update_puzzle_clear_work_procedure::update_puzzle_clear_work; @@ -2374,6 +2431,7 @@ pub use upsert_creation_entry_type_config_procedure::upsert_creation_entry_type_ pub use upsert_custom_world_agent_operation_progress_procedure::upsert_custom_world_agent_operation_progress; pub use upsert_custom_world_profile_and_return_procedure::upsert_custom_world_profile_and_return; pub use upsert_custom_world_profile_reducer::upsert_custom_world_profile; +pub use upsert_editor_showcase_campaign_config_and_return_procedure::upsert_editor_showcase_campaign_config_and_return; pub use upsert_npc_state_and_return_procedure::upsert_npc_state_and_return; pub use upsert_npc_state_reducer::upsert_npc_state; pub use upsert_platform_browse_history_and_return_procedure::upsert_platform_browse_history_and_return; @@ -2783,6 +2841,9 @@ pub struct DbUpdate { editor_canvas: __sdk::TableUpdate, editor_project: __sdk::TableUpdate, editor_project_resource: __sdk::TableUpdate, + editor_showcase_asset: __sdk::TableUpdate, + editor_showcase_asset_like: __sdk::TableUpdate, + editor_showcase_campaign_config: __sdk::TableUpdate, external_api_key: __sdk::TableUpdate, external_generation_job: __sdk::TableUpdate, external_generation_job_event: __sdk::TableUpdate, @@ -3012,6 +3073,17 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "editor_project_resource" => db_update.editor_project_resource.append( editor_project_resource_table::parse_table_update(table_update)?, ), + "editor_showcase_asset" => db_update.editor_showcase_asset.append( + editor_showcase_asset_table::parse_table_update(table_update)?, + ), + "editor_showcase_asset_like" => db_update.editor_showcase_asset_like.append( + editor_showcase_asset_like_table::parse_table_update(table_update)?, + ), + "editor_showcase_campaign_config" => { + db_update.editor_showcase_campaign_config.append( + editor_showcase_campaign_config_table::parse_table_update(table_update)?, + ) + } "external_api_key" => db_update .external_api_key .append(external_api_key_table::parse_table_update(table_update)?), @@ -3502,6 +3574,24 @@ impl __sdk::DbUpdate for DbUpdate { &self.editor_project_resource, ) .with_updates_by_pk(|row| &row.resource_id); + diff.editor_showcase_asset = cache + .apply_diff_to_table::( + "editor_showcase_asset", + &self.editor_showcase_asset, + ) + .with_updates_by_pk(|row| &row.showcase_id); + diff.editor_showcase_asset_like = cache + .apply_diff_to_table::( + "editor_showcase_asset_like", + &self.editor_showcase_asset_like, + ) + .with_updates_by_pk(|row| &row.like_id); + diff.editor_showcase_campaign_config = cache + .apply_diff_to_table::( + "editor_showcase_campaign_config", + &self.editor_showcase_campaign_config, + ) + .with_updates_by_pk(|row| &row.config_id); diff.external_api_key = cache .apply_diff_to_table::("external_api_key", &self.external_api_key) .with_updates_by_pk(|row| &row.key_id); @@ -4068,6 +4158,15 @@ impl __sdk::DbUpdate for DbUpdate { "editor_project_resource" => db_update .editor_project_resource .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "editor_showcase_asset" => db_update + .editor_showcase_asset + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "editor_showcase_asset_like" => db_update + .editor_showcase_asset_like + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "editor_showcase_campaign_config" => db_update + .editor_showcase_campaign_config + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "external_api_key" => db_update .external_api_key .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -4462,6 +4561,15 @@ impl __sdk::DbUpdate for DbUpdate { "editor_project_resource" => db_update .editor_project_resource .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "editor_showcase_asset" => db_update + .editor_showcase_asset + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "editor_showcase_asset_like" => db_update + .editor_showcase_asset_like + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "editor_showcase_campaign_config" => db_update + .editor_showcase_campaign_config + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "external_api_key" => db_update .external_api_key .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -4774,6 +4882,9 @@ pub struct AppliedDiff<'r> { editor_canvas: __sdk::TableAppliedDiff<'r, EditorCanvas>, editor_project: __sdk::TableAppliedDiff<'r, EditorProject>, editor_project_resource: __sdk::TableAppliedDiff<'r, EditorProjectResource>, + editor_showcase_asset: __sdk::TableAppliedDiff<'r, EditorShowcaseAsset>, + editor_showcase_asset_like: __sdk::TableAppliedDiff<'r, EditorShowcaseAssetLike>, + editor_showcase_campaign_config: __sdk::TableAppliedDiff<'r, EditorShowcaseCampaignConfig>, external_api_key: __sdk::TableAppliedDiff<'r, ExternalApiKey>, external_generation_job: __sdk::TableAppliedDiff<'r, ExternalGenerationJob>, external_generation_job_event: __sdk::TableAppliedDiff<'r, ExternalGenerationJobEvent>, @@ -5079,6 +5190,21 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.editor_project_resource, event, ); + callbacks.invoke_table_row_callbacks::( + "editor_showcase_asset", + &self.editor_showcase_asset, + event, + ); + callbacks.invoke_table_row_callbacks::( + "editor_showcase_asset_like", + &self.editor_showcase_asset_like, + event, + ); + callbacks.invoke_table_row_callbacks::( + "editor_showcase_campaign_config", + &self.editor_showcase_campaign_config, + event, + ); callbacks.invoke_table_row_callbacks::( "external_api_key", &self.external_api_key, @@ -6190,6 +6316,9 @@ impl __sdk::SpacetimeModule for RemoteModule { editor_canvas_table::register_table(client_cache); editor_project_table::register_table(client_cache); editor_project_resource_table::register_table(client_cache); + editor_showcase_asset_table::register_table(client_cache); + editor_showcase_asset_like_table::register_table(client_cache); + editor_showcase_campaign_config_table::register_table(client_cache); external_api_key_table::register_table(client_cache); external_generation_job_table::register_table(client_cache); external_generation_job_event_table::register_table(client_cache); @@ -6319,6 +6448,9 @@ impl __sdk::SpacetimeModule for RemoteModule { "editor_canvas", "editor_project", "editor_project_resource", + "editor_showcase_asset", + "editor_showcase_asset_like", + "editor_showcase_campaign_config", "external_api_key", "external_generation_job", "external_generation_job_event", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs new file mode 100644 index 000000000..69ce4beca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_list_editor_showcase_assets_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_admin_list_input_type::EditorShowcaseAssetAdminListInput; +use super::editor_showcase_asset_list_procedure_result_type::EditorShowcaseAssetListProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct AdminListEditorShowcaseAssetsAndReturnArgs { + pub input: EditorShowcaseAssetAdminListInput, +} + +impl __sdk::InModule for AdminListEditorShowcaseAssetsAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `admin_list_editor_showcase_assets_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait admin_list_editor_showcase_assets_and_return { + fn admin_list_editor_showcase_assets_and_return( + &self, + input: EditorShowcaseAssetAdminListInput, + ) { + self.admin_list_editor_showcase_assets_and_return_then(input, |_, _| {}); + } + + fn admin_list_editor_showcase_assets_and_return_then( + &self, + input: EditorShowcaseAssetAdminListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl admin_list_editor_showcase_assets_and_return for super::RemoteProcedures { + fn admin_list_editor_showcase_assets_and_return_then( + &self, + input: EditorShowcaseAssetAdminListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetListProcedureResult>( + "admin_list_editor_showcase_assets_and_return", + AdminListEditorShowcaseAssetsAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs new file mode 100644 index 000000000..29b2ecf60 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_review_editor_showcase_asset_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_admin_review_input_type::EditorShowcaseAssetAdminReviewInput; +use super::editor_showcase_asset_procedure_result_type::EditorShowcaseAssetProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct AdminReviewEditorShowcaseAssetAndReturnArgs { + pub input: EditorShowcaseAssetAdminReviewInput, +} + +impl __sdk::InModule for AdminReviewEditorShowcaseAssetAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `admin_review_editor_showcase_asset_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait admin_review_editor_showcase_asset_and_return { + fn admin_review_editor_showcase_asset_and_return( + &self, + input: EditorShowcaseAssetAdminReviewInput, + ) { + self.admin_review_editor_showcase_asset_and_return_then(input, |_, _| {}); + } + + fn admin_review_editor_showcase_asset_and_return_then( + &self, + input: EditorShowcaseAssetAdminReviewInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl admin_review_editor_showcase_asset_and_return for super::RemoteProcedures { + fn admin_review_editor_showcase_asset_and_return_then( + &self, + input: EditorShowcaseAssetAdminReviewInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetProcedureResult>( + "admin_review_editor_showcase_asset_and_return", + AdminReviewEditorShowcaseAssetAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs index 74aa13bfd..a1f296c1f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_create_input_type.rs @@ -27,6 +27,7 @@ pub struct EditorAssetCreateInput { pub source_resource_id: Option, pub now_micros: i64, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, } impl __sdk::InModule for EditorAssetCreateInput { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs index c1111852c..4967bcb71 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_snapshot_type.rs @@ -28,6 +28,11 @@ pub struct EditorAssetSnapshot { pub created_at_micros: i64, pub updated_at_micros: i64, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub showcase_id: Option, + pub showcase_review_status: Option, + pub showcase_display_enabled: Option, + pub showcase_like_count: Option, } impl __sdk::InModule for EditorAssetSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs index dccacd1f3..48cbbae26 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_asset_type.rs @@ -28,6 +28,7 @@ pub struct EditorAsset { pub generation_inputs_json: Option, pub source_resource_id: Option, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, } impl __sdk::InModule for EditorAsset { @@ -59,6 +60,7 @@ pub struct EditorAssetCols { pub generation_inputs_json: __sdk::__query_builder::Col>, pub source_resource_id: __sdk::__query_builder::Col>, pub thumbnail_src: __sdk::__query_builder::Col>, + pub generation_cost_mud_points: __sdk::__query_builder::Col, } impl __sdk::__query_builder::HasCols for EditorAsset { @@ -89,6 +91,10 @@ impl __sdk::__query_builder::HasCols for EditorAsset { ), source_resource_id: __sdk::__query_builder::Col::new(table_name, "source_resource_id"), thumbnail_src: __sdk::__query_builder::Col::new(table_name, "thumbnail_src"), + generation_cost_mud_points: __sdk::__query_builder::Col::new( + table_name, + "generation_cost_mud_points", + ), } } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_admin_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_admin_list_input_type.rs new file mode 100644 index 000000000..91fe7ebdd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_admin_list_input_type.rs @@ -0,0 +1,21 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetAdminListInput { + pub limit: u32, + pub cursor: Option, + pub owner_user_id: Option, + pub asset_kind: Option, + pub review_status: Option, + pub submitted_after_micros: Option, + pub submitted_before_micros: Option, +} + +impl __sdk::InModule for EditorShowcaseAssetAdminListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_admin_review_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_admin_review_input_type.rs new file mode 100644 index 000000000..9af0b0e39 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_admin_review_input_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetAdminReviewInput { + pub showcase_id: String, + pub admin_user_id: String, + pub review_status: String, + pub review_note: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseAssetAdminReviewInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_display_update_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_display_update_input_type.rs new file mode 100644 index 000000000..dbbd6a4b1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_display_update_input_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetDisplayUpdateInput { + pub showcase_id: String, + pub admin_user_id: String, + pub display_enabled: bool, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseAssetDisplayUpdateInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_table.rs new file mode 100644 index 000000000..ab54e1420 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_table.rs @@ -0,0 +1,162 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::editor_showcase_asset_like_type::EditorShowcaseAssetLike; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `editor_showcase_asset_like`. +/// +/// Obtain a handle from the [`EditorShowcaseAssetLikeTableAccess::editor_showcase_asset_like`] method on [`super::RemoteTables`], +/// like `ctx.db.editor_showcase_asset_like()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_showcase_asset_like().on_insert(...)`. +pub struct EditorShowcaseAssetLikeTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `editor_showcase_asset_like`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait EditorShowcaseAssetLikeTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`EditorShowcaseAssetLikeTableHandle`], which mediates access to the table `editor_showcase_asset_like`. + fn editor_showcase_asset_like(&self) -> EditorShowcaseAssetLikeTableHandle<'_>; +} + +impl EditorShowcaseAssetLikeTableAccess for super::RemoteTables { + fn editor_showcase_asset_like(&self) -> EditorShowcaseAssetLikeTableHandle<'_> { + EditorShowcaseAssetLikeTableHandle { + imp: self + .imp + .get_table::("editor_showcase_asset_like"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct EditorShowcaseAssetLikeInsertCallbackId(__sdk::CallbackId); +pub struct EditorShowcaseAssetLikeDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for EditorShowcaseAssetLikeTableHandle<'ctx> { + type Row = EditorShowcaseAssetLike; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = EditorShowcaseAssetLikeInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorShowcaseAssetLikeInsertCallbackId { + EditorShowcaseAssetLikeInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: EditorShowcaseAssetLikeInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = EditorShowcaseAssetLikeDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorShowcaseAssetLikeDeleteCallbackId { + EditorShowcaseAssetLikeDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: EditorShowcaseAssetLikeDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct EditorShowcaseAssetLikeUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for EditorShowcaseAssetLikeTableHandle<'ctx> { + type UpdateCallbackId = EditorShowcaseAssetLikeUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> EditorShowcaseAssetLikeUpdateCallbackId { + EditorShowcaseAssetLikeUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: EditorShowcaseAssetLikeUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `like_id` unique index on the table `editor_showcase_asset_like`, +/// which allows point queries on the field of the same name +/// via the [`EditorShowcaseAssetLikeLikeIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_showcase_asset_like().like_id().find(...)`. +pub struct EditorShowcaseAssetLikeLikeIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> EditorShowcaseAssetLikeTableHandle<'ctx> { + /// Get a handle on the `like_id` unique index on the table `editor_showcase_asset_like`. + pub fn like_id(&self) -> EditorShowcaseAssetLikeLikeIdUnique<'ctx> { + EditorShowcaseAssetLikeLikeIdUnique { + imp: self.imp.get_unique_constraint::("like_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> EditorShowcaseAssetLikeLikeIdUnique<'ctx> { + /// Find the subscribed row whose `like_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("editor_showcase_asset_like"); + _table.add_unique_constraint::("like_id", |row| &row.like_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `EditorShowcaseAssetLike`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait editor_showcase_asset_likeQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `EditorShowcaseAssetLike`. + fn editor_showcase_asset_like(&self) -> __sdk::__query_builder::Table; +} + +impl editor_showcase_asset_likeQueryTableAccess for __sdk::QueryTableAccessor { + fn editor_showcase_asset_like(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("editor_showcase_asset_like") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_toggle_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_toggle_input_type.rs new file mode 100644 index 000000000..11ab3d5a8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_toggle_input_type.rs @@ -0,0 +1,18 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetLikeToggleInput { + pub showcase_id: String, + pub user_id: String, + pub liked: bool, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseAssetLikeToggleInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_type.rs new file mode 100644 index 000000000..13bc136e0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_like_type.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetLike { + pub like_id: String, + pub showcase_id: String, + pub user_id: String, + pub created_at: __sdk::Timestamp, +} + +impl __sdk::InModule for EditorShowcaseAssetLike { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `EditorShowcaseAssetLike`. +/// +/// Provides typed access to columns for query building. +pub struct EditorShowcaseAssetLikeCols { + pub like_id: __sdk::__query_builder::Col, + pub showcase_id: __sdk::__query_builder::Col, + pub user_id: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for EditorShowcaseAssetLike { + type Cols = EditorShowcaseAssetLikeCols; + fn cols(table_name: &'static str) -> Self::Cols { + EditorShowcaseAssetLikeCols { + like_id: __sdk::__query_builder::Col::new(table_name, "like_id"), + showcase_id: __sdk::__query_builder::Col::new(table_name, "showcase_id"), + user_id: __sdk::__query_builder::Col::new(table_name, "user_id"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + } + } +} + +/// Indexed column accessor struct for the table `EditorShowcaseAssetLike`. +/// +/// Provides typed access to indexed columns for query building. +pub struct EditorShowcaseAssetLikeIxCols { + pub like_id: __sdk::__query_builder::IxCol, + pub showcase_id: __sdk::__query_builder::IxCol, + pub user_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for EditorShowcaseAssetLike { + type IxCols = EditorShowcaseAssetLikeIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + EditorShowcaseAssetLikeIxCols { + like_id: __sdk::__query_builder::IxCol::new(table_name, "like_id"), + showcase_id: __sdk::__query_builder::IxCol::new(table_name, "showcase_id"), + user_id: __sdk::__query_builder::IxCol::new(table_name, "user_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for EditorShowcaseAssetLike {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_list_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_list_procedure_result_type.rs new file mode 100644 index 000000000..c48fa4ebe --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_list_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_snapshot_type::EditorShowcaseAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetListProcedureResult { + pub ok: bool, + pub assets: Vec, + pub error_message: Option, +} + +impl __sdk::InModule for EditorShowcaseAssetListProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_procedure_result_type.rs new file mode 100644 index 000000000..76ed87de9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_snapshot_type::EditorShowcaseAssetSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetProcedureResult { + pub ok: bool, + pub asset: Option, + pub error_message: Option, +} + +impl __sdk::InModule for EditorShowcaseAssetProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_public_list_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_public_list_input_type.rs new file mode 100644 index 000000000..eee38c3d0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_public_list_input_type.rs @@ -0,0 +1,16 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetPublicListInput { + pub limit: u32, + pub cursor: Option, +} + +impl __sdk::InModule for EditorShowcaseAssetPublicListInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_refund_mark_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_refund_mark_input_type.rs new file mode 100644 index 000000000..cd7501fe8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_refund_mark_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetRefundMarkInput { + pub showcase_id: String, + pub refund_ledger_id: String, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseAssetRefundMarkInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs new file mode 100644 index 000000000..6c075a9cc --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs @@ -0,0 +1,48 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetSnapshot { + pub showcase_id: String, + pub asset_id: String, + pub owner_user_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub source_resource_id: Option, + pub asset_kind: Option, + pub generation_inputs_json: Option, + pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub refund_mud_points: u64, + pub review_status: String, + pub display_enabled: bool, + pub like_count: u64, + pub asset_deleted_while_pending: bool, + pub reviewed_by_admin_user_id: Option, + pub review_note: Option, + pub refund_ledger_id: Option, + pub refund_completed_at_micros: Option, + pub submitted_at_micros: i64, + pub reviewed_at_micros: Option, + pub approved_at_micros: Option, + pub rejected_at_micros: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseAssetSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_submit_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_submit_input_type.rs new file mode 100644 index 000000000..0172a99d8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_submit_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAssetSubmitInput { + pub asset_id: String, + pub owner_user_id: String, + pub now_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseAssetSubmitInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_table.rs new file mode 100644 index 000000000..3c12986be --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_table.rs @@ -0,0 +1,161 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::editor_showcase_asset_type::EditorShowcaseAsset; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `editor_showcase_asset`. +/// +/// Obtain a handle from the [`EditorShowcaseAssetTableAccess::editor_showcase_asset`] method on [`super::RemoteTables`], +/// like `ctx.db.editor_showcase_asset()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_showcase_asset().on_insert(...)`. +pub struct EditorShowcaseAssetTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `editor_showcase_asset`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait EditorShowcaseAssetTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`EditorShowcaseAssetTableHandle`], which mediates access to the table `editor_showcase_asset`. + fn editor_showcase_asset(&self) -> EditorShowcaseAssetTableHandle<'_>; +} + +impl EditorShowcaseAssetTableAccess for super::RemoteTables { + fn editor_showcase_asset(&self) -> EditorShowcaseAssetTableHandle<'_> { + EditorShowcaseAssetTableHandle { + imp: self + .imp + .get_table::("editor_showcase_asset"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct EditorShowcaseAssetInsertCallbackId(__sdk::CallbackId); +pub struct EditorShowcaseAssetDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for EditorShowcaseAssetTableHandle<'ctx> { + type Row = EditorShowcaseAsset; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = EditorShowcaseAssetInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorShowcaseAssetInsertCallbackId { + EditorShowcaseAssetInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: EditorShowcaseAssetInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = EditorShowcaseAssetDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorShowcaseAssetDeleteCallbackId { + EditorShowcaseAssetDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: EditorShowcaseAssetDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct EditorShowcaseAssetUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for EditorShowcaseAssetTableHandle<'ctx> { + type UpdateCallbackId = EditorShowcaseAssetUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> EditorShowcaseAssetUpdateCallbackId { + EditorShowcaseAssetUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: EditorShowcaseAssetUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `showcase_id` unique index on the table `editor_showcase_asset`, +/// which allows point queries on the field of the same name +/// via the [`EditorShowcaseAssetShowcaseIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_showcase_asset().showcase_id().find(...)`. +pub struct EditorShowcaseAssetShowcaseIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> EditorShowcaseAssetTableHandle<'ctx> { + /// Get a handle on the `showcase_id` unique index on the table `editor_showcase_asset`. + pub fn showcase_id(&self) -> EditorShowcaseAssetShowcaseIdUnique<'ctx> { + EditorShowcaseAssetShowcaseIdUnique { + imp: self.imp.get_unique_constraint::("showcase_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> EditorShowcaseAssetShowcaseIdUnique<'ctx> { + /// Find the subscribed row whose `showcase_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("editor_showcase_asset"); + _table.add_unique_constraint::("showcase_id", |row| &row.showcase_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `EditorShowcaseAsset`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait editor_showcase_assetQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `EditorShowcaseAsset`. + fn editor_showcase_asset(&self) -> __sdk::__query_builder::Table; +} + +impl editor_showcase_assetQueryTableAccess for __sdk::QueryTableAccessor { + fn editor_showcase_asset(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("editor_showcase_asset") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_type.rs new file mode 100644 index 000000000..50aca78fb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_type.rs @@ -0,0 +1,168 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseAsset { + pub showcase_id: String, + pub asset_id: String, + pub owner_user_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub source_resource_id: Option, + pub asset_kind: Option, + pub generation_inputs_json: Option, + pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub refund_mud_points: u64, + pub review_status: String, + pub display_enabled: bool, + pub like_count: u64, + pub asset_deleted_while_pending: bool, + pub reviewed_by_admin_user_id: Option, + pub review_note: Option, + pub refund_ledger_id: Option, + pub refund_completed_at: Option<__sdk::Timestamp>, + pub submitted_at: __sdk::Timestamp, + pub reviewed_at: Option<__sdk::Timestamp>, + pub approved_at: Option<__sdk::Timestamp>, + pub rejected_at: Option<__sdk::Timestamp>, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for EditorShowcaseAsset { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `EditorShowcaseAsset`. +/// +/// Provides typed access to columns for query building. +pub struct EditorShowcaseAssetCols { + pub showcase_id: __sdk::__query_builder::Col, + pub asset_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub label: __sdk::__query_builder::Col, + pub asset_object_id: __sdk::__query_builder::Col>, + pub image_src: __sdk::__query_builder::Col, + pub object_key: __sdk::__query_builder::Col>, + pub width: __sdk::__query_builder::Col, + pub height: __sdk::__query_builder::Col, + pub source_type: __sdk::__query_builder::Col, + pub prompt: __sdk::__query_builder::Col>, + pub actual_prompt: __sdk::__query_builder::Col>, + pub model: __sdk::__query_builder::Col>, + pub provider: __sdk::__query_builder::Col>, + pub task_id: __sdk::__query_builder::Col>, + pub source_resource_id: __sdk::__query_builder::Col>, + pub asset_kind: __sdk::__query_builder::Col>, + pub generation_inputs_json: __sdk::__query_builder::Col>, + pub thumbnail_src: __sdk::__query_builder::Col>, + pub generation_cost_mud_points: __sdk::__query_builder::Col, + pub refund_mud_points: __sdk::__query_builder::Col, + pub review_status: __sdk::__query_builder::Col, + pub display_enabled: __sdk::__query_builder::Col, + pub like_count: __sdk::__query_builder::Col, + pub asset_deleted_while_pending: __sdk::__query_builder::Col, + pub reviewed_by_admin_user_id: __sdk::__query_builder::Col>, + pub review_note: __sdk::__query_builder::Col>, + pub refund_ledger_id: __sdk::__query_builder::Col>, + pub refund_completed_at: + __sdk::__query_builder::Col>, + pub submitted_at: __sdk::__query_builder::Col, + pub reviewed_at: __sdk::__query_builder::Col>, + pub approved_at: __sdk::__query_builder::Col>, + pub rejected_at: __sdk::__query_builder::Col>, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for EditorShowcaseAsset { + type Cols = EditorShowcaseAssetCols; + fn cols(table_name: &'static str) -> Self::Cols { + EditorShowcaseAssetCols { + showcase_id: __sdk::__query_builder::Col::new(table_name, "showcase_id"), + asset_id: __sdk::__query_builder::Col::new(table_name, "asset_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + label: __sdk::__query_builder::Col::new(table_name, "label"), + asset_object_id: __sdk::__query_builder::Col::new(table_name, "asset_object_id"), + image_src: __sdk::__query_builder::Col::new(table_name, "image_src"), + object_key: __sdk::__query_builder::Col::new(table_name, "object_key"), + width: __sdk::__query_builder::Col::new(table_name, "width"), + height: __sdk::__query_builder::Col::new(table_name, "height"), + source_type: __sdk::__query_builder::Col::new(table_name, "source_type"), + prompt: __sdk::__query_builder::Col::new(table_name, "prompt"), + actual_prompt: __sdk::__query_builder::Col::new(table_name, "actual_prompt"), + model: __sdk::__query_builder::Col::new(table_name, "model"), + provider: __sdk::__query_builder::Col::new(table_name, "provider"), + task_id: __sdk::__query_builder::Col::new(table_name, "task_id"), + source_resource_id: __sdk::__query_builder::Col::new(table_name, "source_resource_id"), + asset_kind: __sdk::__query_builder::Col::new(table_name, "asset_kind"), + generation_inputs_json: __sdk::__query_builder::Col::new( + table_name, + "generation_inputs_json", + ), + thumbnail_src: __sdk::__query_builder::Col::new(table_name, "thumbnail_src"), + generation_cost_mud_points: __sdk::__query_builder::Col::new( + table_name, + "generation_cost_mud_points", + ), + refund_mud_points: __sdk::__query_builder::Col::new(table_name, "refund_mud_points"), + review_status: __sdk::__query_builder::Col::new(table_name, "review_status"), + display_enabled: __sdk::__query_builder::Col::new(table_name, "display_enabled"), + like_count: __sdk::__query_builder::Col::new(table_name, "like_count"), + asset_deleted_while_pending: __sdk::__query_builder::Col::new( + table_name, + "asset_deleted_while_pending", + ), + reviewed_by_admin_user_id: __sdk::__query_builder::Col::new( + table_name, + "reviewed_by_admin_user_id", + ), + review_note: __sdk::__query_builder::Col::new(table_name, "review_note"), + refund_ledger_id: __sdk::__query_builder::Col::new(table_name, "refund_ledger_id"), + refund_completed_at: __sdk::__query_builder::Col::new( + table_name, + "refund_completed_at", + ), + submitted_at: __sdk::__query_builder::Col::new(table_name, "submitted_at"), + reviewed_at: __sdk::__query_builder::Col::new(table_name, "reviewed_at"), + approved_at: __sdk::__query_builder::Col::new(table_name, "approved_at"), + rejected_at: __sdk::__query_builder::Col::new(table_name, "rejected_at"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `EditorShowcaseAsset`. +/// +/// Provides typed access to indexed columns for query building. +pub struct EditorShowcaseAssetIxCols { + pub owner_user_id: __sdk::__query_builder::IxCol, + pub review_status: __sdk::__query_builder::IxCol, + pub showcase_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for EditorShowcaseAsset { + type IxCols = EditorShowcaseAssetIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + EditorShowcaseAssetIxCols { + owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), + review_status: __sdk::__query_builder::IxCol::new(table_name, "review_status"), + showcase_id: __sdk::__query_builder::IxCol::new(table_name, "showcase_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for EditorShowcaseAsset {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_get_input_type.rs new file mode 100644 index 000000000..1d80ef213 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_get_input_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseCampaignConfigGetInput { + pub include_disabled: bool, +} + +impl __sdk::InModule for EditorShowcaseCampaignConfigGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_procedure_result_type.rs new file mode 100644 index 000000000..60da31414 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_campaign_config_snapshot_type::EditorShowcaseCampaignConfigSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseCampaignConfigProcedureResult { + pub ok: bool, + pub config: Option, + pub error_message: Option, +} + +impl __sdk::InModule for EditorShowcaseCampaignConfigProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs new file mode 100644 index 000000000..5f71e9552 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_snapshot_type.rs @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseCampaignConfigSnapshot { + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_by_admin_user_id: Option, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseCampaignConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_table.rs new file mode 100644 index 000000000..5e7f913fd --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_table.rs @@ -0,0 +1,169 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::editor_showcase_campaign_config_type::EditorShowcaseCampaignConfig; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `editor_showcase_campaign_config`. +/// +/// Obtain a handle from the [`EditorShowcaseCampaignConfigTableAccess::editor_showcase_campaign_config`] method on [`super::RemoteTables`], +/// like `ctx.db.editor_showcase_campaign_config()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_showcase_campaign_config().on_insert(...)`. +pub struct EditorShowcaseCampaignConfigTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `editor_showcase_campaign_config`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait EditorShowcaseCampaignConfigTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`EditorShowcaseCampaignConfigTableHandle`], which mediates access to the table `editor_showcase_campaign_config`. + fn editor_showcase_campaign_config(&self) -> EditorShowcaseCampaignConfigTableHandle<'_>; +} + +impl EditorShowcaseCampaignConfigTableAccess for super::RemoteTables { + fn editor_showcase_campaign_config(&self) -> EditorShowcaseCampaignConfigTableHandle<'_> { + EditorShowcaseCampaignConfigTableHandle { + imp: self + .imp + .get_table::("editor_showcase_campaign_config"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct EditorShowcaseCampaignConfigInsertCallbackId(__sdk::CallbackId); +pub struct EditorShowcaseCampaignConfigDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for EditorShowcaseCampaignConfigTableHandle<'ctx> { + type Row = EditorShowcaseCampaignConfig; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = EditorShowcaseCampaignConfigInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorShowcaseCampaignConfigInsertCallbackId { + EditorShowcaseCampaignConfigInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: EditorShowcaseCampaignConfigInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = EditorShowcaseCampaignConfigDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> EditorShowcaseCampaignConfigDeleteCallbackId { + EditorShowcaseCampaignConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: EditorShowcaseCampaignConfigDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct EditorShowcaseCampaignConfigUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for EditorShowcaseCampaignConfigTableHandle<'ctx> { + type UpdateCallbackId = EditorShowcaseCampaignConfigUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> EditorShowcaseCampaignConfigUpdateCallbackId { + EditorShowcaseCampaignConfigUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: EditorShowcaseCampaignConfigUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `config_id` unique index on the table `editor_showcase_campaign_config`, +/// which allows point queries on the field of the same name +/// via the [`EditorShowcaseCampaignConfigConfigIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.editor_showcase_campaign_config().config_id().find(...)`. +pub struct EditorShowcaseCampaignConfigConfigIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> EditorShowcaseCampaignConfigTableHandle<'ctx> { + /// Get a handle on the `config_id` unique index on the table `editor_showcase_campaign_config`. + pub fn config_id(&self) -> EditorShowcaseCampaignConfigConfigIdUnique<'ctx> { + EditorShowcaseCampaignConfigConfigIdUnique { + imp: self.imp.get_unique_constraint::("config_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> EditorShowcaseCampaignConfigConfigIdUnique<'ctx> { + /// Find the subscribed row whose `config_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache + .get_or_make_table::("editor_showcase_campaign_config"); + _table.add_unique_constraint::("config_id", |row| &row.config_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse( + "TableUpdate", + "TableUpdate", + ) + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `EditorShowcaseCampaignConfig`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait editor_showcase_campaign_configQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `EditorShowcaseCampaignConfig`. + fn editor_showcase_campaign_config( + &self, + ) -> __sdk::__query_builder::Table; +} + +impl editor_showcase_campaign_configQueryTableAccess for __sdk::QueryTableAccessor { + fn editor_showcase_campaign_config( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("editor_showcase_campaign_config") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs new file mode 100644 index 000000000..44b188966 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_type.rs @@ -0,0 +1,77 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseCampaignConfig { + pub config_id: String, + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_by_admin_user_id: Option, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for EditorShowcaseCampaignConfig { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `EditorShowcaseCampaignConfig`. +/// +/// Provides typed access to columns for query building. +pub struct EditorShowcaseCampaignConfigCols { + pub config_id: __sdk::__query_builder::Col, + pub enabled: __sdk::__query_builder::Col, + pub title: __sdk::__query_builder::Col, + pub image_src: __sdk::__query_builder::Col, + pub prompt: __sdk::__query_builder::Col, + pub author: __sdk::__query_builder::Col, + pub cost_text: __sdk::__query_builder::Col, + pub updated_by_admin_user_id: + __sdk::__query_builder::Col>, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig { + type Cols = EditorShowcaseCampaignConfigCols; + fn cols(table_name: &'static str) -> Self::Cols { + EditorShowcaseCampaignConfigCols { + config_id: __sdk::__query_builder::Col::new(table_name, "config_id"), + enabled: __sdk::__query_builder::Col::new(table_name, "enabled"), + title: __sdk::__query_builder::Col::new(table_name, "title"), + image_src: __sdk::__query_builder::Col::new(table_name, "image_src"), + prompt: __sdk::__query_builder::Col::new(table_name, "prompt"), + author: __sdk::__query_builder::Col::new(table_name, "author"), + cost_text: __sdk::__query_builder::Col::new(table_name, "cost_text"), + updated_by_admin_user_id: __sdk::__query_builder::Col::new( + table_name, + "updated_by_admin_user_id", + ), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `EditorShowcaseCampaignConfig`. +/// +/// Provides typed access to indexed columns for query building. +pub struct EditorShowcaseCampaignConfigIxCols { + pub config_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for EditorShowcaseCampaignConfig { + type IxCols = EditorShowcaseCampaignConfigIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + EditorShowcaseCampaignConfigIxCols { + config_id: __sdk::__query_builder::IxCol::new(table_name, "config_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for EditorShowcaseCampaignConfig {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs new file mode 100644 index 000000000..813101fd1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_campaign_config_upsert_input_type.rs @@ -0,0 +1,22 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct EditorShowcaseCampaignConfigUpsertInput { + pub admin_user_id: String, + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for EditorShowcaseCampaignConfigUpsertInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/get_editor_showcase_campaign_config_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/get_editor_showcase_campaign_config_and_return_procedure.rs new file mode 100644 index 000000000..baeb8c837 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/get_editor_showcase_campaign_config_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_campaign_config_get_input_type::EditorShowcaseCampaignConfigGetInput; +use super::editor_showcase_campaign_config_procedure_result_type::EditorShowcaseCampaignConfigProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct GetEditorShowcaseCampaignConfigAndReturnArgs { + pub input: EditorShowcaseCampaignConfigGetInput, +} + +impl __sdk::InModule for GetEditorShowcaseCampaignConfigAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `get_editor_showcase_campaign_config_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait get_editor_showcase_campaign_config_and_return { + fn get_editor_showcase_campaign_config_and_return( + &self, + input: EditorShowcaseCampaignConfigGetInput, + ) { + self.get_editor_showcase_campaign_config_and_return_then(input, |_, _| {}); + } + + fn get_editor_showcase_campaign_config_and_return_then( + &self, + input: EditorShowcaseCampaignConfigGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl get_editor_showcase_campaign_config_and_return for super::RemoteProcedures { + fn get_editor_showcase_campaign_config_and_return_then( + &self, + input: EditorShowcaseCampaignConfigGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseCampaignConfigProcedureResult>( + "get_editor_showcase_campaign_config_and_return", + GetEditorShowcaseCampaignConfigAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/list_public_editor_showcase_assets_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/list_public_editor_showcase_assets_and_return_procedure.rs new file mode 100644 index 000000000..9e357db4f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/list_public_editor_showcase_assets_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_list_procedure_result_type::EditorShowcaseAssetListProcedureResult; +use super::editor_showcase_asset_public_list_input_type::EditorShowcaseAssetPublicListInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ListPublicEditorShowcaseAssetsAndReturnArgs { + pub input: EditorShowcaseAssetPublicListInput, +} + +impl __sdk::InModule for ListPublicEditorShowcaseAssetsAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `list_public_editor_showcase_assets_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait list_public_editor_showcase_assets_and_return { + fn list_public_editor_showcase_assets_and_return( + &self, + input: EditorShowcaseAssetPublicListInput, + ) { + self.list_public_editor_showcase_assets_and_return_then(input, |_, _| {}); + } + + fn list_public_editor_showcase_assets_and_return_then( + &self, + input: EditorShowcaseAssetPublicListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl list_public_editor_showcase_assets_and_return for super::RemoteProcedures { + fn list_public_editor_showcase_assets_and_return_then( + &self, + input: EditorShowcaseAssetPublicListInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetListProcedureResult>( + "list_public_editor_showcase_assets_and_return", + ListPublicEditorShowcaseAssetsAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mark_editor_showcase_asset_refunded_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/mark_editor_showcase_asset_refunded_and_return_procedure.rs new file mode 100644 index 000000000..2da58f2ca --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/mark_editor_showcase_asset_refunded_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_procedure_result_type::EditorShowcaseAssetProcedureResult; +use super::editor_showcase_asset_refund_mark_input_type::EditorShowcaseAssetRefundMarkInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct MarkEditorShowcaseAssetRefundedAndReturnArgs { + pub input: EditorShowcaseAssetRefundMarkInput, +} + +impl __sdk::InModule for MarkEditorShowcaseAssetRefundedAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `mark_editor_showcase_asset_refunded_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait mark_editor_showcase_asset_refunded_and_return { + fn mark_editor_showcase_asset_refunded_and_return( + &self, + input: EditorShowcaseAssetRefundMarkInput, + ) { + self.mark_editor_showcase_asset_refunded_and_return_then(input, |_, _| {}); + } + + fn mark_editor_showcase_asset_refunded_and_return_then( + &self, + input: EditorShowcaseAssetRefundMarkInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl mark_editor_showcase_asset_refunded_and_return for super::RemoteProcedures { + fn mark_editor_showcase_asset_refunded_and_return_then( + &self, + input: EditorShowcaseAssetRefundMarkInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetProcedureResult>( + "mark_editor_showcase_asset_refunded_and_return", + MarkEditorShowcaseAssetRefundedAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/submit_editor_showcase_asset_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/submit_editor_showcase_asset_and_return_procedure.rs new file mode 100644 index 000000000..7002e581b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/submit_editor_showcase_asset_and_return_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_procedure_result_type::EditorShowcaseAssetProcedureResult; +use super::editor_showcase_asset_submit_input_type::EditorShowcaseAssetSubmitInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct SubmitEditorShowcaseAssetAndReturnArgs { + pub input: EditorShowcaseAssetSubmitInput, +} + +impl __sdk::InModule for SubmitEditorShowcaseAssetAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `submit_editor_showcase_asset_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait submit_editor_showcase_asset_and_return { + fn submit_editor_showcase_asset_and_return(&self, input: EditorShowcaseAssetSubmitInput) { + self.submit_editor_showcase_asset_and_return_then(input, |_, _| {}); + } + + fn submit_editor_showcase_asset_and_return_then( + &self, + input: EditorShowcaseAssetSubmitInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl submit_editor_showcase_asset_and_return for super::RemoteProcedures { + fn submit_editor_showcase_asset_and_return_then( + &self, + input: EditorShowcaseAssetSubmitInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetProcedureResult>( + "submit_editor_showcase_asset_and_return", + SubmitEditorShowcaseAssetAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/toggle_editor_showcase_asset_like_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/toggle_editor_showcase_asset_like_and_return_procedure.rs new file mode 100644 index 000000000..a014ceba1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/toggle_editor_showcase_asset_like_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_like_toggle_input_type::EditorShowcaseAssetLikeToggleInput; +use super::editor_showcase_asset_procedure_result_type::EditorShowcaseAssetProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct ToggleEditorShowcaseAssetLikeAndReturnArgs { + pub input: EditorShowcaseAssetLikeToggleInput, +} + +impl __sdk::InModule for ToggleEditorShowcaseAssetLikeAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `toggle_editor_showcase_asset_like_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait toggle_editor_showcase_asset_like_and_return { + fn toggle_editor_showcase_asset_like_and_return( + &self, + input: EditorShowcaseAssetLikeToggleInput, + ) { + self.toggle_editor_showcase_asset_like_and_return_then(input, |_, _| {}); + } + + fn toggle_editor_showcase_asset_like_and_return_then( + &self, + input: EditorShowcaseAssetLikeToggleInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl toggle_editor_showcase_asset_like_and_return for super::RemoteProcedures { + fn toggle_editor_showcase_asset_like_and_return_then( + &self, + input: EditorShowcaseAssetLikeToggleInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetProcedureResult>( + "toggle_editor_showcase_asset_like_and_return", + ToggleEditorShowcaseAssetLikeAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/update_editor_showcase_asset_display_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/update_editor_showcase_asset_display_and_return_procedure.rs new file mode 100644 index 000000000..40b912558 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/update_editor_showcase_asset_display_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_asset_display_update_input_type::EditorShowcaseAssetDisplayUpdateInput; +use super::editor_showcase_asset_procedure_result_type::EditorShowcaseAssetProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpdateEditorShowcaseAssetDisplayAndReturnArgs { + pub input: EditorShowcaseAssetDisplayUpdateInput, +} + +impl __sdk::InModule for UpdateEditorShowcaseAssetDisplayAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `update_editor_showcase_asset_display_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait update_editor_showcase_asset_display_and_return { + fn update_editor_showcase_asset_display_and_return( + &self, + input: EditorShowcaseAssetDisplayUpdateInput, + ) { + self.update_editor_showcase_asset_display_and_return_then(input, |_, _| {}); + } + + fn update_editor_showcase_asset_display_and_return_then( + &self, + input: EditorShowcaseAssetDisplayUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl update_editor_showcase_asset_display_and_return for super::RemoteProcedures { + fn update_editor_showcase_asset_display_and_return_then( + &self, + input: EditorShowcaseAssetDisplayUpdateInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseAssetProcedureResult>( + "update_editor_showcase_asset_display_and_return", + UpdateEditorShowcaseAssetDisplayAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/upsert_editor_showcase_campaign_config_and_return_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/upsert_editor_showcase_campaign_config_and_return_procedure.rs new file mode 100644 index 000000000..490d65d5d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/upsert_editor_showcase_campaign_config_and_return_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::editor_showcase_campaign_config_procedure_result_type::EditorShowcaseCampaignConfigProcedureResult; +use super::editor_showcase_campaign_config_upsert_input_type::EditorShowcaseCampaignConfigUpsertInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct UpsertEditorShowcaseCampaignConfigAndReturnArgs { + pub input: EditorShowcaseCampaignConfigUpsertInput, +} + +impl __sdk::InModule for UpsertEditorShowcaseCampaignConfigAndReturnArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `upsert_editor_showcase_campaign_config_and_return`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait upsert_editor_showcase_campaign_config_and_return { + fn upsert_editor_showcase_campaign_config_and_return( + &self, + input: EditorShowcaseCampaignConfigUpsertInput, + ) { + self.upsert_editor_showcase_campaign_config_and_return_then(input, |_, _| {}); + } + + fn upsert_editor_showcase_campaign_config_and_return_then( + &self, + input: EditorShowcaseCampaignConfigUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl upsert_editor_showcase_campaign_config_and_return for super::RemoteProcedures { + fn upsert_editor_showcase_campaign_config_and_return_then( + &self, + input: EditorShowcaseCampaignConfigUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, EditorShowcaseCampaignConfigProcedureResult>( + "upsert_editor_showcase_campaign_config_and_return", + UpsertEditorShowcaseCampaignConfigAndReturnArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index ba0fbb3cf..7e12f8cf5 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -9,6 +9,10 @@ const EDITOR_PROJECT_MAX_TITLE_CHARS: usize = 80; const EDITOR_ASSET_MAX_LABEL_CHARS: usize = 80; const EDITOR_PROJECT_MAX_LAYOUT_JSON_BYTES: usize = 256 * 1024; const EDITOR_PROJECT_SOURCE_TYPES: [&str; 3] = ["uploaded", "generated", "mock_generated"]; +const EDITOR_SHOWCASE_STATUS_PENDING: &str = "pending"; +const EDITOR_SHOWCASE_STATUS_APPROVED: &str = "approved"; +const EDITOR_SHOWCASE_STATUS_REJECTED: &str = "rejected"; +const EDITOR_SHOWCASE_CAMPAIGN_CONFIG_ID: &str = "global"; #[spacetimedb::table( accessor = editor_project, @@ -126,6 +130,78 @@ pub struct EditorAsset { source_resource_id: Option, #[default(None::)] thumbnail_src: Option, + #[default(0u64)] + generation_cost_mud_points: u64, +} + +#[spacetimedb::table( + accessor = editor_showcase_asset, + index(accessor = by_editor_showcase_asset_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_editor_showcase_asset_review_status, btree(columns = [review_status])) +)] +pub struct EditorShowcaseAsset { + #[primary_key] + showcase_id: String, + asset_id: String, + owner_user_id: String, + label: String, + asset_object_id: Option, + image_src: String, + object_key: Option, + width: u32, + height: u32, + source_type: String, + prompt: Option, + actual_prompt: Option, + model: Option, + provider: Option, + task_id: Option, + source_resource_id: Option, + asset_kind: Option, + generation_inputs_json: Option, + thumbnail_src: Option, + generation_cost_mud_points: u64, + refund_mud_points: u64, + review_status: String, + display_enabled: bool, + like_count: u64, + asset_deleted_while_pending: bool, + reviewed_by_admin_user_id: Option, + review_note: Option, + refund_ledger_id: Option, + refund_completed_at: Option, + submitted_at: Timestamp, + reviewed_at: Option, + approved_at: Option, + rejected_at: Option, + updated_at: Timestamp, +} + +#[spacetimedb::table( + accessor = editor_showcase_asset_like, + index(accessor = by_editor_showcase_asset_like_showcase_id, btree(columns = [showcase_id])), + index(accessor = by_editor_showcase_asset_like_user_id, btree(columns = [user_id])) +)] +pub struct EditorShowcaseAssetLike { + #[primary_key] + like_id: String, + showcase_id: String, + user_id: String, + created_at: Timestamp, +} + +#[spacetimedb::table(accessor = editor_showcase_campaign_config)] +pub struct EditorShowcaseCampaignConfig { + #[primary_key] + config_id: String, + enabled: bool, + title: String, + image_src: String, + prompt: String, + author: String, + cost_text: String, + updated_by_admin_user_id: Option, + updated_at: Timestamp, } #[derive(Clone, Debug, PartialEq, SpacetimeType)] @@ -286,6 +362,11 @@ pub struct EditorAssetSnapshot { pub created_at_micros: i64, pub updated_at_micros: i64, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub showcase_id: Option, + pub showcase_review_status: Option, + pub showcase_display_enabled: Option, + pub showcase_like_count: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] @@ -294,6 +375,56 @@ pub struct EditorAssetLibrarySnapshot { pub assets: Vec, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetSnapshot { + pub showcase_id: String, + pub asset_id: String, + pub owner_user_id: String, + pub label: String, + pub asset_object_id: Option, + pub image_src: String, + pub object_key: Option, + pub width: u32, + pub height: u32, + pub source_type: String, + pub prompt: Option, + pub actual_prompt: Option, + pub model: Option, + pub provider: Option, + pub task_id: Option, + pub source_resource_id: Option, + pub asset_kind: Option, + pub generation_inputs_json: Option, + pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, + pub refund_mud_points: u64, + pub review_status: String, + pub display_enabled: bool, + pub like_count: u64, + pub asset_deleted_while_pending: bool, + pub reviewed_by_admin_user_id: Option, + pub review_note: Option, + pub refund_ledger_id: Option, + pub refund_completed_at_micros: Option, + pub submitted_at_micros: i64, + pub reviewed_at_micros: Option, + pub approved_at_micros: Option, + pub rejected_at_micros: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseCampaignConfigSnapshot { + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_by_admin_user_id: Option, + pub updated_at_micros: i64, +} + #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct EditorAssetLibraryGetInput { pub owner_user_id: String, @@ -347,6 +478,7 @@ pub struct EditorAssetCreateInput { pub source_resource_id: Option, pub now_micros: i64, pub thumbnail_src: Option, + pub generation_cost_mud_points: u64, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] @@ -374,6 +506,79 @@ pub struct EditorAssetDeleteInput { pub owner_user_id: String, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetSubmitInput { + pub asset_id: String, + pub owner_user_id: String, + pub now_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetPublicListInput { + pub limit: u32, + pub cursor: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetAdminListInput { + pub limit: u32, + pub cursor: Option, + pub owner_user_id: Option, + pub asset_kind: Option, + pub review_status: Option, + pub submitted_after_micros: Option, + pub submitted_before_micros: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetAdminReviewInput { + pub showcase_id: String, + pub admin_user_id: String, + pub review_status: String, + pub review_note: Option, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetDisplayUpdateInput { + pub showcase_id: String, + pub admin_user_id: String, + pub display_enabled: bool, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetRefundMarkInput { + pub showcase_id: String, + pub refund_ledger_id: String, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetLikeToggleInput { + pub showcase_id: String, + pub user_id: String, + pub liked: bool, + pub updated_at_micros: i64, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseCampaignConfigGetInput { + pub include_disabled: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseCampaignConfigUpsertInput { + pub admin_user_id: String, + pub enabled: bool, + pub title: String, + pub image_src: String, + pub prompt: String, + pub author: String, + pub cost_text: String, + pub updated_at_micros: i64, +} + #[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct EditorCanvasSnapshot { pub canvas_id: String, @@ -467,6 +672,27 @@ pub struct EditorAssetProcedureResult { pub error_message: Option, } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetProcedureResult { + pub ok: bool, + pub asset: Option, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseAssetListProcedureResult { + pub ok: bool, + pub assets: Vec, + pub error_message: Option, +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct EditorShowcaseCampaignConfigProcedureResult { + pub ok: bool, + pub config: Option, + pub error_message: Option, +} + #[spacetimedb::procedure] pub fn create_editor_project_and_return( ctx: &mut ProcedureContext, @@ -735,6 +961,105 @@ pub fn delete_editor_asset_and_return( } } +#[spacetimedb::procedure] +pub fn submit_editor_showcase_asset_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetSubmitInput, +) -> EditorShowcaseAssetProcedureResult { + match ctx.try_with_tx(|tx| submit_editor_showcase_asset(tx, input.clone())) { + Ok(asset) => editor_showcase_asset_ok(Some(asset)), + Err(message) => editor_showcase_asset_error(message), + } +} + +#[spacetimedb::procedure] +pub fn list_public_editor_showcase_assets_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetPublicListInput, +) -> EditorShowcaseAssetListProcedureResult { + match ctx.try_with_tx(|tx| list_public_editor_showcase_assets(tx, input.clone())) { + Ok(assets) => editor_showcase_asset_list_ok(assets), + Err(message) => editor_showcase_asset_list_error(message), + } +} + +#[spacetimedb::procedure] +pub fn admin_list_editor_showcase_assets_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetAdminListInput, +) -> EditorShowcaseAssetListProcedureResult { + match ctx.try_with_tx(|tx| admin_list_editor_showcase_assets(tx, input.clone())) { + Ok(assets) => editor_showcase_asset_list_ok(assets), + Err(message) => editor_showcase_asset_list_error(message), + } +} + +#[spacetimedb::procedure] +pub fn admin_review_editor_showcase_asset_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetAdminReviewInput, +) -> EditorShowcaseAssetProcedureResult { + match ctx.try_with_tx(|tx| admin_review_editor_showcase_asset(tx, input.clone())) { + Ok(asset) => editor_showcase_asset_ok(Some(asset)), + Err(message) => editor_showcase_asset_error(message), + } +} + +#[spacetimedb::procedure] +pub fn update_editor_showcase_asset_display_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetDisplayUpdateInput, +) -> EditorShowcaseAssetProcedureResult { + match ctx.try_with_tx(|tx| update_editor_showcase_asset_display(tx, input.clone())) { + Ok(asset) => editor_showcase_asset_ok(Some(asset)), + Err(message) => editor_showcase_asset_error(message), + } +} + +#[spacetimedb::procedure] +pub fn mark_editor_showcase_asset_refunded_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetRefundMarkInput, +) -> EditorShowcaseAssetProcedureResult { + match ctx.try_with_tx(|tx| mark_editor_showcase_asset_refunded(tx, input.clone())) { + Ok(asset) => editor_showcase_asset_ok(Some(asset)), + Err(message) => editor_showcase_asset_error(message), + } +} + +#[spacetimedb::procedure] +pub fn toggle_editor_showcase_asset_like_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseAssetLikeToggleInput, +) -> EditorShowcaseAssetProcedureResult { + match ctx.try_with_tx(|tx| toggle_editor_showcase_asset_like(tx, input.clone())) { + Ok(asset) => editor_showcase_asset_ok(Some(asset)), + Err(message) => editor_showcase_asset_error(message), + } +} + +#[spacetimedb::procedure] +pub fn get_editor_showcase_campaign_config_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseCampaignConfigGetInput, +) -> EditorShowcaseCampaignConfigProcedureResult { + match ctx.try_with_tx(|tx| get_editor_showcase_campaign_config(tx, input.clone())) { + Ok(config) => editor_showcase_campaign_config_ok(config), + Err(message) => editor_showcase_campaign_config_error(message), + } +} + +#[spacetimedb::procedure] +pub fn upsert_editor_showcase_campaign_config_and_return( + ctx: &mut ProcedureContext, + input: EditorShowcaseCampaignConfigUpsertInput, +) -> EditorShowcaseCampaignConfigProcedureResult { + match ctx.try_with_tx(|tx| upsert_editor_showcase_campaign_config(tx, input.clone())) { + Ok(config) => editor_showcase_campaign_config_ok(Some(config)), + Err(message) => editor_showcase_campaign_config_error(message), + } +} + fn create_editor_project( ctx: &ReducerContext, input: EditorProjectCreateInput, @@ -1021,15 +1346,7 @@ fn create_editor_project_resource( return Err("画布资源已存在".to_string()); } - let public_showcase_enabled = source_type != "generated" - || !project_resource_input_copies_source_media( - ctx, - owner_user_id.as_str(), - source_resource_id.as_ref(), - asset_object_id.as_ref(), - object_key.as_ref(), - image_src.as_str(), - ); + let public_showcase_enabled = false; let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); ctx.db .editor_project_resource() @@ -1273,6 +1590,7 @@ fn create_editor_asset( generation_inputs_json: normalize_optional(input.generation_inputs_json), source_resource_id: normalize_optional(input.source_resource_id), thumbnail_src: normalize_optional(input.thumbnail_src), + generation_cost_mud_points: input.generation_cost_mud_points, }); ctx.db .editor_asset() @@ -1322,6 +1640,7 @@ fn update_editor_asset( generation_inputs_json: asset.generation_inputs_json, source_resource_id: asset.source_resource_id, thumbnail_src: asset.thumbnail_src, + generation_cost_mud_points: asset.generation_cost_mud_points, }); ctx.db .editor_asset() @@ -1367,6 +1686,7 @@ fn repair_editor_asset_media( generation_inputs_json: asset.generation_inputs_json, source_resource_id: asset.source_resource_id, thumbnail_src: asset.thumbnail_src, + generation_cost_mud_points: asset.generation_cost_mud_points, }); ctx.db @@ -1384,10 +1704,442 @@ fn delete_editor_asset( let asset_id = normalize_required(&input.asset_id, "editor_asset.asset_id")?; let owner_user_id = normalize_required(&input.owner_user_id, "editor_asset.owner_user_id")?; let asset = require_owned_asset(ctx, asset_id.as_str(), owner_user_id.as_str())?; + let showcase_id = showcase_id_for_asset(asset.asset_id.as_str()); + if let Some(showcase_asset) = ctx + .db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + { + if showcase_asset.owner_user_id != owner_user_id { + return Err("无权访问该精选审核记录".to_string()); + } + if showcase_asset.review_status == EDITOR_SHOWCASE_STATUS_PENDING { + ctx.db + .editor_showcase_asset() + .showcase_id() + .delete(&showcase_asset.showcase_id); + ctx.db.editor_showcase_asset().insert(EditorShowcaseAsset { + asset_deleted_while_pending: true, + ..showcase_asset + }); + } else if showcase_asset.review_status == EDITOR_SHOWCASE_STATUS_REJECTED { + ctx.db + .editor_showcase_asset() + .showcase_id() + .delete(&showcase_asset.showcase_id); + } + } ctx.db.editor_asset().asset_id().delete(&asset_id); Ok(asset_snapshot_from_row(ctx, asset)) } +fn submit_editor_showcase_asset( + ctx: &ReducerContext, + input: EditorShowcaseAssetSubmitInput, +) -> Result { + let asset_id = normalize_required(&input.asset_id, "editor_showcase_asset.asset_id")?; + let owner_user_id = + normalize_required(&input.owner_user_id, "editor_showcase_asset.owner_user_id")?; + let asset = require_owned_asset(ctx, asset_id.as_str(), owner_user_id.as_str())?; + if asset.source_type != "generated" { + return Err("只有生成素材可以提交精选审核".to_string()); + } + if asset.image_src.trim().is_empty() { + return Err("没有媒体内容的素材不能提交精选审核".to_string()); + } + + let showcase_id = showcase_id_for_asset(asset_id.as_str()); + if let Some(existing) = ctx + .db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + { + if existing.review_status == EDITOR_SHOWCASE_STATUS_REJECTED { + return Err("该素材审核未通过,不能重复提交".to_string()); + } + return Ok(showcase_snapshot_from_row(existing)); + } + + let now = Timestamp::from_micros_since_unix_epoch(input.now_micros); + let refund_mud_points = asset.generation_cost_mud_points.saturating_add(1) / 2; + ctx.db.editor_showcase_asset().insert(EditorShowcaseAsset { + showcase_id: showcase_id.clone(), + asset_id: asset.asset_id, + owner_user_id: asset.owner_user_id, + label: asset.label, + asset_object_id: asset.asset_object_id, + image_src: asset.image_src, + object_key: asset.object_key, + width: asset.width, + height: asset.height, + source_type: asset.source_type, + prompt: asset.prompt, + actual_prompt: asset.actual_prompt, + model: asset.model, + provider: asset.provider, + task_id: asset.task_id, + source_resource_id: asset.source_resource_id, + asset_kind: asset.asset_kind, + generation_inputs_json: asset.generation_inputs_json, + thumbnail_src: asset.thumbnail_src, + generation_cost_mud_points: asset.generation_cost_mud_points, + refund_mud_points, + review_status: EDITOR_SHOWCASE_STATUS_PENDING.to_string(), + display_enabled: false, + like_count: 0, + asset_deleted_while_pending: false, + reviewed_by_admin_user_id: None, + review_note: None, + refund_ledger_id: None, + refund_completed_at: None, + submitted_at: now, + reviewed_at: None, + approved_at: None, + rejected_at: None, + updated_at: now, + }); + + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + .map(showcase_snapshot_from_row) + .ok_or_else(|| "精选审核记录创建失败".to_string()) +} + +fn list_public_editor_showcase_assets( + ctx: &ReducerContext, + input: EditorShowcaseAssetPublicListInput, +) -> Result, String> { + let limit = input.limit.clamp(1, 120) as usize; + let cursor = parse_public_showcase_cursor(input.cursor.as_deref()); + let mut assets = ctx + .db + .editor_showcase_asset() + .iter() + .filter(|asset| { + let approved_at_micros = asset + .approved_at + .unwrap_or(asset.updated_at) + .to_micros_since_unix_epoch(); + asset.review_status == EDITOR_SHOWCASE_STATUS_APPROVED + && asset.display_enabled + && !asset.asset_deleted_while_pending + && (asset.refund_mud_points == 0 || asset.refund_completed_at.is_some()) + && !asset.image_src.trim().is_empty() + && is_after_public_showcase_cursor( + approved_at_micros, + asset.showcase_id.as_str(), + cursor.as_ref(), + ) + }) + .map(showcase_snapshot_from_row) + .collect::>(); + + assets.sort_by(|left, right| { + right + .approved_at_micros + .unwrap_or(right.updated_at_micros) + .cmp(&left.approved_at_micros.unwrap_or(left.updated_at_micros)) + .then_with(|| right.showcase_id.cmp(&left.showcase_id)) + }); + assets.truncate(limit); + Ok(assets) +} + +fn admin_list_editor_showcase_assets( + ctx: &ReducerContext, + input: EditorShowcaseAssetAdminListInput, +) -> Result, String> { + let limit = input.limit.clamp(1, 500) as usize; + let cursor = parse_public_showcase_cursor(input.cursor.as_deref()); + let owner_user_id = normalize_optional(input.owner_user_id); + let asset_kind = normalize_optional(input.asset_kind); + let review_status = normalize_optional(input.review_status) + .map(|status| normalize_showcase_review_status(status.as_str())) + .transpose()?; + let mut assets = ctx + .db + .editor_showcase_asset() + .iter() + .filter(|asset| { + let submitted_at_micros = asset.submitted_at.to_micros_since_unix_epoch(); + owner_user_id + .as_ref() + .is_none_or(|value| asset.owner_user_id == *value) + && asset_kind.as_ref().is_none_or(|value| { + asset + .asset_kind + .as_deref() + .map(str::trim) + .is_some_and(|kind| kind == value) + }) + && review_status + .as_ref() + .is_none_or(|value| asset.review_status == *value) + && input + .submitted_after_micros + .is_none_or(|value| submitted_at_micros >= value) + && input + .submitted_before_micros + .is_none_or(|value| submitted_at_micros <= value) + && is_after_public_showcase_cursor( + submitted_at_micros, + asset.showcase_id.as_str(), + cursor.as_ref(), + ) + }) + .map(showcase_snapshot_from_row) + .collect::>(); + + assets.sort_by(|left, right| { + right + .submitted_at_micros + .cmp(&left.submitted_at_micros) + .then_with(|| right.showcase_id.cmp(&left.showcase_id)) + }); + assets.truncate(limit); + Ok(assets) +} + +fn admin_review_editor_showcase_asset( + ctx: &ReducerContext, + input: EditorShowcaseAssetAdminReviewInput, +) -> Result { + let showcase_id = normalize_required(&input.showcase_id, "editor_showcase_asset.showcase_id")?; + let admin_user_id = normalize_required( + &input.admin_user_id, + "editor_showcase_asset.reviewed_by_admin_user_id", + )?; + let review_status = normalize_showcase_review_status(input.review_status.as_str())?; + if review_status == EDITOR_SHOWCASE_STATUS_PENDING { + return Err("审核操作只能批准或拒绝".to_string()); + } + let asset = require_showcase_asset(ctx, showcase_id.as_str())?; + if asset.review_status != EDITOR_SHOWCASE_STATUS_PENDING { + if asset.review_status == EDITOR_SHOWCASE_STATUS_APPROVED + && review_status == EDITOR_SHOWCASE_STATUS_APPROVED + { + return Ok(showcase_snapshot_from_row(asset)); + } + return Err("只有待审核素材可以审核".to_string()); + } + + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + let approved = review_status == EDITOR_SHOWCASE_STATUS_APPROVED; + if approved && asset.asset_deleted_while_pending { + return Err("素材已删除,不能通过精选审核".to_string()); + } + let refund_ledger_id = if approved && asset.refund_mud_points > 0 { + Some(editor_showcase_refund_ledger_id(showcase_id.as_str())) + } else { + None + }; + ctx.db + .editor_showcase_asset() + .showcase_id() + .delete(&showcase_id); + ctx.db.editor_showcase_asset().insert(EditorShowcaseAsset { + review_status, + display_enabled: approved && asset.refund_mud_points == 0, + reviewed_by_admin_user_id: Some(admin_user_id), + review_note: normalize_optional(input.review_note), + refund_ledger_id, + reviewed_at: Some(now), + approved_at: if approved { Some(now) } else { None }, + rejected_at: if approved { None } else { Some(now) }, + updated_at: now, + ..asset + }); + + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + .map(showcase_snapshot_from_row) + .ok_or_else(|| "精选审核记录更新失败".to_string()) +} + +fn update_editor_showcase_asset_display( + ctx: &ReducerContext, + input: EditorShowcaseAssetDisplayUpdateInput, +) -> Result { + let showcase_id = normalize_required(&input.showcase_id, "editor_showcase_asset.showcase_id")?; + let admin_user_id = normalize_required( + &input.admin_user_id, + "editor_showcase_asset.reviewed_by_admin_user_id", + )?; + let asset = require_showcase_asset(ctx, showcase_id.as_str())?; + if asset.review_status != EDITOR_SHOWCASE_STATUS_APPROVED { + return Err("只有已通过审核的精选素材可以修改展示状态".to_string()); + } + if asset.asset_deleted_while_pending { + return Err("素材已删除,不能公开展示".to_string()); + } + if input.display_enabled && asset.refund_mud_points > 0 && asset.refund_completed_at.is_none() { + return Err("精选返还完成前不能公开展示".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + ctx.db + .editor_showcase_asset() + .showcase_id() + .delete(&showcase_id); + ctx.db.editor_showcase_asset().insert(EditorShowcaseAsset { + display_enabled: input.display_enabled, + reviewed_by_admin_user_id: Some(admin_user_id), + updated_at: now, + ..asset + }); + + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + .map(showcase_snapshot_from_row) + .ok_or_else(|| "精选展示状态更新失败".to_string()) +} + +fn mark_editor_showcase_asset_refunded( + ctx: &ReducerContext, + input: EditorShowcaseAssetRefundMarkInput, +) -> Result { + let showcase_id = normalize_required(&input.showcase_id, "editor_showcase_asset.showcase_id")?; + let refund_ledger_id = normalize_required( + &input.refund_ledger_id, + "editor_showcase_asset.refund_ledger_id", + )?; + let asset = require_showcase_asset(ctx, showcase_id.as_str())?; + if asset.refund_ledger_id.as_deref() != Some(refund_ledger_id.as_str()) { + return Err("精选返还流水不匹配".to_string()); + } + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + ctx.db + .editor_showcase_asset() + .showcase_id() + .delete(&showcase_id); + ctx.db.editor_showcase_asset().insert(EditorShowcaseAsset { + display_enabled: true, + refund_completed_at: Some(now), + updated_at: now, + ..asset + }); + + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + .map(showcase_snapshot_from_row) + .ok_or_else(|| "精选返还状态更新失败".to_string()) +} + +fn toggle_editor_showcase_asset_like( + ctx: &ReducerContext, + input: EditorShowcaseAssetLikeToggleInput, +) -> Result { + let showcase_id = normalize_required(&input.showcase_id, "editor_showcase_asset.showcase_id")?; + let user_id = normalize_required(&input.user_id, "editor_showcase_asset_like.user_id")?; + let asset = require_showcase_asset(ctx, showcase_id.as_str())?; + if asset.review_status != EDITOR_SHOWCASE_STATUS_APPROVED || !asset.display_enabled { + return Err("精选素材未展示,不能点赞".to_string()); + } + let like_id = editor_showcase_like_id(showcase_id.as_str(), user_id.as_str()); + let existing_like = ctx.db.editor_showcase_asset_like().like_id().find(&like_id); + if input.liked && existing_like.is_none() { + ctx.db + .editor_showcase_asset_like() + .insert(EditorShowcaseAssetLike { + like_id, + showcase_id: showcase_id.clone(), + user_id, + created_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros), + }); + return update_editor_showcase_like_count(ctx, asset, 1); + } + if !input.liked { + if existing_like.is_some() { + ctx.db + .editor_showcase_asset_like() + .like_id() + .delete(&like_id); + return update_editor_showcase_like_count(ctx, asset, -1); + } + } + Ok(showcase_snapshot_from_row(asset)) +} + +fn get_editor_showcase_campaign_config( + ctx: &ReducerContext, + input: EditorShowcaseCampaignConfigGetInput, +) -> Result, String> { + let Some(config) = ctx + .db + .editor_showcase_campaign_config() + .config_id() + .find(&EDITOR_SHOWCASE_CAMPAIGN_CONFIG_ID.to_string()) + else { + return Ok(None); + }; + if !input.include_disabled && !config.enabled { + return Ok(None); + } + Ok(Some(showcase_campaign_config_snapshot_from_row(config))) +} + +fn upsert_editor_showcase_campaign_config( + ctx: &ReducerContext, + input: EditorShowcaseCampaignConfigUpsertInput, +) -> Result { + let admin_user_id = normalize_required( + &input.admin_user_id, + "editor_showcase_campaign_config.updated_by_admin_user_id", + )?; + let title = input.title.trim().to_string(); + let image_src = input.image_src.trim().to_string(); + if input.enabled { + if title.is_empty() { + return Err("editor_showcase_campaign_config.title 不能为空".to_string()); + } + if image_src.is_empty() { + return Err("editor_showcase_campaign_config.image_src 不能为空".to_string()); + } + } + let now = Timestamp::from_micros_since_unix_epoch(input.updated_at_micros); + let config_id = EDITOR_SHOWCASE_CAMPAIGN_CONFIG_ID.to_string(); + if ctx + .db + .editor_showcase_campaign_config() + .config_id() + .find(&config_id) + .is_some() + { + ctx.db + .editor_showcase_campaign_config() + .config_id() + .delete(&config_id); + } + ctx.db + .editor_showcase_campaign_config() + .insert(EditorShowcaseCampaignConfig { + config_id: config_id.clone(), + enabled: input.enabled, + title, + image_src, + prompt: input.prompt.trim().to_string(), + author: input.author.trim().to_string(), + cost_text: input.cost_text.trim().to_string(), + updated_by_admin_user_id: Some(admin_user_id), + updated_at: now, + }); + ctx.db + .editor_showcase_campaign_config() + .config_id() + .find(&config_id) + .map(showcase_campaign_config_snapshot_from_row) + .ok_or_else(|| "精选活动卡保存失败".to_string()) +} + fn build_project_snapshot( ctx: &ReducerContext, project_id: &str, @@ -1434,6 +2186,9 @@ fn update_editor_project_resource_showcase( ctx: &ReducerContext, input: EditorProjectResourceShowcaseUpdateInput, ) -> Result { + if input.public_showcase_enabled { + return Err("精选公开请从素材库提交审核".to_string()); + } let resource_id = normalize_required(&input.resource_id, "editor_project_resource.resource_id")?; let owner_user_id = normalize_required( @@ -1465,38 +2220,10 @@ fn update_editor_project_resource_showcase( } fn list_public_editor_project_resources( - ctx: &ReducerContext, - input: EditorProjectResourcePublicShowcaseListInput, + _ctx: &ReducerContext, + _input: EditorProjectResourcePublicShowcaseListInput, ) -> Result, String> { - let limit = input.limit.clamp(1, 120) as usize; - let cursor = parse_public_showcase_cursor(input.cursor.as_deref()); - let mut resources = ctx - .db - .editor_project_resource() - .iter() - .filter(|resource| { - resource.public_showcase_enabled - && resource.source_type == "generated" - && !resource.image_src.trim().is_empty() - && !project_resource_copies_source_media(ctx, resource) - && is_after_public_showcase_cursor( - resource.created_at.to_micros_since_unix_epoch(), - resource.resource_id.as_str(), - cursor.as_ref(), - ) - }) - .map(resource_snapshot_from_row) - .collect::>(); - - resources.sort_by(|left, right| { - right - .created_at_micros - .cmp(&left.created_at_micros) - .then_with(|| right.resource_id.cmp(&left.resource_id)) - }); - resources.truncate(limit); - - Ok(resources) + Ok(Vec::new()) } fn parse_public_showcase_cursor(cursor: Option<&str>) -> Option<(i64, String)> { @@ -1688,6 +2415,18 @@ fn require_owned_asset( Ok(asset) } +fn require_showcase_asset( + ctx: &ReducerContext, + showcase_id: &str, +) -> Result { + let showcase_key = showcase_id.to_string(); + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_key) + .ok_or_else(|| "精选审核记录不存在".to_string()) +} + fn build_asset_library_snapshot( ctx: &ReducerContext, owner_user_id: &str, @@ -1721,6 +2460,66 @@ fn build_asset_library_snapshot( Ok(EditorAssetLibrarySnapshot { folders, assets }) } +fn find_showcase_asset_for_asset( + ctx: &ReducerContext, + asset_id: &str, +) -> Option { + let showcase_id = showcase_id_for_asset(asset_id); + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) +} + +fn showcase_id_for_asset(asset_id: &str) -> String { + format!("editor-showcase:{asset_id}") +} + +fn editor_showcase_like_id(showcase_id: &str, user_id: &str) -> String { + format!("{showcase_id}:{user_id}") +} + +fn editor_showcase_refund_ledger_id(showcase_id: &str) -> String { + format!("editor-showcase-refund:{showcase_id}") +} + +fn normalize_showcase_review_status(value: &str) -> Result { + let normalized = value.trim(); + match normalized { + EDITOR_SHOWCASE_STATUS_PENDING + | EDITOR_SHOWCASE_STATUS_APPROVED + | EDITOR_SHOWCASE_STATUS_REJECTED => Ok(normalized.to_string()), + _ => Err("精选审核状态只支持 pending、approved 或 rejected".to_string()), + } +} + +fn update_editor_showcase_like_count( + ctx: &ReducerContext, + asset: EditorShowcaseAsset, + delta: i64, +) -> Result { + let showcase_id = asset.showcase_id.clone(); + let like_count = if delta < 0 { + asset.like_count.saturating_sub(delta.unsigned_abs()) + } else { + asset.like_count.saturating_add(delta as u64) + }; + ctx.db + .editor_showcase_asset() + .showcase_id() + .delete(&showcase_id); + ctx.db.editor_showcase_asset().insert(EditorShowcaseAsset { + like_count, + ..asset + }); + ctx.db + .editor_showcase_asset() + .showcase_id() + .find(&showcase_id) + .map(showcase_snapshot_from_row) + .ok_or_else(|| "精选点赞状态更新失败".to_string()) +} + fn asset_folder_snapshot_from_row(row: EditorAssetFolder) -> EditorAssetFolderSnapshot { EditorAssetFolderSnapshot { folder_id: row.folder_id, @@ -1733,6 +2532,64 @@ fn asset_folder_snapshot_from_row(row: EditorAssetFolder) -> EditorAssetFolderSn } } +fn showcase_snapshot_from_row(row: EditorShowcaseAsset) -> EditorShowcaseAssetSnapshot { + EditorShowcaseAssetSnapshot { + showcase_id: row.showcase_id, + asset_id: row.asset_id, + owner_user_id: row.owner_user_id, + label: row.label, + asset_object_id: row.asset_object_id, + image_src: row.image_src, + object_key: row.object_key, + width: row.width, + height: row.height, + source_type: row.source_type, + prompt: row.prompt, + actual_prompt: row.actual_prompt, + model: row.model, + provider: row.provider, + task_id: row.task_id, + source_resource_id: row.source_resource_id, + asset_kind: row.asset_kind, + generation_inputs_json: row.generation_inputs_json, + thumbnail_src: row.thumbnail_src, + generation_cost_mud_points: row.generation_cost_mud_points, + refund_mud_points: row.refund_mud_points, + review_status: row.review_status, + display_enabled: row.display_enabled, + like_count: row.like_count, + asset_deleted_while_pending: row.asset_deleted_while_pending, + reviewed_by_admin_user_id: row.reviewed_by_admin_user_id, + review_note: row.review_note, + refund_ledger_id: row.refund_ledger_id, + refund_completed_at_micros: timestamp_option_micros(row.refund_completed_at), + submitted_at_micros: row.submitted_at.to_micros_since_unix_epoch(), + reviewed_at_micros: timestamp_option_micros(row.reviewed_at), + approved_at_micros: timestamp_option_micros(row.approved_at), + rejected_at_micros: timestamp_option_micros(row.rejected_at), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn showcase_campaign_config_snapshot_from_row( + row: EditorShowcaseCampaignConfig, +) -> EditorShowcaseCampaignConfigSnapshot { + EditorShowcaseCampaignConfigSnapshot { + enabled: row.enabled, + title: row.title, + image_src: row.image_src, + prompt: row.prompt, + author: row.author, + cost_text: row.cost_text, + updated_by_admin_user_id: row.updated_by_admin_user_id, + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + +fn timestamp_option_micros(value: Option) -> Option { + value.map(|timestamp| timestamp.to_micros_since_unix_epoch()) +} + fn asset_snapshot_from_row(ctx: &ReducerContext, row: EditorAsset) -> EditorAssetSnapshot { let linked_resource = find_asset_source_project_resource(ctx, &row); let inferred_source_resource_id = linked_resource @@ -1741,6 +2598,7 @@ fn asset_snapshot_from_row(ctx: &ReducerContext, row: EditorAsset) -> EditorAsse let public_showcase_enabled = linked_resource .as_ref() .map(|resource| resource.public_showcase_enabled); + let showcase_asset = find_showcase_asset_for_asset(ctx, row.asset_id.as_str()); EditorAssetSnapshot { asset_id: row.asset_id, @@ -1764,6 +2622,15 @@ fn asset_snapshot_from_row(ctx: &ReducerContext, row: EditorAsset) -> EditorAsse created_at_micros: row.created_at.to_micros_since_unix_epoch(), updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), thumbnail_src: row.thumbnail_src, + generation_cost_mud_points: row.generation_cost_mud_points, + showcase_id: showcase_asset + .as_ref() + .map(|asset| asset.showcase_id.clone()), + showcase_review_status: showcase_asset + .as_ref() + .map(|asset| asset.review_status.clone()), + showcase_display_enabled: showcase_asset.as_ref().map(|asset| asset.display_enabled), + showcase_like_count: showcase_asset.as_ref().map(|asset| asset.like_count), } } @@ -1850,45 +2717,6 @@ fn project_resource_matches_media( matching_media_ref(resource.image_src.as_str(), image_src) } -fn project_resource_copies_source_media( - ctx: &ReducerContext, - resource: &EditorProjectResource, -) -> bool { - project_resource_input_copies_source_media( - ctx, - resource.owner_user_id.as_str(), - resource.source_resource_id.as_ref(), - resource.asset_object_id.as_ref(), - resource.object_key.as_ref(), - resource.image_src.as_str(), - ) -} - -fn project_resource_input_copies_source_media( - ctx: &ReducerContext, - owner_user_id: &str, - source_resource_id: Option<&String>, - asset_object_id: Option<&String>, - object_key: Option<&String>, - image_src: &str, -) -> bool { - let Some(source_resource_id) = source_resource_id - .map(String::as_str) - .map(str::trim) - .filter(|value| !value.is_empty()) - else { - return false; - }; - ctx.db - .editor_project_resource() - .resource_id() - .find(&source_resource_id.to_string()) - .filter(|source_resource| source_resource.owner_user_id == owner_user_id) - .is_some_and(|source_resource| { - project_resource_matches_media(&source_resource, asset_object_id, object_key, image_src) - }) -} - fn find_reusable_project_resource_for_input( ctx: &ReducerContext, project_id: &str, @@ -2386,6 +3214,62 @@ fn editor_asset_error(message: String) -> EditorAssetProcedureResult { } } +fn editor_showcase_asset_ok( + asset: Option, +) -> EditorShowcaseAssetProcedureResult { + EditorShowcaseAssetProcedureResult { + ok: true, + asset, + error_message: None, + } +} + +fn editor_showcase_asset_error(message: String) -> EditorShowcaseAssetProcedureResult { + EditorShowcaseAssetProcedureResult { + ok: false, + asset: None, + error_message: Some(message), + } +} + +fn editor_showcase_asset_list_ok( + assets: Vec, +) -> EditorShowcaseAssetListProcedureResult { + EditorShowcaseAssetListProcedureResult { + ok: true, + assets, + error_message: None, + } +} + +fn editor_showcase_asset_list_error(message: String) -> EditorShowcaseAssetListProcedureResult { + EditorShowcaseAssetListProcedureResult { + ok: false, + assets: Vec::new(), + error_message: Some(message), + } +} + +fn editor_showcase_campaign_config_ok( + config: Option, +) -> EditorShowcaseCampaignConfigProcedureResult { + EditorShowcaseCampaignConfigProcedureResult { + ok: true, + config, + error_message: None, + } +} + +fn editor_showcase_campaign_config_error( + message: String, +) -> EditorShowcaseCampaignConfigProcedureResult { + EditorShowcaseCampaignConfigProcedureResult { + ok: false, + config: None, + error_message: Some(message), + } +} + #[cfg(test)] mod tests { use super::*; @@ -2520,10 +3404,30 @@ mod tests { fn public_showcase_cursor_filters_strictly_after_desc_position() { let cursor = parse_public_showcase_cursor(Some("2000:resource-b")); - assert!(is_after_public_showcase_cursor(1999, "resource-z", cursor.as_ref())); - assert!(is_after_public_showcase_cursor(2000, "resource-a", cursor.as_ref())); - assert!(!is_after_public_showcase_cursor(2000, "resource-b", cursor.as_ref())); - assert!(!is_after_public_showcase_cursor(2000, "resource-c", cursor.as_ref())); - assert!(!is_after_public_showcase_cursor(2001, "resource-a", cursor.as_ref())); + assert!(is_after_public_showcase_cursor( + 1999, + "resource-z", + cursor.as_ref() + )); + assert!(is_after_public_showcase_cursor( + 2000, + "resource-a", + cursor.as_ref() + )); + assert!(!is_after_public_showcase_cursor( + 2000, + "resource-b", + cursor.as_ref() + )); + assert!(!is_after_public_showcase_cursor( + 2000, + "resource-c", + cursor.as_ref() + )); + assert!(!is_after_public_showcase_cursor( + 2001, + "resource-a", + cursor.as_ref() + )); } } diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index a05a54030..340b984cf 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -237,6 +237,9 @@ macro_rules! migration_tables { editor_project_resource, editor_asset_folder, editor_asset, + editor_showcase_asset, + editor_showcase_asset_like, + editor_showcase_campaign_config, puzzle_agent_session, puzzle_background_compile_task, puzzle_agent_message, diff --git a/src/components/creation-home/CreationLandingView.test.tsx b/src/components/creation-home/CreationLandingView.test.tsx index b88f331fc..ebfb6830a 100644 --- a/src/components/creation-home/CreationLandingView.test.tsx +++ b/src/components/creation-home/CreationLandingView.test.tsx @@ -11,6 +11,7 @@ import { CreationLandingView } from './CreationLandingView'; const listEditorProjectsMock = vi.hoisted(() => vi.fn()); const listPublicEditorProjectResourcesMock = vi.hoisted(() => vi.fn()); const createEditorProjectMock = vi.hoisted(() => vi.fn()); +const toggleEditorShowcaseAssetLikeMock = vi.hoisted(() => vi.fn()); type AuthValue = NonNullable>; @@ -48,6 +49,7 @@ vi.mock('../../services/image-editor/editorProjectClient', () => ({ listEditorProjects: listEditorProjectsMock, listPublicEditorProjectResources: listPublicEditorProjectResourcesMock, createEditorProject: createEditorProjectMock, + toggleEditorShowcaseAssetLike: toggleEditorShowcaseAssetLikeMock, })); const projectItems = [ @@ -146,6 +148,7 @@ describe('CreationLandingView', () => { listEditorProjectsMock.mockReset(); listPublicEditorProjectResourcesMock.mockReset(); createEditorProjectMock.mockReset(); + toggleEditorShowcaseAssetLikeMock.mockReset(); vi.unstubAllGlobals(); }); diff --git a/src/components/creation-home/CreationLandingView.tsx b/src/components/creation-home/CreationLandingView.tsx index b11d5b179..67014b3d4 100644 --- a/src/components/creation-home/CreationLandingView.tsx +++ b/src/components/creation-home/CreationLandingView.tsx @@ -1,4 +1,4 @@ -import { Film, Image as ImageIcon, Music2 } from 'lucide-react'; +import { Film, Heart, Image as ImageIcon, Music2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useResolvedAssetReadUrl } from '../../hooks/useResolvedAssetReadUrl'; @@ -8,8 +8,10 @@ import { type EditorProjectResourceListPage, type EditorProjectResourceSnapshot, type EditorProjectSnapshot, + type EditorShowcaseCampaignSnapshot, listEditorProjects, listPublicEditorProjectResources, + toggleEditorShowcaseAssetLike, } from '../../services/image-editor/editorProjectClient'; import { useAuthUi } from '../auth/AuthUiContext'; import { PlatformActionButton } from '../common/PlatformActionButton'; @@ -124,7 +126,7 @@ function normalizeShowcaseResourcePage( page: EditorProjectResourceListPage | EditorProjectResourceSnapshot[], ): EditorProjectResourceListPage { return Array.isArray(page) - ? { resources: page, nextCursor: null } + ? { resources: page, nextCursor: null, campaign: null } : page; } @@ -310,12 +312,18 @@ function ShowcaseBundlePreview({ function CreationShowcaseModal({ item, activeIndex, + isLiked, + isLikePending, onSelectIndex, + onToggleLike, onClose, }: { item: ShowcaseAssetItem | null; activeIndex: number; + isLiked: boolean; + isLikePending: boolean; onSelectIndex: (index: number) => void; + onToggleLike: (item: ShowcaseAssetItem) => void; onClose: () => void; }) { const safeActiveIndex = item @@ -364,7 +372,29 @@ function CreationShowcaseModal({
生成总成本
{item.cost}
+ {typeof item.likeCount === 'number' ? ( +
+
+
{item.likeCount}
+
+ ) : null} + {item.showcaseId ? ( + onToggleLike(item)} + > + + ) : null}
([]); + const [showcaseCampaign, setShowcaseCampaign] = + useState(null); const [showcaseNextCursor, setShowcaseNextCursor] = useState(null); const [assetError, setAssetError] = useState(null); const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState(null); @@ -427,6 +459,12 @@ export function CreationLandingView({ const [selectedShowcaseItem, setSelectedShowcaseItem] = useState(null); const [selectedShowcaseIndex, setSelectedShowcaseIndex] = useState(0); + const [likedShowcaseIds, setLikedShowcaseIds] = useState>( + () => new Set(), + ); + const [pendingLikeShowcaseIds, setPendingLikeShowcaseIds] = useState< + Set + >(() => new Set()); const [isPendingFeatureDialogOpen, setIsPendingFeatureDialogOpen] = useState(false); const showcaseLoadMoreRef = useRef(null); @@ -479,10 +517,12 @@ export function CreationLandingView({ .then((page) => { const normalizedPage = normalizeShowcaseResourcePage(page); setProjectShowcaseResources(normalizedPage.resources); + setShowcaseCampaign(normalizedPage.campaign ?? null); setShowcaseNextCursor(normalizedPage.nextCursor); }) .catch((error: unknown) => { setProjectShowcaseResources([]); + setShowcaseCampaign(null); setShowcaseNextCursor(null); setShowcaseLoadMoreError(null); setAssetError( @@ -516,6 +556,7 @@ export function CreationLandingView({ setProjectShowcaseResources((current) => mergeShowcaseResources(current, normalizedPage.resources), ); + setShowcaseCampaign((current) => normalizedPage.campaign ?? current); setShowcaseNextCursor(normalizedPage.nextCursor); showcaseFailedLoadMoreCursorRef.current = null; }) @@ -597,14 +638,93 @@ export function CreationLandingView({ [createProject], ); + const updateShowcaseLikeCount = useCallback( + (showcaseId: string, likeCount: number) => { + setProjectShowcaseResources((current) => + current.map((resource) => + resource.showcaseId === showcaseId || resource.resourceId === showcaseId + ? { ...resource, likeCount } + : resource, + ), + ); + setSelectedShowcaseItem((current) => + current?.showcaseId === showcaseId ? { ...current, likeCount } : current, + ); + }, + [], + ); + + const handleToggleShowcaseLike = useCallback( + (item: ShowcaseAssetItem) => { + const showcaseId = item.showcaseId?.trim(); + if (!showcaseId) { + return; + } + if (!authUi?.user) { + authUi?.openLoginModal(); + return; + } + if (pendingLikeShowcaseIds.has(showcaseId)) { + return; + } + + const nextLiked = !likedShowcaseIds.has(showcaseId); + const previousLikeCount = item.likeCount ?? 0; + const optimisticLikeCount = Math.max( + 0, + previousLikeCount + (nextLiked ? 1 : -1), + ); + setPendingLikeShowcaseIds((current) => new Set(current).add(showcaseId)); + setLikedShowcaseIds((current) => { + const next = new Set(current); + if (nextLiked) { + next.add(showcaseId); + } else { + next.delete(showcaseId); + } + return next; + }); + updateShowcaseLikeCount(showcaseId, optimisticLikeCount); + + toggleEditorShowcaseAssetLike(showcaseId, nextLiked) + .then((record) => { + updateShowcaseLikeCount( + showcaseId, + record.likeCount ?? optimisticLikeCount, + ); + }) + .catch(() => { + setLikedShowcaseIds((current) => { + const next = new Set(current); + if (nextLiked) { + next.delete(showcaseId); + } else { + next.add(showcaseId); + } + return next; + }); + updateShowcaseLikeCount(showcaseId, previousLikeCount); + }) + .finally(() => { + setPendingLikeShowcaseIds((current) => { + const next = new Set(current); + next.delete(showcaseId); + return next; + }); + }); + }, + [authUi, likedShowcaseIds, pendingLikeShowcaseIds, updateShowcaseLikeCount], + ); + const showcaseItems = useMemo( () => buildCreationShowcaseItems({ activeTab: activeShowcaseTab, projectResources: projectShowcaseResources, + campaign: showcaseCampaign, fallbackAuthorName: null, }), - [activeShowcaseTab, projectShowcaseResources], + [activeShowcaseTab, projectShowcaseResources, showcaseCampaign], ); const isShowcaseLoading = isLoadingShowcase; @@ -683,6 +803,12 @@ export function CreationLandingView({
生成总成本
{item.cost}
+ {typeof item.likeCount === 'number' ? ( +
+
+
{item.likeCount}
+
+ ) : null} @@ -888,7 +1014,16 @@ export function CreationLandingView({ setSelectedShowcaseItem(null)} /> { expect(items[0]?.label).toBe('画布生成素材'); }); - it('filters out generated resources that are not public showcase enabled', () => { + it('filters out generated showcase records with display disabled', () => { const items = buildCreationShowcaseItems({ activeTab: 'packs', projectResources: [ @@ -76,6 +76,47 @@ describe('creationShowcaseModel', () => { expect(items[0]?.label).toBe('公开素材'); }); + it('prepends enabled campaign cards and keeps showcase metrics', () => { + const items = buildCreationShowcaseItems({ + activeTab: 'packs', + campaign: { + enabled: true, + title: '活动精选', + imageSrc: '/campaign.png', + prompt: '活动提示词', + author: '运营', + costText: '50泥点', + }, + projectResources: [ + createResource({ + resourceId: 'showcase-1', + showcaseId: 'showcase-1', + assetId: 'asset-1', + label: '审核通过素材', + sourceType: 'generated', + publicShowcaseEnabled: true, + likeCount: 12, + generationCostMudPoints: 20, + }), + ], + }); + + expect(items).toHaveLength(2); + expect(items[0]).toMatchObject({ + id: 'campaign:global', + label: '活动精选', + campaign: true, + cost: '50泥点', + }); + expect(items[1]).toMatchObject({ + id: 'standalone:asset-1', + label: '审核通过素材', + likeCount: 12, + showcaseId: 'showcase-1', + cost: '20泥点', + }); + }); + it('uses public author fields without falling back to internal owner user id', () => { const items = buildCreationShowcaseItems({ activeTab: 'characters', diff --git a/src/components/creation-home/creationShowcaseModel.ts b/src/components/creation-home/creationShowcaseModel.ts index b27ec0773..ae94e12b3 100644 --- a/src/components/creation-home/creationShowcaseModel.ts +++ b/src/components/creation-home/creationShowcaseModel.ts @@ -2,6 +2,7 @@ import type { EditorAssetGenerationInputReference, EditorAssetSnapshot, EditorProjectResourceSnapshot, + EditorShowcaseCampaignSnapshot, } from '../../services/image-editor/editorProjectClient'; import { calculateCharacterAnimationPrice, @@ -42,6 +43,9 @@ export type ShowcaseAssetItem = { prompt: string; author: string; cost: string; + likeCount?: number | null; + showcaseId?: string | null; + campaign?: boolean; }; type ShowcaseAssetGroup = { @@ -175,7 +179,7 @@ function projectResourceToShowcaseAsset( return { ...record, - assetId: resource.resourceId, + assetId: resource.assetId?.trim() || resource.showcaseId?.trim() || resource.resourceId, folderId: `project:${resource.projectId}`, label: fallbackLabel, imageSrc: resource.imageSrc, @@ -709,6 +713,9 @@ function toLibraryShowcaseItem( return null; } const primaryAsset = sortedAssets[0]; + if (!primaryAsset) { + return null; + } const label = group.label?.trim() || primaryAsset?.label?.trim() || @@ -720,16 +727,44 @@ function toLibraryShowcaseItem( prompt: resolveGroupPrompt(sortedAssets), author: resolveGroupAuthor(sortedAssets, fallbackAuthorName), cost: resolveGroupCost(sortedAssets), + likeCount: normalizeNumber(assetRecord(primaryAsset).likeCount), + showcaseId: normalizeText(assetRecord(primaryAsset).showcaseId), + }; +} + +function campaignToShowcaseItem( + campaign?: EditorShowcaseCampaignSnapshot | null, +): ShowcaseAssetItem | null { + if (!campaign?.enabled || !campaign.imageSrc.trim()) { + return null; + } + return { + id: 'campaign:global', + label: campaign.title.trim() || '陶泥儿精选', + previews: [ + { + id: 'campaign:global:image', + label: campaign.title.trim() || '陶泥儿精选', + src: campaign.imageSrc, + mediaType: 'image', + }, + ], + prompt: campaign.prompt.trim() || UNKNOWN_META_VALUE, + author: campaign.author.trim() || UNKNOWN_META_VALUE, + cost: campaign.costText.trim() || UNKNOWN_META_VALUE, + campaign: true, }; } export function buildCreationShowcaseItems({ activeTab, projectResources, + campaign, fallbackAuthorName, }: { activeTab: ShowcaseTabId; projectResources: EditorProjectResourceSnapshot[]; + campaign?: EditorShowcaseCampaignSnapshot | null; fallbackAuthorName?: string | null; }) { const projectItems = getGroupsForTab( @@ -739,5 +774,6 @@ export function buildCreationShowcaseItems({ .map((group) => toLibraryShowcaseItem(group, fallbackAuthorName)) .filter((item): item is ShowcaseAssetItem => item !== null); - return projectItems; + const campaignItem = campaignToShowcaseItem(campaign); + return campaignItem ? [campaignItem, ...projectItems] : projectItems; } diff --git a/src/components/image-editor/ImageCanvasAssetFolderSectionView.tsx b/src/components/image-editor/ImageCanvasAssetFolderSectionView.tsx index 5d575ae4f..595cdce12 100644 --- a/src/components/image-editor/ImageCanvasAssetFolderSectionView.tsx +++ b/src/components/image-editor/ImageCanvasAssetFolderSectionView.tsx @@ -60,10 +60,7 @@ type ImageCanvasAssetFolderSectionViewProps = { startRenamingAsset: (asset: EditorAsset) => void; commitAssetRename: (asset: EditorAsset) => void; deleteUploadedAsset: (asset: EditorAsset) => void; - setAssetPublicShowcaseEnabled: ( - asset: EditorAsset, - publicShowcaseEnabled: boolean, - ) => void; + submitAssetToShowcase: (asset: EditorAsset) => void; toggleAssetSelected: (assetId: string) => void; addAssetLayer: (asset: EditorAsset) => void; onDownloadAsset: (asset: EditorAsset) => void; @@ -95,7 +92,7 @@ export function ImageCanvasAssetFolderSectionView({ startRenamingAsset, commitAssetRename, deleteUploadedAsset, - setAssetPublicShowcaseEnabled, + submitAssetToShowcase, toggleAssetSelected, addAssetLayer, onDownloadAsset, @@ -255,7 +252,7 @@ export function ImageCanvasAssetFolderSectionView({ startRenamingAsset={startRenamingAsset} commitAssetRename={commitAssetRename} deleteUploadedAsset={deleteUploadedAsset} - setAssetPublicShowcaseEnabled={setAssetPublicShowcaseEnabled} + submitAssetToShowcase={submitAssetToShowcase} toggleAssetSelected={toggleAssetSelected} addAssetLayer={addAssetLayer} onDownloadAsset={onDownloadAsset} diff --git a/src/components/image-editor/ImageCanvasAssetLibraryPanelView.tsx b/src/components/image-editor/ImageCanvasAssetLibraryPanelView.tsx index ffdfaa990..0209b88d9 100644 --- a/src/components/image-editor/ImageCanvasAssetLibraryPanelView.tsx +++ b/src/components/image-editor/ImageCanvasAssetLibraryPanelView.tsx @@ -90,10 +90,7 @@ export type ImageCanvasAssetLibraryPanelViewProps = { startRenamingAsset: (asset: EditorAsset) => void; commitAssetRename: (asset: EditorAsset) => void; deleteUploadedAsset: (asset: EditorAsset) => void; - setAssetPublicShowcaseEnabled: ( - asset: EditorAsset, - publicShowcaseEnabled: boolean, - ) => void; + submitAssetToShowcase: (asset: EditorAsset) => void; toggleAssetSelected: (assetId: string) => void; addAssetLayer: (asset: EditorAsset) => void; onDownloadAsset: (asset: EditorAsset) => void; @@ -141,7 +138,7 @@ export function ImageCanvasAssetLibraryPanelView({ startRenamingAsset, commitAssetRename, deleteUploadedAsset, - setAssetPublicShowcaseEnabled, + submitAssetToShowcase, toggleAssetSelected, addAssetLayer, onDownloadAsset, @@ -233,7 +230,7 @@ export function ImageCanvasAssetLibraryPanelView({ startRenamingAsset={startRenamingAsset} commitAssetRename={commitAssetRename} deleteUploadedAsset={deleteUploadedAsset} - setAssetPublicShowcaseEnabled={setAssetPublicShowcaseEnabled} + submitAssetToShowcase={submitAssetToShowcase} toggleAssetSelected={toggleAssetSelected} addAssetLayer={addAssetLayer} onDownloadAsset={onDownloadAsset} diff --git a/src/components/image-editor/ImageCanvasAssetRowView.test.tsx b/src/components/image-editor/ImageCanvasAssetRowView.test.tsx index beaad4df0..61bb764a1 100644 --- a/src/components/image-editor/ImageCanvasAssetRowView.test.tsx +++ b/src/components/image-editor/ImageCanvasAssetRowView.test.tsx @@ -24,18 +24,23 @@ vi.mock('../common/PlatformMediaFrame', () => ({ objectKey?: string | null; alt: string; previewOverlay?: ReactNode; - }) => ( -
- {src?.trim() ? ( + }) => { + const imageSrc = + src?.trim() || + (objectKey?.trim() ? `https://signed.example.com/${objectKey.trim()}` : ''); + return ( +
+ {imageSrc ? ( {alt} ) : null} {previewOverlay}
- ), + ); + }, })); beforeEach(() => { @@ -75,7 +80,7 @@ function renderAssetRow({ toggleAssetSelected = vi.fn(), commitAssetRename = vi.fn(), deleteUploadedAsset = vi.fn(), - setAssetPublicShowcaseEnabled = vi.fn(), + submitAssetToShowcase = vi.fn(), onDownloadAsset = vi.fn(), }: { asset?: EditorAsset; @@ -87,10 +92,7 @@ function renderAssetRow({ toggleAssetSelected?: (assetId: string) => void; commitAssetRename?: (asset: EditorAsset) => void; deleteUploadedAsset?: (asset: EditorAsset) => void; - setAssetPublicShowcaseEnabled?: ( - asset: EditorAsset, - publicShowcaseEnabled: boolean, - ) => void; + submitAssetToShowcase?: (asset: EditorAsset) => void; onDownloadAsset?: (asset: EditorAsset) => void; } = {}) { const props = { @@ -110,7 +112,7 @@ function renderAssetRow({ startRenamingAsset: vi.fn(), commitAssetRename, deleteUploadedAsset, - setAssetPublicShowcaseEnabled, + submitAssetToShowcase, toggleAssetSelected, addAssetLayer, onDownloadAsset, @@ -280,17 +282,33 @@ describe('ImageCanvasAssetRowView', () => { ); }); + it('renders image thumbnails when the persisted asset only has objectKey', () => { + renderAssetRow({ + asset: createAsset({ + label: '规范图', + src: '', + objectKey: 'generated-character-drafts/editor/spec.png', + mediaType: 'image', + }), + }); + + const image = screen.getByRole('img', { name: '素材:规范图' }); + + expect(image.getAttribute('src')).toBe( + 'https://signed.example.com/generated-character-drafts/editor/spec.png', + ); + }); + it('opens asset actions from the right click menu', () => { const asset = createAsset({ - sourceResourceId: 'resource-asset-1', - publicShowcaseEnabled: true, + sourceType: 'generated', }); const deleteUploadedAsset = vi.fn(); - const setAssetPublicShowcaseEnabled = vi.fn(); + const submitAssetToShowcase = vi.fn(); renderAssetRow({ asset, deleteUploadedAsset, - setAssetPublicShowcaseEnabled, + submitAssetToShowcase, }); expect( @@ -302,13 +320,12 @@ describe('ImageCanvasAssetRowView', () => { clientY: 80, }); - const publicMenuItem = screen.getByRole('menuitemcheckbox', { - name: /公开展示该素材/u, + const submitMenuItem = screen.getByRole('menuitem', { + name: /提交精选审核/u, }); - expect(publicMenuItem.getAttribute('aria-checked')).toBe('true'); - fireEvent.click(publicMenuItem); - expect(setAssetPublicShowcaseEnabled).toHaveBeenCalledWith(asset, false); + fireEvent.click(submitMenuItem); + expect(submitAssetToShowcase).toHaveBeenCalledWith(asset); fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), { clientX: 120, @@ -318,45 +335,42 @@ describe('ImageCanvasAssetRowView', () => { expect(deleteUploadedAsset).toHaveBeenCalledWith(asset); }); - it('disables public showcase toggle when the asset is not linked to a project resource', () => { - const setAssetPublicShowcaseEnabled = vi.fn(); - renderAssetRow({ setAssetPublicShowcaseEnabled }); + it('disables showcase submit when the asset is not generated', () => { + const submitAssetToShowcase = vi.fn(); + renderAssetRow({ submitAssetToShowcase }); fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), { clientX: 120, clientY: 80, }); - const publicMenuItem = screen.getByRole('menuitemcheckbox', { - name: /公开展示该素材/u, + const submitMenuItem = screen.getByRole('menuitem', { + name: /提交精选审核/u, }); - expect((publicMenuItem as HTMLButtonElement).disabled).toBe(true); + expect((submitMenuItem as HTMLButtonElement).disabled).toBe(true); - fireEvent.click(publicMenuItem); - expect(setAssetPublicShowcaseEnabled).not.toHaveBeenCalled(); + fireEvent.click(submitMenuItem); + expect(submitAssetToShowcase).not.toHaveBeenCalled(); }); - it('keeps public showcase toggle enabled for legacy generated assets once a source resource is inferred', () => { + it('enables showcase submit for persisted generated assets', () => { const asset = createAsset({ sourceType: 'generated', - sourceResourceId: 'resource-inferred-from-media', - publicShowcaseEnabled: null, }); - const setAssetPublicShowcaseEnabled = vi.fn(); - renderAssetRow({ asset, setAssetPublicShowcaseEnabled }); + const submitAssetToShowcase = vi.fn(); + renderAssetRow({ asset, submitAssetToShowcase }); fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), { clientX: 120, clientY: 80, }); - const publicMenuItem = screen.getByRole('menuitemcheckbox', { - name: /公开展示该素材/u, + const submitMenuItem = screen.getByRole('menuitem', { + name: /提交精选审核/u, }); - expect((publicMenuItem as HTMLButtonElement).disabled).toBe(false); - expect(publicMenuItem.getAttribute('aria-checked')).toBe('true'); + expect((submitMenuItem as HTMLButtonElement).disabled).toBe(false); - fireEvent.click(publicMenuItem); - expect(setAssetPublicShowcaseEnabled).toHaveBeenCalledWith(asset, false); + fireEvent.click(submitMenuItem); + expect(submitAssetToShowcase).toHaveBeenCalledWith(asset); }); }); diff --git a/src/components/image-editor/ImageCanvasAssetRowView.tsx b/src/components/image-editor/ImageCanvasAssetRowView.tsx index 6782e3496..393500139 100644 --- a/src/components/image-editor/ImageCanvasAssetRowView.tsx +++ b/src/components/image-editor/ImageCanvasAssetRowView.tsx @@ -43,10 +43,7 @@ export type ImageCanvasAssetRowViewProps = { startRenamingAsset: (asset: EditorAsset) => void; commitAssetRename: (asset: EditorAsset) => void; deleteUploadedAsset: (asset: EditorAsset) => void; - setAssetPublicShowcaseEnabled: ( - asset: EditorAsset, - publicShowcaseEnabled: boolean, - ) => void; + submitAssetToShowcase: (asset: EditorAsset) => void; toggleAssetSelected: (assetId: string) => void; addAssetLayer: (asset: EditorAsset) => void; onDownloadAsset: (asset: EditorAsset) => void; @@ -69,7 +66,7 @@ export function ImageCanvasAssetRowView({ startRenamingAsset, commitAssetRename, deleteUploadedAsset, - setAssetPublicShowcaseEnabled, + submitAssetToShowcase, toggleAssetSelected, addAssetLayer, onDownloadAsset, @@ -82,8 +79,19 @@ export function ImageCanvasAssetRowView({ const isRenaming = renamingAsset?.assetId === asset.id; const isUploadingAsset = asset.uploadStatus === 'uploading'; const isFailedUpload = asset.uploadStatus === 'failed'; - const canShowcaseAsset = Boolean(asset.sourceResourceId); - const publicShowcaseEnabled = asset.publicShowcaseEnabled ?? true; + const showcaseReviewStatus = asset.showcaseReviewStatus ?? null; + const canSubmitShowcaseAsset = + asset.sourceType === 'generated' && asset.persisted && !showcaseReviewStatus; + const showcaseMenuLabel = + showcaseReviewStatus === 'pending' + ? '精选审核中' + : showcaseReviewStatus === 'approved' + ? asset.showcaseDisplayEnabled === false + ? '已入选精选,暂未展示' + : '已入选精选' + : showcaseReviewStatus === 'rejected' + ? '精选未通过' + : '提交精选审核'; const closeMenu = useCallback(() => { setMenuPosition(null); @@ -444,22 +452,21 @@ export function ImageCanvasAssetRowView({ >