merge: admin work visibility controls
This commit is contained in:
@@ -13,10 +13,13 @@ import type {
|
|||||||
AdminOverviewResponse,
|
AdminOverviewResponse,
|
||||||
AdminTrackingEventListQuery,
|
AdminTrackingEventListQuery,
|
||||||
AdminTrackingEventListResponse,
|
AdminTrackingEventListResponse,
|
||||||
|
AdminUpdateWorkVisibilityRequest,
|
||||||
|
AdminUpdateWorkVisibilityResponse,
|
||||||
AdminUpsertProfileInviteCodeRequest,
|
AdminUpsertProfileInviteCodeRequest,
|
||||||
AdminUpsertProfileRechargeProductRequest,
|
AdminUpsertProfileRechargeProductRequest,
|
||||||
AdminUpsertProfileRedeemCodeRequest,
|
AdminUpsertProfileRedeemCodeRequest,
|
||||||
AdminUpsertProfileTaskConfigRequest,
|
AdminUpsertProfileTaskConfigRequest,
|
||||||
|
AdminWorkVisibilityListResponse,
|
||||||
ApiErrorEnvelope,
|
ApiErrorEnvelope,
|
||||||
ApiMeta,
|
ApiMeta,
|
||||||
ApiSuccessEnvelope,
|
ApiSuccessEnvelope,
|
||||||
@@ -194,6 +197,27 @@ export function upsertAdminCreationEntryConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function listAdminWorkVisibility(token: string) {
|
||||||
|
return request<AdminWorkVisibilityListResponse>(
|
||||||
|
'/admin/api/works/visibility',
|
||||||
|
{token},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAdminWorkVisibility(
|
||||||
|
token: string,
|
||||||
|
payload: AdminUpdateWorkVisibilityRequest,
|
||||||
|
) {
|
||||||
|
return request<AdminUpdateWorkVisibilityResponse>(
|
||||||
|
'/admin/api/works/visibility',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
token,
|
||||||
|
body: payload,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function listProfileRedeemCodes(token: string) {
|
export function listProfileRedeemCodes(token: string) {
|
||||||
return request<ProfileRedeemCodeAdminListResponse>(
|
return request<ProfileRedeemCodeAdminListResponse>(
|
||||||
'/admin/api/profile/redeem-codes',
|
'/admin/api/profile/redeem-codes',
|
||||||
|
|||||||
@@ -177,6 +177,36 @@ export interface AdminUpsertCreationEntryTypeConfigRequest {
|
|||||||
categorySortOrder: number;
|
categorySortOrder: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminWorkVisibilityEntryPayload {
|
||||||
|
sourceType: string;
|
||||||
|
workId: string;
|
||||||
|
profileId: string;
|
||||||
|
sourceSessionId?: string | null;
|
||||||
|
publicWorkCode: string;
|
||||||
|
ownerUserId: string;
|
||||||
|
authorDisplayName: string;
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
coverImageSrc?: string | null;
|
||||||
|
visible: boolean;
|
||||||
|
publishedAtMicros?: number | null;
|
||||||
|
updatedAtMicros: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminWorkVisibilityListResponse {
|
||||||
|
entries: AdminWorkVisibilityEntryPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUpdateWorkVisibilityRequest {
|
||||||
|
sourceType: string;
|
||||||
|
profileId: string;
|
||||||
|
visible: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUpdateWorkVisibilityResponse {
|
||||||
|
entry: AdminWorkVisibilityEntryPayload;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminUpsertProfileRedeemCodeRequest {
|
export interface AdminUpsertProfileRedeemCodeRequest {
|
||||||
code: string;
|
code: string;
|
||||||
mode: ProfileRedeemCodeMode;
|
mode: ProfileRedeemCodeMode;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage';
|
|||||||
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
|
import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage';
|
||||||
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
|
import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage';
|
||||||
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
|
import {AdminTrackingEventsPage} from '../pages/AdminTrackingEventsPage';
|
||||||
|
import {AdminWorkVisibilityPage} from '../pages/AdminWorkVisibilityPage';
|
||||||
import {AdminShell} from './AdminShell';
|
import {AdminShell} from './AdminShell';
|
||||||
import type {AdminRouteId} from './adminRoutes';
|
import type {AdminRouteId} from './adminRoutes';
|
||||||
import {resolveAdminRoute, routeHash} from './adminRoutes';
|
import {resolveAdminRoute, routeHash} from './adminRoutes';
|
||||||
@@ -205,6 +206,12 @@ export function AdminApp() {
|
|||||||
onUnauthorized={handleUnauthorized}
|
onUnauthorized={handleUnauthorized}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{routeId === 'work-visibility' ? (
|
||||||
|
<AdminWorkVisibilityPage
|
||||||
|
token={token}
|
||||||
|
onUnauthorized={handleUnauthorized}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
{routeId === 'tasks' ? (
|
{routeId === 'tasks' ? (
|
||||||
<AdminTaskConfigPage
|
<AdminTaskConfigPage
|
||||||
result={taskConfigResult}
|
result={taskConfigResult}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
BadgeDollarSign,
|
BadgeDollarSign,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LogOut,
|
LogOut,
|
||||||
|
Eye,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
@@ -35,6 +36,7 @@ const routeIcons = {
|
|||||||
tasks: ListChecks,
|
tasks: ListChecks,
|
||||||
'recharge-products': BadgeDollarSign,
|
'recharge-products': BadgeDollarSign,
|
||||||
'creation-entry': SlidersHorizontal,
|
'creation-entry': SlidersHorizontal,
|
||||||
|
'work-visibility': Eye,
|
||||||
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
} satisfies Record<AdminRouteId, typeof LayoutDashboard>;
|
||||||
|
|
||||||
export function AdminShell({
|
export function AdminShell({
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export type AdminRouteId =
|
|||||||
| 'invite'
|
| 'invite'
|
||||||
| 'tasks'
|
| 'tasks'
|
||||||
| 'recharge-products'
|
| 'recharge-products'
|
||||||
| 'creation-entry';
|
| 'creation-entry'
|
||||||
|
| 'work-visibility';
|
||||||
|
|
||||||
export interface AdminRouteDefinition {
|
export interface AdminRouteDefinition {
|
||||||
id: AdminRouteId;
|
id: AdminRouteId;
|
||||||
@@ -25,6 +26,7 @@ export const adminRoutes: AdminRouteDefinition[] = [
|
|||||||
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
{id: 'tasks', label: '任务配置', hash: '#tasks'},
|
||||||
{id: 'recharge-products', label: '充值商品', hash: '#recharge-products'},
|
{id: 'recharge-products', label: '充值商品', hash: '#recharge-products'},
|
||||||
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
{id: 'creation-entry', label: '入口开关', hash: '#creation-entry'},
|
||||||
|
{id: 'work-visibility', label: '作品可见性', hash: '#work-visibility'},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function resolveAdminRoute(hash: string): AdminRouteId {
|
export function resolveAdminRoute(hash: string): AdminRouteId {
|
||||||
|
|||||||
269
apps/admin-web/src/pages/AdminWorkVisibilityPage.tsx
Normal file
269
apps/admin-web/src/pages/AdminWorkVisibilityPage.tsx
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import {Eye, EyeOff, RefreshCcw} from 'lucide-react';
|
||||||
|
import {useEffect, useMemo, useState} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
listAdminWorkVisibility,
|
||||||
|
updateAdminWorkVisibility,
|
||||||
|
} from '../api/adminApiClient';
|
||||||
|
import type {AdminWorkVisibilityEntryPayload} from '../api/adminApiTypes';
|
||||||
|
import {useAdminWriteConfirm} from '../components/useAdminWriteConfirm';
|
||||||
|
import {handlePageError} from './pageUtils';
|
||||||
|
|
||||||
|
interface AdminWorkVisibilityPageProps {
|
||||||
|
token: string;
|
||||||
|
onUnauthorized: (message?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceLabels: Record<string, string> = {
|
||||||
|
puzzle: '拼图',
|
||||||
|
'custom-world': '自定义世界',
|
||||||
|
'jump-hop': '跳一跳',
|
||||||
|
'wooden-fish': '敲木鱼',
|
||||||
|
match3d: '抓大鹅',
|
||||||
|
'square-hole': '方洞挑战',
|
||||||
|
'visual-novel': '视觉小说',
|
||||||
|
'big-fish': '大鱼吃小鱼',
|
||||||
|
'bark-battle': '汪汪声浪',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function AdminWorkVisibilityPage({
|
||||||
|
token,
|
||||||
|
onUnauthorized,
|
||||||
|
}: AdminWorkVisibilityPageProps) {
|
||||||
|
const [entries, setEntries] = useState<AdminWorkVisibilityEntryPayload[]>([]);
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [savingKey, setSavingKey] = useState('');
|
||||||
|
const [errorMessage, setErrorMessage] = useState('');
|
||||||
|
const {confirmWrite, confirmDialog} = useAdminWriteConfirm();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void refreshEntries();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
const filteredEntries = useMemo(() => {
|
||||||
|
const normalizedKeyword = keyword.trim().toLowerCase();
|
||||||
|
if (!normalizedKeyword) {
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
return entries.filter((entry) =>
|
||||||
|
[
|
||||||
|
entry.sourceType,
|
||||||
|
sourceLabels[entry.sourceType] ?? '',
|
||||||
|
entry.title,
|
||||||
|
entry.subtitle,
|
||||||
|
entry.authorDisplayName,
|
||||||
|
entry.publicWorkCode,
|
||||||
|
entry.profileId,
|
||||||
|
entry.workId,
|
||||||
|
]
|
||||||
|
.join(' ')
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(normalizedKeyword),
|
||||||
|
);
|
||||||
|
}, [entries, keyword]);
|
||||||
|
|
||||||
|
async function refreshEntries() {
|
||||||
|
setIsLoading(true);
|
||||||
|
setErrorMessage('');
|
||||||
|
try {
|
||||||
|
const response = await listAdminWorkVisibility(token);
|
||||||
|
setEntries(sortEntries(response.entries));
|
||||||
|
} catch (error: unknown) {
|
||||||
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleToggle(entry: AdminWorkVisibilityEntryPayload) {
|
||||||
|
const nextVisible = !entry.visible;
|
||||||
|
const target = entry.title.trim() || entry.publicWorkCode || entry.profileId;
|
||||||
|
const confirmed = await confirmWrite({
|
||||||
|
action: nextVisible ? '显示作品' : '隐藏作品',
|
||||||
|
target,
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowKey = buildEntryKey(entry);
|
||||||
|
setSavingKey(rowKey);
|
||||||
|
setErrorMessage('');
|
||||||
|
try {
|
||||||
|
const response = await updateAdminWorkVisibility(token, {
|
||||||
|
sourceType: entry.sourceType,
|
||||||
|
profileId: entry.profileId,
|
||||||
|
visible: nextVisible,
|
||||||
|
});
|
||||||
|
upsertEntry(response.entry);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||||
|
} finally {
|
||||||
|
setSavingKey('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertEntry(next: AdminWorkVisibilityEntryPayload) {
|
||||||
|
setEntries((current) =>
|
||||||
|
sortEntries([
|
||||||
|
...current.filter((entry) => buildEntryKey(entry) !== buildEntryKey(next)),
|
||||||
|
next,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="admin-page admin-page-wide">
|
||||||
|
<div className="admin-page-heading">
|
||||||
|
<div>
|
||||||
|
<h2>作品可见性</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="admin-secondary-button"
|
||||||
|
disabled={isLoading}
|
||||||
|
type="button"
|
||||||
|
onClick={refreshEntries}
|
||||||
|
>
|
||||||
|
<RefreshCcw size={17} aria-hidden="true" />
|
||||||
|
<span>{isLoading ? '刷新中' : '刷新'}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="admin-panel">
|
||||||
|
<label className="admin-field">
|
||||||
|
<span>搜索</span>
|
||||||
|
<input
|
||||||
|
placeholder="标题 / 作者 / 公开码 / profileId"
|
||||||
|
value={keyword}
|
||||||
|
onChange={(event) => setKeyword(event.target.value)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className="admin-alert" role="status">
|
||||||
|
{errorMessage}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className="admin-table-wrap">
|
||||||
|
<table className="admin-table admin-table-wide">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>玩法</th>
|
||||||
|
<th>作品</th>
|
||||||
|
<th>作者</th>
|
||||||
|
<th>公开码</th>
|
||||||
|
<th>更新时间</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredEntries.map((entry) => {
|
||||||
|
const rowKey = buildEntryKey(entry);
|
||||||
|
const isSaving = savingKey === rowKey;
|
||||||
|
return (
|
||||||
|
<tr key={rowKey}>
|
||||||
|
<td>
|
||||||
|
<span className="admin-tag">
|
||||||
|
{sourceLabels[entry.sourceType] ?? entry.sourceType}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<strong>{entry.title || entry.profileId}</strong>
|
||||||
|
<small>{entry.subtitle || entry.profileId}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{entry.authorDisplayName || '玩家'}
|
||||||
|
<small>{entry.ownerUserId}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span className="admin-table-cell-ellipsis">
|
||||||
|
{entry.publicWorkCode}
|
||||||
|
</span>
|
||||||
|
<small>{entry.profileId}</small>
|
||||||
|
</td>
|
||||||
|
<td>{formatMicros(entry.updatedAtMicros)}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
entry.visible
|
||||||
|
? 'admin-status admin-status-ok'
|
||||||
|
: 'admin-status admin-status-error'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{entry.visible ? '显示' : '隐藏'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
className={
|
||||||
|
entry.visible
|
||||||
|
? 'admin-danger-button'
|
||||||
|
: 'admin-secondary-button'
|
||||||
|
}
|
||||||
|
disabled={isSaving}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleToggle(entry)}
|
||||||
|
>
|
||||||
|
{entry.visible ? (
|
||||||
|
<EyeOff size={16} aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Eye size={16} aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
<span>
|
||||||
|
{isSaving
|
||||||
|
? '处理中'
|
||||||
|
: entry.visible
|
||||||
|
? '隐藏'
|
||||||
|
: '显示'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLoading && filteredEntries.length === 0 ? (
|
||||||
|
<div className="admin-empty-state">暂无作品</div>
|
||||||
|
) : null}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{confirmDialog}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortEntries(entries: AdminWorkVisibilityEntryPayload[]) {
|
||||||
|
return [...entries].sort((left, right) => {
|
||||||
|
const timeCompare = right.updatedAtMicros - left.updatedAtMicros;
|
||||||
|
if (timeCompare !== 0) {
|
||||||
|
return timeCompare;
|
||||||
|
}
|
||||||
|
const sourceCompare = left.sourceType.localeCompare(right.sourceType);
|
||||||
|
if (sourceCompare !== 0) {
|
||||||
|
return sourceCompare;
|
||||||
|
}
|
||||||
|
return left.profileId.localeCompare(right.profileId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEntryKey(entry: AdminWorkVisibilityEntryPayload) {
|
||||||
|
return `${entry.sourceType}:${entry.profileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMicros(value: number) {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
const date = new Date(Math.floor(value / 1000));
|
||||||
|
if (!Number.isFinite(date.getTime())) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
return date.toLocaleString('zh-CN', {hour12: false});
|
||||||
|
}
|
||||||
83
docs/superpowers/plans/2026-05-27-admin-work-visibility.md
Normal file
83
docs/superpowers/plans/2026-05-27-admin-work-visibility.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 作品可见性后台管理 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在后台增加统一作品可见性列表与修改能力,让管理员可以把已发布作品从公开 read model 中隐藏或恢复显示。
|
||||||
|
|
||||||
|
**Architecture:** 可见性仍以各玩法源表 `visible` 字段为真相源;新增 SpacetimeDB admin procedure 统一列出和更新各玩法作品可见性,`api-server` 只做鉴权、DTO 校验和 BFF 转发,后台前端新增简洁管理页。统一公开 read model 继续只消费 `visible=true` 的 source view,不向公开契约暴露后台字段。
|
||||||
|
|
||||||
|
**Tech Stack:** Rust server-rs + SpacetimeDB module/procedure + spacetime-client bindings/facade + shared-contracts DTO + React admin-web TypeScript。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 文档契约补齐
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`
|
||||||
|
- Modify: `docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`
|
||||||
|
|
||||||
|
- [ ] 在 API 路由分组中补充 `/admin/api/works/visibility`。
|
||||||
|
- [ ] 在统一公开作品 ReadModel 文档中写清后台只能修改源表 `visible`,隐藏后不进入 `public_work_gallery_entry` / `public_work_detail_entry`。
|
||||||
|
|
||||||
|
### Task 2: DTO 与后端路由
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server-rs/crates/shared-contracts/src/admin.rs`
|
||||||
|
- Modify: `server-rs/crates/api-server/src/admin.rs`
|
||||||
|
- Modify: `server-rs/crates/api-server/src/app.rs` 或现有 admin module router 文件
|
||||||
|
|
||||||
|
- [ ] 增加 `AdminWorkVisibilityEntryPayload`、`AdminWorkVisibilityListResponse`、`AdminUpdateWorkVisibilityRequest`、`AdminUpdateWorkVisibilityResponse`。
|
||||||
|
- [ ] 新增 `GET /admin/api/works/visibility` handler,必须走 `require_admin_auth`。
|
||||||
|
- [ ] 新增 `POST /admin/api/works/visibility` handler,校验 `sourceType`、`profileId` 非空并转发到 SpacetimeDB facade。
|
||||||
|
|
||||||
|
### Task 3: SpacetimeDB runtime/procedure 与 facade
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `server-rs/crates/module-runtime/src/domain.rs`
|
||||||
|
- Create: `server-rs/crates/spacetime-module/src/runtime/admin_work_visibility.rs`
|
||||||
|
- Modify: `server-rs/crates/spacetime-module/src/runtime.rs`
|
||||||
|
- Modify: `server-rs/crates/spacetime-module/src/lib.rs`
|
||||||
|
- Modify: `server-rs/crates/spacetime-client/src/runtime.rs`
|
||||||
|
- Modify: `server-rs/crates/spacetime-client/src/mapper/runtime.rs`
|
||||||
|
|
||||||
|
- [ ] 增加 module-runtime typed input/output 类型。
|
||||||
|
- [ ] SpacetimeDB procedure 统一读取各玩法已发布源表/view,并返回可见性列表。
|
||||||
|
- [ ] SpacetimeDB procedure 根据 `sourceType + profileId` 修改对应源表 `visible`;`custom-world` 同步 `custom_world_gallery_entry.visible`;`big-fish` 使用 `session_id`,`bark-battle` 使用 `work_id`。
|
||||||
|
- [ ] spacetime-client 增加 list/update facade 和 mapper。
|
||||||
|
|
||||||
|
### Task 4: 后台前端页面
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `apps/admin-web/src/api/adminApiTypes.ts`
|
||||||
|
- Modify: `apps/admin-web/src/api/adminApiClient.ts`
|
||||||
|
- Create: `apps/admin-web/src/pages/AdminWorkVisibilityPage.tsx`
|
||||||
|
- Modify: `apps/admin-web/src/app/adminRoutes.ts`
|
||||||
|
- Modify: `apps/admin-web/src/app/AdminShell.tsx`
|
||||||
|
- Modify: `apps/admin-web/src/app/AdminApp.tsx`
|
||||||
|
|
||||||
|
- [ ] 增加 API 类型和 client 方法。
|
||||||
|
- [ ] 新增简洁表格页,显示玩法、标题、作者、公开码、更新时间、可见状态。
|
||||||
|
- [ ] 修改可见性时使用 `useAdminWriteConfirm` 确认。
|
||||||
|
- [ ] 接入后台导航和 route switch。
|
||||||
|
|
||||||
|
### Task 5: 生成绑定与验证
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Generated: `server-rs/crates/spacetime-client/src/module_bindings*`
|
||||||
|
- Generated: front-end shared bindings if generator updates them
|
||||||
|
|
||||||
|
- [ ] Run: `npm run spacetime:generate`。
|
||||||
|
- [ ] Run: `npm run check:spacetime-schema`。
|
||||||
|
- [ ] Run: `cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`。
|
||||||
|
- [ ] Run: `cargo check -p api-server --manifest-path server-rs/Cargo.toml`。
|
||||||
|
- [ ] Run: `npm run admin-web:typecheck`。
|
||||||
|
- [ ] Run: `npm run check:encoding`。
|
||||||
|
|
||||||
|
### Task 6: 提交并推送
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- All changed files
|
||||||
|
|
||||||
|
- [ ] Inspect `git diff` and `git status --short --branch`。
|
||||||
|
- [ ] Commit with message `feat: add admin work visibility controls`。
|
||||||
|
- [ ] Push current branch `codex/visible-work-field`。
|
||||||
@@ -39,8 +39,21 @@
|
|||||||
- `sort_time_micros`
|
- `sort_time_micros`
|
||||||
- `detail_payload_json`
|
- `detail_payload_json`
|
||||||
|
|
||||||
|
作品源表新增 `visible` 可见性字段,默认 `true`。`visible` 属于源表 / source view 过滤条件,不作为统一公开契约默认返回字段;当 `visible=false` 时,对应作品不得进入 `public_work_gallery_entry` 和 `public_work_detail_entry`。
|
||||||
|
|
||||||
其中 `detail_payload_json` 只承载平台详情页展示扩展,不承载正式 runtime 配置、玩法规则或草稿真相。
|
其中 `detail_payload_json` 只承载平台详情页展示扩展,不承载正式 runtime 配置、玩法规则或草稿真相。
|
||||||
|
|
||||||
|
## 后台可见性管理
|
||||||
|
|
||||||
|
后台通过独立接口管理已发布作品的源表可见性:
|
||||||
|
|
||||||
|
- `GET /admin/api/works/visibility`
|
||||||
|
- `POST /admin/api/works/visibility`
|
||||||
|
|
||||||
|
后台操作 key 使用统一的 `sourceType + profileId` 组合。`profileId` 在大多数玩法中对应作品 profile;特殊玩法维持既有源表身份:`big-fish` 对应 `session_id`,`bark-battle` 对应 `work_id`。`custom-world` 更新源表时必须同步 `custom_world_gallery_entry.visible`,避免兼容 gallery 缓存与统一公开 read model 出现可见性漂移。
|
||||||
|
|
||||||
|
该后台能力只修改源表 / source view 过滤事实,不把 `visible` 暴露到公开列表或公开详情契约。隐藏作品后,统一 `public_work_gallery_entry` 与 `public_work_detail_entry` 不再返回该作品;恢复显示后重新进入公开 read model。
|
||||||
|
|
||||||
## 来源与兼容
|
## 来源与兼容
|
||||||
|
|
||||||
统一 public view 由现有玩法 source view 组装:
|
统一 public view 由现有玩法 source view 组装:
|
||||||
@@ -63,6 +76,7 @@
|
|||||||
- 旧 view 保留,不删除。
|
- 旧 view 保留,不删除。
|
||||||
- 旧 view 退到底层 source / 兼容职责。
|
- 旧 view 退到底层 source / 兼容职责。
|
||||||
- 新 `public_work_*` view 是 `api-server` 公开列表 / 详情的统一主读模型。
|
- 新 `public_work_*` view 是 `api-server` 公开列表 / 详情的统一主读模型。
|
||||||
|
- 各玩法 source view 只暴露 `visible=true` 的已发布作品;旧数据迁移默认补 `visible=true`,避免历史作品被误隐藏。
|
||||||
- 旧 `/api/runtime/<play>/gallery` 响应 shape 保持兼容,由 BFF mapper 把统一 cache 再映射回当前 DTO。
|
- 旧 `/api/runtime/<play>/gallery` 响应 shape 保持兼容,由 BFF mapper 把统一 cache 再映射回当前 DTO。
|
||||||
- 旧详情 / runtime / 点赞 / 游玩 / Remix 仍走玩法专用路径。
|
- 旧详情 / runtime / 点赞 / 游玩 / Remix 仍走玩法专用路径。
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ npm run check:server-rs-ddd
|
|||||||
路由树由 `server-rs/crates/api-server/src/app.rs` 统一构造。当前主要分组:
|
路由树由 `server-rs/crates/api-server/src/app.rs` 统一构造。当前主要分组:
|
||||||
|
|
||||||
- 健康检查:`GET /healthz`。
|
- 健康检查:`GET /healthz`。
|
||||||
- 后台管理:`/admin/api/*`,包括登录、概览、HTTP debug、埋点、表查询、创作入口开关、兑换码、邀请码、任务配置和充值商品配置。
|
- 后台管理:`/admin/api/*`,包括登录、概览、HTTP debug、埋点、表查询、创作入口开关、作品可见性、兑换码、邀请码、任务配置和充值商品配置。
|
||||||
- 认证与账号:`/api/auth/*`、`/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
|
- 认证与账号:`/api/auth/*`、`/api/profile/me`,包括短信、密码、微信、refresh session、多端会话和登出。
|
||||||
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请、兑换、存档、历史浏览和游玩统计。
|
- 个人中心:`/api/profile/*`,包括钱包流水、任务、领奖、充值、反馈、邀请、兑换、存档、历史浏览和游玩统计。
|
||||||
- LLM 与语音:`/api/llm/*`、`/api/speech/volcengine/*`。
|
- LLM 与语音:`/api/llm/*`、`/api/speech/volcengine/*`。
|
||||||
@@ -257,6 +257,7 @@ npm run check:server-rs-ddd
|
|||||||
|
|
||||||
- Rust 结构体:`BarkBattlePublishedConfigRow`
|
- Rust 结构体:`BarkBattlePublishedConfigRow`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/bark_battle/tables.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/bark_battle/tables.rs`
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `bark_battle_runtime_run`
|
### `bark_battle_runtime_run`
|
||||||
|
|
||||||
@@ -293,6 +294,7 @@ npm run check:server-rs-ddd
|
|||||||
- Rust 结构体:`BigFishCreationSession`
|
- Rust 结构体:`BigFishCreationSession`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/big_fish/tables.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/big_fish/tables.rs`
|
||||||
- 索引:`by_big_fish_session_owner_user_id`、`by_big_fish_session_stage`。公开广场 view 使用 `by_big_fish_session_stage` 读取已发布会话,避免扫整表。
|
- 索引:`by_big_fish_session_owner_user_id`、`by_big_fish_session_stage`。公开广场 view 使用 `by_big_fish_session_stage` 读取已发布会话,避免扫整表。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `big_fish_event`
|
### `big_fish_event`
|
||||||
|
|
||||||
@@ -356,11 +358,13 @@ npm run check:server-rs-ddd
|
|||||||
- Rust 结构体:`CustomWorldGalleryEntry`
|
- Rust 结构体:`CustomWorldGalleryEntry`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
|
||||||
- 作用:自定义世界公开 source 读模型。统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该投影并映射成跨玩法契约;`/api/runtime/custom-world-gallery` 保留旧 HTTP shape,并从统一 public cache 映射回旧 DTO。旧 procedure 只用于兼容旧库缺少 gallery 读模型行时的一次性同步兜底。
|
- 作用:自定义世界公开 source 读模型。统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该投影并映射成跨玩法契约;`/api/runtime/custom-world-gallery` 保留旧 HTTP shape,并从统一 public cache 映射回旧 DTO。旧 procedure 只用于兼容旧库缺少 gallery 读模型行时的一次性同步兜底。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `custom_world_profile`
|
### `custom_world_profile`
|
||||||
|
|
||||||
- Rust 结构体:`CustomWorldProfile`
|
- Rust 结构体:`CustomWorldProfile`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/custom_world.rs`
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `custom_world_session`
|
### `custom_world_session`
|
||||||
|
|
||||||
@@ -415,6 +419,7 @@ npm run check:server-rs-ddd
|
|||||||
- 返回类型:`Vec<JumpHopGalleryViewRow>`
|
- 返回类型:`Vec<JumpHopGalleryViewRow>`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs`
|
||||||
- 说明:跳一跳公开详情兼容投影,包含作品、路径和素材字段;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。
|
- 说明:跳一跳公开详情兼容投影,包含作品、路径和素材字段;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `wooden_fish_agent_session`
|
### `wooden_fish_agent_session`
|
||||||
|
|
||||||
@@ -450,6 +455,7 @@ npm run check:server-rs-ddd
|
|||||||
- 返回类型:`Vec<WoodenFishGalleryViewRow>`
|
- 返回类型:`Vec<WoodenFishGalleryViewRow>`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs`
|
||||||
- 说明:敲木鱼公开详情兼容投影,包含敲击物图案、背景环境图、主题返回按钮图、敲击音效和飘字配置;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。
|
- 说明:敲木鱼公开详情兼容投影,包含敲击物图案、背景环境图、主题返回按钮图、敲击音效和飘字配置;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `match3d_agent_message`
|
### `match3d_agent_message`
|
||||||
|
|
||||||
@@ -477,6 +483,7 @@ npm run check:server-rs-ddd
|
|||||||
- 返回类型:`Vec<Match3DGalleryViewRow>`
|
- 返回类型:`Vec<Match3DGalleryViewRow>`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/match3d.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/match3d.rs`
|
||||||
- 说明:抓大鹅公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。
|
- 说明:抓大鹅公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `npc_state`
|
### `npc_state`
|
||||||
|
|
||||||
@@ -663,6 +670,7 @@ RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`;
|
|||||||
结构化创作和 RPG 的 LLM JSON 链路默认不启用 Responses `web_search`;只有在明确需要联网增强时,才通过 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED` 显式打开。否则未开通工具的上游会先吐自然语言再返回 `ToolNotOpen`,这类失败要按上游工具不可用处理,不要误判成模型返回结果解析失败。
|
结构化创作和 RPG 的 LLM JSON 链路默认不启用 Responses `web_search`;只有在明确需要联网增强时,才通过 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED` 显式打开。否则未开通工具的上游会先吐自然语言再返回 `ToolNotOpen`,这类失败要按上游工具不可用处理,不要误判成模型返回结果解析失败。
|
||||||
|
|
||||||
统一公开作品 BFF 路由是 `GET /api/public-works` 与 `GET /api/public-works/{publicWorkCode}`,响应契约由 `shared-contracts::public_work` 和 `packages/shared/src/contracts/publicWork.ts` 共同维护。前端首期仍走 BFF HTTP,不直接订阅 SpacetimeDB;后续若允许浏览器直连订阅,也只能订阅 `public_work_gallery_entry` / `public_work_detail_entry` 这类稳定公开 read model,不能订阅 `puzzle_work_profile`、`custom_world_profile` 等源表后自行拼装列表。设计细节见 `docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`。
|
统一公开作品 BFF 路由是 `GET /api/public-works` 与 `GET /api/public-works/{publicWorkCode}`,响应契约由 `shared-contracts::public_work` 和 `packages/shared/src/contracts/publicWork.ts` 共同维护。前端首期仍走 BFF HTTP,不直接订阅 SpacetimeDB;后续若允许浏览器直连订阅,也只能订阅 `public_work_gallery_entry` / `public_work_detail_entry` 这类稳定公开 read model,不能订阅 `puzzle_work_profile`、`custom_world_profile` 等源表后自行拼装列表。设计细节见 `docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `quest_log`
|
### `quest_log`
|
||||||
|
|
||||||
@@ -715,6 +723,7 @@ RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`;
|
|||||||
- 返回类型:`Vec<SquareHoleGalleryViewRow>`
|
- 返回类型:`Vec<SquareHoleGalleryViewRow>`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/square_hole.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/square_hole.rs`
|
||||||
- 说明:方洞挑战公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。
|
- 说明:方洞挑战公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|
||||||
### `story_event`
|
### `story_event`
|
||||||
|
|
||||||
@@ -790,3 +799,4 @@ RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`;
|
|||||||
- 返回类型:`Vec<VisualNovelGalleryViewRow>`
|
- 返回类型:`Vec<VisualNovelGalleryViewRow>`
|
||||||
- 源码:`server-rs/crates/spacetime-module/src/visual_novel.rs`
|
- 源码:`server-rs/crates/spacetime-module/src/visual_novel.rs`
|
||||||
- 说明:视觉小说公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段,不把完整 `draft` 暴露给公开列表订阅;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人历史、详情、运行态和发布仍按原有 procedure / reducer 路径处理。
|
- 说明:视觉小说公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段,不把完整 `draft` 暴露给公开列表订阅;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人历史、详情、运行态和发布仍按原有 procedure / reducer 路径处理。
|
||||||
|
- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ use shared_contracts::admin::{
|
|||||||
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
|
AdminDebugHeaderInput, AdminDebugHttpRequest, AdminDebugHttpResponse, AdminLoginRequest,
|
||||||
AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, AdminServiceOverviewPayload,
|
AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, AdminServiceOverviewPayload,
|
||||||
AdminSessionPayload, AdminTrackingEventEntryPayload, AdminTrackingEventListQuery,
|
AdminSessionPayload, AdminTrackingEventEntryPayload, AdminTrackingEventListQuery,
|
||||||
AdminTrackingEventListResponse, AdminUpsertCreationEntryTypeConfigRequest,
|
AdminTrackingEventListResponse, AdminUpdateWorkVisibilityRequest,
|
||||||
|
AdminUpdateWorkVisibilityResponse, AdminUpsertCreationEntryTypeConfigRequest,
|
||||||
|
AdminWorkVisibilityListResponse,
|
||||||
};
|
};
|
||||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||||
|
|
||||||
@@ -239,6 +241,40 @@ pub async fn admin_upsert_creation_entry_config(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn admin_list_work_visibility(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let admin_user_id = admin.session().subject.clone();
|
||||||
|
let entries = state
|
||||||
|
.list_admin_work_visibility(admin_user_id)
|
||||||
|
.await
|
||||||
|
.map_err(map_admin_spacetime_error)?;
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
AdminWorkVisibilityListResponse { entries },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn admin_update_work_visibility(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Extension(request_context): Extension<RequestContext>,
|
||||||
|
Extension(admin): Extension<AuthenticatedAdmin>,
|
||||||
|
Json(payload): Json<AdminUpdateWorkVisibilityRequest>,
|
||||||
|
) -> Result<Json<Value>, AppError> {
|
||||||
|
let entry = validate_admin_work_visibility(payload)?;
|
||||||
|
let admin_user_id = admin.session().subject.clone();
|
||||||
|
let record = state
|
||||||
|
.update_admin_work_visibility(admin_user_id, entry.0, entry.1, entry.2)
|
||||||
|
.await
|
||||||
|
.map_err(map_admin_spacetime_error)?;
|
||||||
|
Ok(json_success_body(
|
||||||
|
Some(&request_context),
|
||||||
|
AdminUpdateWorkVisibilityResponse { entry: record },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn map_admin_creation_entry_type_config(
|
fn map_admin_creation_entry_type_config(
|
||||||
entry: shared_contracts::creation_entry_config::CreationEntryTypeResponse,
|
entry: shared_contracts::creation_entry_config::CreationEntryTypeResponse,
|
||||||
) -> AdminCreationEntryTypeConfigPayload {
|
) -> AdminCreationEntryTypeConfigPayload {
|
||||||
@@ -284,6 +320,20 @@ fn validate_admin_creation_entry_config(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_admin_work_visibility(
|
||||||
|
payload: AdminUpdateWorkVisibilityRequest,
|
||||||
|
) -> Result<(String, String, bool), AppError> {
|
||||||
|
let source_type = payload.source_type.trim().to_string();
|
||||||
|
if source_type.is_empty() {
|
||||||
|
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("sourceType 不能为空"));
|
||||||
|
}
|
||||||
|
let profile_id = payload.profile_id.trim().to_string();
|
||||||
|
if profile_id.is_empty() {
|
||||||
|
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("profileId 不能为空"));
|
||||||
|
}
|
||||||
|
Ok((source_type, profile_id, payload.visible))
|
||||||
|
}
|
||||||
|
|
||||||
fn map_admin_spacetime_error(error: spacetime_client::SpacetimeClientError) -> AppError {
|
fn map_admin_spacetime_error(error: spacetime_client::SpacetimeClientError) -> AppError {
|
||||||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(serde_json::json!({
|
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(serde_json::json!({
|
||||||
"provider": "spacetimedb",
|
"provider": "spacetimedb",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ use axum::{Router, middleware, routing::get};
|
|||||||
use crate::{
|
use crate::{
|
||||||
admin::{
|
admin::{
|
||||||
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
|
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
|
||||||
admin_list_database_tables, admin_list_tracking_events, admin_login, admin_me,
|
admin_list_database_tables, admin_list_tracking_events, admin_list_work_visibility,
|
||||||
admin_overview, admin_upsert_creation_entry_config, require_admin_auth,
|
admin_login, admin_me, admin_overview, admin_update_work_visibility,
|
||||||
|
admin_upsert_creation_entry_config, require_admin_auth,
|
||||||
},
|
},
|
||||||
runtime_profile::{
|
runtime_profile::{
|
||||||
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
|
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
|
||||||
@@ -70,6 +71,15 @@ pub fn router(state: AppState) -> Router<AppState> {
|
|||||||
require_admin_auth,
|
require_admin_auth,
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/admin/api/works/visibility",
|
||||||
|
get(admin_list_work_visibility)
|
||||||
|
.post(admin_update_work_visibility)
|
||||||
|
.route_layer(middleware::from_fn_with_state(
|
||||||
|
state.clone(),
|
||||||
|
require_admin_auth,
|
||||||
|
)),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/admin/api/profile/redeem-codes",
|
"/admin/api/profile/redeem-codes",
|
||||||
get(admin_list_profile_redeem_codes)
|
get(admin_list_profile_redeem_codes)
|
||||||
|
|||||||
@@ -489,6 +489,29 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_admin_work_visibility(
|
||||||
|
&self,
|
||||||
|
admin_user_id: String,
|
||||||
|
) -> Result<Vec<shared_contracts::admin::AdminWorkVisibilityEntryPayload>, SpacetimeClientError>
|
||||||
|
{
|
||||||
|
self.spacetime_client
|
||||||
|
.admin_list_work_visibility(admin_user_id)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_admin_work_visibility(
|
||||||
|
&self,
|
||||||
|
admin_user_id: String,
|
||||||
|
source_type: String,
|
||||||
|
profile_id: String,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<shared_contracts::admin::AdminWorkVisibilityEntryPayload, SpacetimeClientError>
|
||||||
|
{
|
||||||
|
self.spacetime_client
|
||||||
|
.admin_update_work_visibility(admin_user_id, source_type, profile_id, visible)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn is_creation_entry_route_enabled(
|
pub async fn is_creation_entry_route_enabled(
|
||||||
&self,
|
&self,
|
||||||
creation_type_id: &str,
|
creation_type_id: &str,
|
||||||
|
|||||||
@@ -139,6 +139,59 @@ pub struct CreationEntryConfigProcedureResult {
|
|||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 后台作品可见性列表项。
|
||||||
|
///
|
||||||
|
/// source_type/profile_id 是后台统一操作键;少数玩法的 profile_id 会映射到底层
|
||||||
|
/// session_id 或 work_id,避免后台了解每个源表的主键差异。
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AdminWorkVisibilitySnapshot {
|
||||||
|
pub source_type: String,
|
||||||
|
pub work_id: String,
|
||||||
|
pub profile_id: String,
|
||||||
|
pub source_session_id: Option<String>,
|
||||||
|
pub public_work_code: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
|
pub author_display_name: String,
|
||||||
|
pub title: String,
|
||||||
|
pub subtitle: String,
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
|
pub published_at_micros: Option<i64>,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AdminWorkVisibilityListInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AdminWorkVisibilityUpdateInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
pub source_type: String,
|
||||||
|
pub profile_id: String,
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AdminWorkVisibilityListProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub entries: Vec<AdminWorkVisibilitySnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AdminWorkVisibilityProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option<AdminWorkVisibilitySnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// 分析日期维表的纯领域快照。
|
/// 分析日期维表的纯领域快照。
|
||||||
///
|
///
|
||||||
/// date_key 沿用现有北京时间自然日桶:floor((occurred_at_micros + 8h) / 1d)。
|
/// date_key 沿用现有北京时间自然日桶:floor((occurred_at_micros + 8h) / 1d)。
|
||||||
|
|||||||
@@ -53,6 +53,48 @@ pub struct AdminUpsertCreationEntryTypeConfigRequest {
|
|||||||
pub category_sort_order: i32,
|
pub category_sort_order: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 后台作品可见性列表项。
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AdminWorkVisibilityEntryPayload {
|
||||||
|
pub source_type: String,
|
||||||
|
pub work_id: String,
|
||||||
|
pub profile_id: String,
|
||||||
|
pub source_session_id: Option<String>,
|
||||||
|
pub public_work_code: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
|
pub author_display_name: String,
|
||||||
|
pub title: String,
|
||||||
|
pub subtitle: String,
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
|
pub published_at_micros: Option<i64>,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 后台作品可见性列表响应。
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AdminWorkVisibilityListResponse {
|
||||||
|
pub entries: Vec<AdminWorkVisibilityEntryPayload>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 后台修改作品可见性请求。
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AdminUpdateWorkVisibilityRequest {
|
||||||
|
pub source_type: String,
|
||||||
|
pub profile_id: String,
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 后台修改作品可见性响应。
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AdminUpdateWorkVisibilityResponse {
|
||||||
|
pub entry: AdminWorkVisibilityEntryPayload,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AdminLoginResponse {
|
pub struct AdminLoginResponse {
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ pub use mapper::{
|
|||||||
BigFishDraftCompileRecordInput, BigFishGameDraftRecord, BigFishInputSubmitRecordInput,
|
BigFishDraftCompileRecordInput, BigFishGameDraftRecord, BigFishInputSubmitRecordInput,
|
||||||
BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput,
|
BigFishLevelBlueprintRecord, BigFishLikeReportRecordInput, BigFishMessageFinalizeRecordInput,
|
||||||
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput,
|
BigFishMessageSubmitRecordInput, BigFishPlayReportRecordInput, BigFishRunStartRecordInput,
|
||||||
BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRunRecord,
|
AdminWorkVisibilityRecord, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord,
|
||||||
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
|
BigFishRuntimeRunRecord, BigFishSessionCreateRecordInput, BigFishSessionRecord,
|
||||||
BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord, CreationEntryConfigRecord,
|
BigFishVector2Record, BigFishWorkRemixRecordInput, BigFishWorkSummaryRecord,
|
||||||
|
CreationEntryConfigRecord,
|
||||||
CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput,
|
CustomWorldAgentActionExecuteRecord, CustomWorldAgentActionExecuteRecordInput,
|
||||||
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput,
|
CustomWorldAgentCheckpointRecord, CustomWorldAgentMessageFinalizeRecordInput,
|
||||||
CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput,
|
CustomWorldAgentMessageRecord, CustomWorldAgentMessageSubmitRecordInput,
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ pub use self::puzzle::{
|
|||||||
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
|
PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput,
|
||||||
};
|
};
|
||||||
pub use self::runtime::{
|
pub use self::runtime::{
|
||||||
|
AdminWorkVisibilityRecord,
|
||||||
BigFishGameDraftRecord, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord,
|
BigFishGameDraftRecord, BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord,
|
||||||
BigFishRuntimeRunRecord, CreationEntryConfigRecord,
|
BigFishRuntimeRunRecord, CreationEntryConfigRecord,
|
||||||
};
|
};
|
||||||
@@ -193,7 +194,9 @@ pub(crate) use self::puzzle::{
|
|||||||
parse_puzzle_agent_stage_record,
|
parse_puzzle_agent_stage_record,
|
||||||
};
|
};
|
||||||
pub(crate) use self::runtime::{
|
pub(crate) use self::runtime::{
|
||||||
build_creation_entry_config_record_from_rows, map_creation_entry_config_procedure_result,
|
build_admin_work_visibility_list_input, build_admin_work_visibility_update_input,
|
||||||
|
build_creation_entry_config_record_from_rows, map_admin_work_visibility_list_procedure_result,
|
||||||
|
map_admin_work_visibility_procedure_result, map_creation_entry_config_procedure_result,
|
||||||
map_runtime_setting_procedure_result, map_runtime_snapshot_delete_procedure_result,
|
map_runtime_setting_procedure_result, map_runtime_snapshot_delete_procedure_result,
|
||||||
map_runtime_snapshot_procedure_result, map_runtime_snapshot_required_procedure_result,
|
map_runtime_snapshot_procedure_result, map_runtime_snapshot_required_procedure_result,
|
||||||
map_runtime_tracking_event_batch_procedure_result, map_runtime_tracking_event_procedure_result,
|
map_runtime_tracking_event_batch_procedure_result, map_runtime_tracking_event_procedure_result,
|
||||||
|
|||||||
@@ -18,6 +18,61 @@ impl From<module_runtime::CreationEntryTypeAdminUpsertInput> for CreationEntryTy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<module_runtime::AdminWorkVisibilityListInput> for AdminWorkVisibilityListInput {
|
||||||
|
fn from(input: module_runtime::AdminWorkVisibilityListInput) -> Self {
|
||||||
|
Self {
|
||||||
|
admin_user_id: input.admin_user_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<module_runtime::AdminWorkVisibilityUpdateInput> for AdminWorkVisibilityUpdateInput {
|
||||||
|
fn from(input: module_runtime::AdminWorkVisibilityUpdateInput) -> Self {
|
||||||
|
Self {
|
||||||
|
admin_user_id: input.admin_user_id,
|
||||||
|
source_type: input.source_type,
|
||||||
|
profile_id: input.profile_id,
|
||||||
|
visible: input.visible,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_admin_work_visibility_list_input(
|
||||||
|
admin_user_id: String,
|
||||||
|
) -> Result<module_runtime::AdminWorkVisibilityListInput, String> {
|
||||||
|
let admin_user_id = admin_user_id.trim().to_string();
|
||||||
|
if admin_user_id.is_empty() {
|
||||||
|
return Err("adminUserId 不能为空".to_string());
|
||||||
|
}
|
||||||
|
Ok(module_runtime::AdminWorkVisibilityListInput { admin_user_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_admin_work_visibility_update_input(
|
||||||
|
admin_user_id: String,
|
||||||
|
source_type: String,
|
||||||
|
profile_id: String,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<module_runtime::AdminWorkVisibilityUpdateInput, String> {
|
||||||
|
let admin_user_id = admin_user_id.trim().to_string();
|
||||||
|
if admin_user_id.is_empty() {
|
||||||
|
return Err("adminUserId 不能为空".to_string());
|
||||||
|
}
|
||||||
|
let source_type = source_type.trim().to_string();
|
||||||
|
if source_type.is_empty() {
|
||||||
|
return Err("sourceType 不能为空".to_string());
|
||||||
|
}
|
||||||
|
let profile_id = profile_id.trim().to_string();
|
||||||
|
if profile_id.is_empty() {
|
||||||
|
return Err("profileId 不能为空".to_string());
|
||||||
|
}
|
||||||
|
Ok(module_runtime::AdminWorkVisibilityUpdateInput {
|
||||||
|
admin_user_id,
|
||||||
|
source_type,
|
||||||
|
profile_id,
|
||||||
|
visible,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
|
impl From<module_runtime::RuntimeSettingGetInput> for RuntimeSettingGetInput {
|
||||||
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
|
fn from(input: module_runtime::RuntimeSettingGetInput) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -114,6 +169,7 @@ impl From<module_runtime::RuntimeTrackingEventInput> for RuntimeTrackingEventInp
|
|||||||
|
|
||||||
pub type CreationEntryConfigRecord =
|
pub type CreationEntryConfigRecord =
|
||||||
shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||||||
|
pub type AdminWorkVisibilityRecord = shared_contracts::admin::AdminWorkVisibilityEntryPayload;
|
||||||
|
|
||||||
pub(crate) fn map_creation_entry_config_procedure_result(
|
pub(crate) fn map_creation_entry_config_procedure_result(
|
||||||
result: CreationEntryConfigProcedureResult,
|
result: CreationEntryConfigProcedureResult,
|
||||||
@@ -131,6 +187,51 @@ pub(crate) fn map_creation_entry_config_procedure_result(
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_admin_work_visibility_list_procedure_result(
|
||||||
|
result: AdminWorkVisibilityListProcedureResult,
|
||||||
|
) -> Result<Vec<AdminWorkVisibilityRecord>, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||||
|
}
|
||||||
|
Ok(result
|
||||||
|
.entries
|
||||||
|
.into_iter()
|
||||||
|
.map(map_admin_work_visibility_snapshot)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_admin_work_visibility_procedure_result(
|
||||||
|
result: AdminWorkVisibilityProcedureResult,
|
||||||
|
) -> Result<AdminWorkVisibilityRecord, SpacetimeClientError> {
|
||||||
|
if !result.ok {
|
||||||
|
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||||
|
}
|
||||||
|
result
|
||||||
|
.record
|
||||||
|
.map(map_admin_work_visibility_snapshot)
|
||||||
|
.ok_or_else(|| SpacetimeClientError::missing_snapshot("后台作品可见性快照"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_admin_work_visibility_snapshot(
|
||||||
|
snapshot: AdminWorkVisibilitySnapshot,
|
||||||
|
) -> AdminWorkVisibilityRecord {
|
||||||
|
AdminWorkVisibilityRecord {
|
||||||
|
source_type: snapshot.source_type,
|
||||||
|
work_id: snapshot.work_id,
|
||||||
|
profile_id: snapshot.profile_id,
|
||||||
|
source_session_id: snapshot.source_session_id,
|
||||||
|
public_work_code: snapshot.public_work_code,
|
||||||
|
owner_user_id: snapshot.owner_user_id,
|
||||||
|
author_display_name: snapshot.author_display_name,
|
||||||
|
title: snapshot.title,
|
||||||
|
subtitle: snapshot.subtitle,
|
||||||
|
cover_image_src: snapshot.cover_image_src,
|
||||||
|
visible: snapshot.visible,
|
||||||
|
published_at_micros: snapshot.published_at_micros,
|
||||||
|
updated_at_micros: snapshot.updated_at_micros,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn build_creation_entry_config_record_from_rows(
|
pub(crate) fn build_creation_entry_config_record_from_rows(
|
||||||
header: CreationEntryConfig,
|
header: CreationEntryConfig,
|
||||||
mut creation_types: Vec<CreationEntryTypeConfig>,
|
mut creation_types: Vec<CreationEntryTypeConfig>,
|
||||||
|
|||||||
@@ -14,10 +14,17 @@ pub mod admin_list_profile_invite_codes_procedure;
|
|||||||
pub mod admin_list_profile_recharge_products_procedure;
|
pub mod admin_list_profile_recharge_products_procedure;
|
||||||
pub mod admin_list_profile_redeem_codes_procedure;
|
pub mod admin_list_profile_redeem_codes_procedure;
|
||||||
pub mod admin_list_profile_task_configs_procedure;
|
pub mod admin_list_profile_task_configs_procedure;
|
||||||
|
pub mod admin_list_work_visibility_procedure;
|
||||||
|
pub mod admin_update_work_visibility_procedure;
|
||||||
pub mod admin_upsert_profile_invite_code_procedure;
|
pub mod admin_upsert_profile_invite_code_procedure;
|
||||||
pub mod admin_upsert_profile_recharge_product_procedure;
|
pub mod admin_upsert_profile_recharge_product_procedure;
|
||||||
pub mod admin_upsert_profile_redeem_code_procedure;
|
pub mod admin_upsert_profile_redeem_code_procedure;
|
||||||
pub mod admin_upsert_profile_task_config_procedure;
|
pub mod admin_upsert_profile_task_config_procedure;
|
||||||
|
pub mod admin_work_visibility_list_input_type;
|
||||||
|
pub mod admin_work_visibility_list_procedure_result_type;
|
||||||
|
pub mod admin_work_visibility_procedure_result_type;
|
||||||
|
pub mod admin_work_visibility_snapshot_type;
|
||||||
|
pub mod admin_work_visibility_update_input_type;
|
||||||
pub mod advance_puzzle_next_level_procedure;
|
pub mod advance_puzzle_next_level_procedure;
|
||||||
pub mod ai_result_reference_input_type;
|
pub mod ai_result_reference_input_type;
|
||||||
pub mod ai_result_reference_kind_type;
|
pub mod ai_result_reference_kind_type;
|
||||||
@@ -1046,10 +1053,17 @@ pub use admin_list_profile_invite_codes_procedure::admin_list_profile_invite_cod
|
|||||||
pub use admin_list_profile_recharge_products_procedure::admin_list_profile_recharge_products;
|
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_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_profile_task_configs_procedure::admin_list_profile_task_configs;
|
||||||
|
pub use admin_list_work_visibility_procedure::admin_list_work_visibility;
|
||||||
|
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_invite_code_procedure::admin_upsert_profile_invite_code;
|
||||||
pub use admin_upsert_profile_recharge_product_procedure::admin_upsert_profile_recharge_product;
|
pub use admin_upsert_profile_recharge_product_procedure::admin_upsert_profile_recharge_product;
|
||||||
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code;
|
||||||
pub use admin_upsert_profile_task_config_procedure::admin_upsert_profile_task_config;
|
pub use admin_upsert_profile_task_config_procedure::admin_upsert_profile_task_config;
|
||||||
|
pub use admin_work_visibility_list_input_type::AdminWorkVisibilityListInput;
|
||||||
|
pub use admin_work_visibility_list_procedure_result_type::AdminWorkVisibilityListProcedureResult;
|
||||||
|
pub use admin_work_visibility_procedure_result_type::AdminWorkVisibilityProcedureResult;
|
||||||
|
pub use admin_work_visibility_snapshot_type::AdminWorkVisibilitySnapshot;
|
||||||
|
pub use admin_work_visibility_update_input_type::AdminWorkVisibilityUpdateInput;
|
||||||
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
pub use advance_puzzle_next_level_procedure::advance_puzzle_next_level;
|
||||||
pub use ai_result_reference_input_type::AiResultReferenceInput;
|
pub use ai_result_reference_input_type::AiResultReferenceInput;
|
||||||
pub use ai_result_reference_kind_type::AiResultReferenceKind;
|
pub use ai_result_reference_kind_type::AiResultReferenceKind;
|
||||||
|
|||||||
@@ -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::admin_work_visibility_list_input_type::AdminWorkVisibilityListInput;
|
||||||
|
use super::admin_work_visibility_list_procedure_result_type::AdminWorkVisibilityListProcedureResult;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct AdminListWorkVisibilityArgs {
|
||||||
|
pub input: AdminWorkVisibilityListInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminListWorkVisibilityArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `admin_list_work_visibility`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait admin_list_work_visibility {
|
||||||
|
fn admin_list_work_visibility(&self, input: AdminWorkVisibilityListInput) {
|
||||||
|
self.admin_list_work_visibility_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_list_work_visibility_then(
|
||||||
|
&self,
|
||||||
|
input: AdminWorkVisibilityListInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<AdminWorkVisibilityListProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl admin_list_work_visibility for super::RemoteProcedures {
|
||||||
|
fn admin_list_work_visibility_then(
|
||||||
|
&self,
|
||||||
|
input: AdminWorkVisibilityListInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<AdminWorkVisibilityListProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
) {
|
||||||
|
self.imp
|
||||||
|
.invoke_procedure_with_callback::<_, AdminWorkVisibilityListProcedureResult>(
|
||||||
|
"admin_list_work_visibility",
|
||||||
|
AdminListWorkVisibilityArgs { input },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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::admin_work_visibility_procedure_result_type::AdminWorkVisibilityProcedureResult;
|
||||||
|
use super::admin_work_visibility_update_input_type::AdminWorkVisibilityUpdateInput;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
struct AdminUpdateWorkVisibilityArgs {
|
||||||
|
pub input: AdminWorkVisibilityUpdateInput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminUpdateWorkVisibilityArgs {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// Extension trait for access to the procedure `admin_update_work_visibility`.
|
||||||
|
///
|
||||||
|
/// Implemented for [`super::RemoteProcedures`].
|
||||||
|
pub trait admin_update_work_visibility {
|
||||||
|
fn admin_update_work_visibility(&self, input: AdminWorkVisibilityUpdateInput) {
|
||||||
|
self.admin_update_work_visibility_then(input, |_, _| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_update_work_visibility_then(
|
||||||
|
&self,
|
||||||
|
input: AdminWorkVisibilityUpdateInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<AdminWorkVisibilityProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl admin_update_work_visibility for super::RemoteProcedures {
|
||||||
|
fn admin_update_work_visibility_then(
|
||||||
|
&self,
|
||||||
|
input: AdminWorkVisibilityUpdateInput,
|
||||||
|
|
||||||
|
__callback: impl FnOnce(
|
||||||
|
&super::ProcedureEventContext,
|
||||||
|
Result<AdminWorkVisibilityProcedureResult, __sdk::InternalError>,
|
||||||
|
) + Send
|
||||||
|
+ 'static,
|
||||||
|
) {
|
||||||
|
self.imp
|
||||||
|
.invoke_procedure_with_callback::<_, AdminWorkVisibilityProcedureResult>(
|
||||||
|
"admin_update_work_visibility",
|
||||||
|
AdminUpdateWorkVisibilityArgs { input },
|
||||||
|
__callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 AdminWorkVisibilityListInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminWorkVisibilityListInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -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::admin_work_visibility_snapshot_type::AdminWorkVisibilitySnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct AdminWorkVisibilityListProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub entries: Vec<AdminWorkVisibilitySnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminWorkVisibilityListProcedureResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -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::admin_work_visibility_snapshot_type::AdminWorkVisibilitySnapshot;
|
||||||
|
|
||||||
|
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
#[sats(crate = __lib)]
|
||||||
|
pub struct AdminWorkVisibilityProcedureResult {
|
||||||
|
pub ok: bool,
|
||||||
|
pub record: Option<AdminWorkVisibilitySnapshot>,
|
||||||
|
pub error_message: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminWorkVisibilityProcedureResult {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
// 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 AdminWorkVisibilitySnapshot {
|
||||||
|
pub source_type: String,
|
||||||
|
pub work_id: String,
|
||||||
|
pub profile_id: String,
|
||||||
|
pub source_session_id: Option<String>,
|
||||||
|
pub public_work_code: String,
|
||||||
|
pub owner_user_id: String,
|
||||||
|
pub author_display_name: String,
|
||||||
|
pub title: String,
|
||||||
|
pub subtitle: String,
|
||||||
|
pub cover_image_src: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
|
pub published_at_micros: Option<i64>,
|
||||||
|
pub updated_at_micros: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminWorkVisibilitySnapshot {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -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 AdminWorkVisibilityUpdateInput {
|
||||||
|
pub admin_user_id: String,
|
||||||
|
pub source_type: String,
|
||||||
|
pub profile_id: String,
|
||||||
|
pub visible: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl __sdk::InModule for AdminWorkVisibilityUpdateInput {
|
||||||
|
type Module = super::RemoteModule;
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ pub struct BarkBattlePublishedConfigRow {
|
|||||||
pub created_at: __sdk::Timestamp,
|
pub created_at: __sdk::Timestamp,
|
||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
pub published_at: __sdk::Timestamp,
|
pub published_at: __sdk::Timestamp,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for BarkBattlePublishedConfigRow {
|
impl __sdk::InModule for BarkBattlePublishedConfigRow {
|
||||||
@@ -41,6 +42,7 @@ pub struct BarkBattlePublishedConfigRowCols {
|
|||||||
pub created_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
|
pub created_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
|
||||||
pub updated_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
|
||||||
pub published_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
|
pub published_at: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, __sdk::Timestamp>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<BarkBattlePublishedConfigRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for BarkBattlePublishedConfigRow {
|
impl __sdk::__query_builder::HasCols for BarkBattlePublishedConfigRow {
|
||||||
@@ -65,6 +67,7 @@ impl __sdk::__query_builder::HasCols for BarkBattlePublishedConfigRow {
|
|||||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub struct BigFishCreationSession {
|
|||||||
pub remix_count: u32,
|
pub remix_count: u32,
|
||||||
pub like_count: u32,
|
pub like_count: u32,
|
||||||
pub published_at: Option<__sdk::Timestamp>,
|
pub published_at: Option<__sdk::Timestamp>,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for BigFishCreationSession {
|
impl __sdk::InModule for BigFishCreationSession {
|
||||||
@@ -53,6 +54,7 @@ pub struct BigFishCreationSessionCols {
|
|||||||
pub remix_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
pub remix_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
||||||
pub like_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
pub like_count: __sdk::__query_builder::Col<BigFishCreationSession, u32>,
|
||||||
pub published_at: __sdk::__query_builder::Col<BigFishCreationSession, Option<__sdk::Timestamp>>,
|
pub published_at: __sdk::__query_builder::Col<BigFishCreationSession, Option<__sdk::Timestamp>>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<BigFishCreationSession, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for BigFishCreationSession {
|
impl __sdk::__query_builder::HasCols for BigFishCreationSession {
|
||||||
@@ -82,6 +84,7 @@ impl __sdk::__query_builder::HasCols for BigFishCreationSession {
|
|||||||
remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"),
|
remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"),
|
||||||
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
||||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ pub struct CustomWorldGalleryEntry {
|
|||||||
pub like_count: u32,
|
pub like_count: u32,
|
||||||
pub published_at: __sdk::Timestamp,
|
pub published_at: __sdk::Timestamp,
|
||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for CustomWorldGalleryEntry {
|
impl __sdk::InModule for CustomWorldGalleryEntry {
|
||||||
@@ -53,6 +54,7 @@ pub struct CustomWorldGalleryEntryCols {
|
|||||||
pub like_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
pub like_count: __sdk::__query_builder::Col<CustomWorldGalleryEntry, u32>,
|
||||||
pub published_at: __sdk::__query_builder::Col<CustomWorldGalleryEntry, __sdk::Timestamp>,
|
pub published_at: __sdk::__query_builder::Col<CustomWorldGalleryEntry, __sdk::Timestamp>,
|
||||||
pub updated_at: __sdk::__query_builder::Col<CustomWorldGalleryEntry, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<CustomWorldGalleryEntry, __sdk::Timestamp>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<CustomWorldGalleryEntry, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for CustomWorldGalleryEntry {
|
impl __sdk::__query_builder::HasCols for CustomWorldGalleryEntry {
|
||||||
@@ -82,6 +84,7 @@ impl __sdk::__query_builder::HasCols for CustomWorldGalleryEntry {
|
|||||||
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
like_count: __sdk::__query_builder::Col::new(table_name, "like_count"),
|
||||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub struct CustomWorldProfile {
|
|||||||
pub deleted_at: Option<__sdk::Timestamp>,
|
pub deleted_at: Option<__sdk::Timestamp>,
|
||||||
pub created_at: __sdk::Timestamp,
|
pub created_at: __sdk::Timestamp,
|
||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for CustomWorldProfile {
|
impl __sdk::InModule for CustomWorldProfile {
|
||||||
@@ -65,6 +66,7 @@ pub struct CustomWorldProfileCols {
|
|||||||
pub deleted_at: __sdk::__query_builder::Col<CustomWorldProfile, Option<__sdk::Timestamp>>,
|
pub deleted_at: __sdk::__query_builder::Col<CustomWorldProfile, Option<__sdk::Timestamp>>,
|
||||||
pub created_at: __sdk::__query_builder::Col<CustomWorldProfile, __sdk::Timestamp>,
|
pub created_at: __sdk::__query_builder::Col<CustomWorldProfile, __sdk::Timestamp>,
|
||||||
pub updated_at: __sdk::__query_builder::Col<CustomWorldProfile, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<CustomWorldProfile, __sdk::Timestamp>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<CustomWorldProfile, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for CustomWorldProfile {
|
impl __sdk::__query_builder::HasCols for CustomWorldProfile {
|
||||||
@@ -105,6 +107,7 @@ impl __sdk::__query_builder::HasCols for CustomWorldProfile {
|
|||||||
deleted_at: __sdk::__query_builder::Col::new(table_name, "deleted_at"),
|
deleted_at: __sdk::__query_builder::Col::new(table_name, "deleted_at"),
|
||||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ pub struct JumpHopWorkProfileRow {
|
|||||||
pub play_count: u32,
|
pub play_count: u32,
|
||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
pub published_at: Option<__sdk::Timestamp>,
|
pub published_at: Option<__sdk::Timestamp>,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for JumpHopWorkProfileRow {
|
impl __sdk::InModule for JumpHopWorkProfileRow {
|
||||||
@@ -65,6 +66,7 @@ pub struct JumpHopWorkProfileRowCols {
|
|||||||
pub play_count: __sdk::__query_builder::Col<JumpHopWorkProfileRow, u32>,
|
pub play_count: __sdk::__query_builder::Col<JumpHopWorkProfileRow, u32>,
|
||||||
pub updated_at: __sdk::__query_builder::Col<JumpHopWorkProfileRow, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<JumpHopWorkProfileRow, __sdk::Timestamp>,
|
||||||
pub published_at: __sdk::__query_builder::Col<JumpHopWorkProfileRow, Option<__sdk::Timestamp>>,
|
pub published_at: __sdk::__query_builder::Col<JumpHopWorkProfileRow, Option<__sdk::Timestamp>>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<JumpHopWorkProfileRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow {
|
impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow {
|
||||||
@@ -104,6 +106,7 @@ impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow {
|
|||||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub struct Match3DWorkProfileRow {
|
|||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
pub published_at: Option<__sdk::Timestamp>,
|
pub published_at: Option<__sdk::Timestamp>,
|
||||||
pub generated_item_assets_json: Option<String>,
|
pub generated_item_assets_json: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for Match3DWorkProfileRow {
|
impl __sdk::InModule for Match3DWorkProfileRow {
|
||||||
@@ -54,6 +55,7 @@ pub struct Match3DWorkProfileRowCols {
|
|||||||
pub published_at: __sdk::__query_builder::Col<Match3DWorkProfileRow, Option<__sdk::Timestamp>>,
|
pub published_at: __sdk::__query_builder::Col<Match3DWorkProfileRow, Option<__sdk::Timestamp>>,
|
||||||
pub generated_item_assets_json:
|
pub generated_item_assets_json:
|
||||||
__sdk::__query_builder::Col<Match3DWorkProfileRow, Option<String>>,
|
__sdk::__query_builder::Col<Match3DWorkProfileRow, Option<String>>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<Match3DWorkProfileRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for Match3DWorkProfileRow {
|
impl __sdk::__query_builder::HasCols for Match3DWorkProfileRow {
|
||||||
@@ -84,6 +86,7 @@ impl __sdk::__query_builder::HasCols for Match3DWorkProfileRow {
|
|||||||
table_name,
|
table_name,
|
||||||
"generated_item_assets_json",
|
"generated_item_assets_json",
|
||||||
),
|
),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ pub struct PuzzleWorkProfileRow {
|
|||||||
pub like_count: u32,
|
pub like_count: u32,
|
||||||
pub point_incentive_total_half_points: u64,
|
pub point_incentive_total_half_points: u64,
|
||||||
pub point_incentive_claimed_points: u64,
|
pub point_incentive_claimed_points: u64,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for PuzzleWorkProfileRow {
|
impl __sdk::InModule for PuzzleWorkProfileRow {
|
||||||
@@ -68,6 +69,7 @@ pub struct PuzzleWorkProfileRowCols {
|
|||||||
pub like_count: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u32>,
|
pub like_count: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u32>,
|
||||||
pub point_incentive_total_half_points: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u64>,
|
pub point_incentive_total_half_points: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u64>,
|
||||||
pub point_incentive_claimed_points: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u64>,
|
pub point_incentive_claimed_points: __sdk::__query_builder::Col<PuzzleWorkProfileRow, u64>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<PuzzleWorkProfileRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow {
|
impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow {
|
||||||
@@ -107,6 +109,7 @@ impl __sdk::__query_builder::HasCols for PuzzleWorkProfileRow {
|
|||||||
table_name,
|
table_name,
|
||||||
"point_incentive_claimed_points",
|
"point_incentive_claimed_points",
|
||||||
),
|
),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ pub struct SquareHoleWorkProfileRow {
|
|||||||
pub play_count: u32,
|
pub play_count: u32,
|
||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
pub published_at: Option<__sdk::Timestamp>,
|
pub published_at: Option<__sdk::Timestamp>,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for SquareHoleWorkProfileRow {
|
impl __sdk::InModule for SquareHoleWorkProfileRow {
|
||||||
@@ -54,6 +55,7 @@ pub struct SquareHoleWorkProfileRowCols {
|
|||||||
pub updated_at: __sdk::__query_builder::Col<SquareHoleWorkProfileRow, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<SquareHoleWorkProfileRow, __sdk::Timestamp>,
|
||||||
pub published_at:
|
pub published_at:
|
||||||
__sdk::__query_builder::Col<SquareHoleWorkProfileRow, Option<__sdk::Timestamp>>,
|
__sdk::__query_builder::Col<SquareHoleWorkProfileRow, Option<__sdk::Timestamp>>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<SquareHoleWorkProfileRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for SquareHoleWorkProfileRow {
|
impl __sdk::__query_builder::HasCols for SquareHoleWorkProfileRow {
|
||||||
@@ -81,6 +83,7 @@ impl __sdk::__query_builder::HasCols for SquareHoleWorkProfileRow {
|
|||||||
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
play_count: __sdk::__query_builder::Col::new(table_name, "play_count"),
|
||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub struct VisualNovelWorkProfileRow {
|
|||||||
pub created_at: __sdk::Timestamp,
|
pub created_at: __sdk::Timestamp,
|
||||||
pub updated_at: __sdk::Timestamp,
|
pub updated_at: __sdk::Timestamp,
|
||||||
pub published_at: Option<__sdk::Timestamp>,
|
pub published_at: Option<__sdk::Timestamp>,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for VisualNovelWorkProfileRow {
|
impl __sdk::InModule for VisualNovelWorkProfileRow {
|
||||||
@@ -52,6 +53,7 @@ pub struct VisualNovelWorkProfileRowCols {
|
|||||||
pub updated_at: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, __sdk::Timestamp>,
|
pub updated_at: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, __sdk::Timestamp>,
|
||||||
pub published_at:
|
pub published_at:
|
||||||
__sdk::__query_builder::Col<VisualNovelWorkProfileRow, Option<__sdk::Timestamp>>,
|
__sdk::__query_builder::Col<VisualNovelWorkProfileRow, Option<__sdk::Timestamp>>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<VisualNovelWorkProfileRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for VisualNovelWorkProfileRow {
|
impl __sdk::__query_builder::HasCols for VisualNovelWorkProfileRow {
|
||||||
@@ -81,6 +83,7 @@ impl __sdk::__query_builder::HasCols for VisualNovelWorkProfileRow {
|
|||||||
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
|
||||||
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
|
||||||
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
published_at: __sdk::__query_builder::Col::new(table_name, "published_at"),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ pub struct WoodenFishWorkProfileRow {
|
|||||||
pub published_at: Option<__sdk::Timestamp>,
|
pub published_at: Option<__sdk::Timestamp>,
|
||||||
pub background_asset_json: Option<String>,
|
pub background_asset_json: Option<String>,
|
||||||
pub back_button_asset_json: Option<String>,
|
pub back_button_asset_json: Option<String>,
|
||||||
|
pub visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::InModule for WoodenFishWorkProfileRow {
|
impl __sdk::InModule for WoodenFishWorkProfileRow {
|
||||||
@@ -65,6 +66,7 @@ pub struct WoodenFishWorkProfileRowCols {
|
|||||||
__sdk::__query_builder::Col<WoodenFishWorkProfileRow, Option<String>>,
|
__sdk::__query_builder::Col<WoodenFishWorkProfileRow, Option<String>>,
|
||||||
pub back_button_asset_json:
|
pub back_button_asset_json:
|
||||||
__sdk::__query_builder::Col<WoodenFishWorkProfileRow, Option<String>>,
|
__sdk::__query_builder::Col<WoodenFishWorkProfileRow, Option<String>>,
|
||||||
|
pub visible: __sdk::__query_builder::Col<WoodenFishWorkProfileRow, bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl __sdk::__query_builder::HasCols for WoodenFishWorkProfileRow {
|
impl __sdk::__query_builder::HasCols for WoodenFishWorkProfileRow {
|
||||||
@@ -114,6 +116,7 @@ impl __sdk::__query_builder::HasCols for WoodenFishWorkProfileRow {
|
|||||||
table_name,
|
table_name,
|
||||||
"back_button_asset_json",
|
"back_button_asset_json",
|
||||||
),
|
),
|
||||||
|
visible: __sdk::__query_builder::Col::new(table_name, "visible"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,61 @@ impl SpacetimeClient {
|
|||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn admin_list_work_visibility(
|
||||||
|
&self,
|
||||||
|
admin_user_id: String,
|
||||||
|
) -> Result<Vec<shared_contracts::admin::AdminWorkVisibilityEntryPayload>, SpacetimeClientError>
|
||||||
|
{
|
||||||
|
let procedure_input = build_admin_work_visibility_list_input(admin_user_id)
|
||||||
|
.map_err(SpacetimeClientError::validation_failed)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
self.call_after_connect("admin_list_work_visibility", move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.admin_list_work_visibility_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(SpacetimeClientError::from_sdk_error)
|
||||||
|
.and_then(map_admin_work_visibility_list_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn admin_update_work_visibility(
|
||||||
|
&self,
|
||||||
|
admin_user_id: String,
|
||||||
|
source_type: String,
|
||||||
|
profile_id: String,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<shared_contracts::admin::AdminWorkVisibilityEntryPayload, SpacetimeClientError>
|
||||||
|
{
|
||||||
|
let procedure_input = build_admin_work_visibility_update_input(
|
||||||
|
admin_user_id,
|
||||||
|
source_type,
|
||||||
|
profile_id,
|
||||||
|
visible,
|
||||||
|
)
|
||||||
|
.map_err(SpacetimeClientError::validation_failed)?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
self.call_after_connect(
|
||||||
|
"admin_update_work_visibility",
|
||||||
|
move |connection, sender| {
|
||||||
|
connection
|
||||||
|
.procedures()
|
||||||
|
.admin_update_work_visibility_then(procedure_input, move |_, result| {
|
||||||
|
let mapped = result
|
||||||
|
.map_err(SpacetimeClientError::from_sdk_error)
|
||||||
|
.and_then(map_admin_work_visibility_procedure_result);
|
||||||
|
send_once(&sender, mapped);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_runtime_settings(
|
pub async fn get_runtime_settings(
|
||||||
&self,
|
&self,
|
||||||
user_id: String,
|
user_id: String,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub fn bark_battle_gallery_view(ctx: &AnonymousViewContext) -> Vec<BarkBattleGal
|
|||||||
.bark_battle_published_config()
|
.bark_battle_published_config()
|
||||||
.by_bark_battle_published_owner_user_id()
|
.by_bark_battle_published_owner_user_id()
|
||||||
.filter(""..)
|
.filter(""..)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_bark_battle_gallery_view_row(ctx, &row) {
|
.filter_map(|row| match build_bark_battle_gallery_view_row(ctx, &row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -260,6 +261,7 @@ fn publish_bark_battle_work_tx(
|
|||||||
created_at: published_at,
|
created_at: published_at,
|
||||||
updated_at: published_at,
|
updated_at: published_at,
|
||||||
published_at,
|
published_at,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
let mut published = published;
|
let mut published = published;
|
||||||
match ctx
|
match ctx
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = bark_battle_draft_config,
|
accessor = bark_battle_draft_config,
|
||||||
index(accessor = by_bark_battle_draft_owner_user_id, btree(columns = [owner_user_id])),
|
index(accessor = by_bark_battle_draft_owner_user_id, btree(columns = [owner_user_id])),
|
||||||
@@ -40,6 +42,9 @@ pub struct BarkBattlePublishedConfigRow {
|
|||||||
pub(crate) created_at: Timestamp,
|
pub(crate) created_at: Timestamp,
|
||||||
pub(crate) updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
pub(crate) published_at: Timestamp,
|
pub(crate) published_at: Timestamp,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ pub(crate) fn generate_big_fish_asset_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
for event in readiness.events {
|
for event in readiness.events {
|
||||||
@@ -200,6 +201,7 @@ pub(crate) fn publish_big_fish_game_tx(
|
|||||||
published_at: Some(published_at),
|
published_at: Some(published_at),
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at: published_at,
|
updated_at: published_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
for event in readiness.events {
|
for event in readiness.events {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ pub fn big_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec<BigFishWorkSumma
|
|||||||
.big_fish_creation_session()
|
.big_fish_creation_session()
|
||||||
.by_big_fish_session_stage()
|
.by_big_fish_session_stage()
|
||||||
.filter(BigFishCreationStage::Published)
|
.filter(BigFishCreationStage::Published)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_big_fish_gallery_view_row(ctx, &row) {
|
.filter_map(|row| match build_big_fish_gallery_view_row(ctx, &row) {
|
||||||
Ok(snapshot) => Some(snapshot),
|
Ok(snapshot) => Some(snapshot),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -284,6 +285,7 @@ pub(crate) fn create_big_fish_session_tx(
|
|||||||
published_at: None,
|
published_at: None,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at: created_at,
|
updated_at: created_at,
|
||||||
|
visible: true,
|
||||||
});
|
});
|
||||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||||
message_id: input.welcome_message_id,
|
message_id: input.welcome_message_id,
|
||||||
@@ -492,6 +494,7 @@ pub(crate) fn submit_big_fish_message_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at: submitted_at,
|
updated_at: submitted_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
|
|
||||||
@@ -542,6 +545,7 @@ pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
return Err(error_message.to_string());
|
return Err(error_message.to_string());
|
||||||
@@ -600,6 +604,7 @@ pub(crate) fn finalize_big_fish_agent_message_turn_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
|
|
||||||
@@ -667,6 +672,7 @@ pub(crate) fn compile_big_fish_draft_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at: compiled_at,
|
updated_at: compiled_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
for event in readiness.events {
|
for event in readiness.events {
|
||||||
@@ -768,6 +774,7 @@ pub(crate) fn record_big_fish_play_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at: played_at,
|
updated_at: played_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
|
|
||||||
@@ -821,6 +828,7 @@ pub(crate) fn record_big_fish_like_tx(
|
|||||||
published_at: session.published_at,
|
published_at: session.published_at,
|
||||||
created_at: session.created_at,
|
created_at: session.created_at,
|
||||||
updated_at: liked_at,
|
updated_at: liked_at,
|
||||||
|
visible: session.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &session, next_session);
|
replace_big_fish_session(ctx, &session, next_session);
|
||||||
}
|
}
|
||||||
@@ -888,6 +896,7 @@ fn remix_big_fish_work_tx(
|
|||||||
published_at: source.published_at,
|
published_at: source.published_at,
|
||||||
created_at: source.created_at,
|
created_at: source.created_at,
|
||||||
updated_at: remixed_at,
|
updated_at: remixed_at,
|
||||||
|
visible: source.visible,
|
||||||
};
|
};
|
||||||
replace_big_fish_session(ctx, &source, next_source);
|
replace_big_fish_session(ctx, &source, next_source);
|
||||||
|
|
||||||
@@ -909,6 +918,7 @@ fn remix_big_fish_work_tx(
|
|||||||
published_at: None,
|
published_at: None,
|
||||||
created_at: remixed_at,
|
created_at: remixed_at,
|
||||||
updated_at: remixed_at,
|
updated_at: remixed_at,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
ctx.db.big_fish_creation_session().insert(target_session);
|
ctx.db.big_fish_creation_session().insert(target_session);
|
||||||
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
ctx.db.big_fish_agent_message().insert(BigFishAgentMessage {
|
||||||
@@ -1238,6 +1248,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
visible: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = big_fish_creation_session,
|
accessor = big_fish_creation_session,
|
||||||
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])),
|
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])),
|
||||||
@@ -28,6 +30,9 @@ pub struct BigFishCreationSession {
|
|||||||
pub(crate) like_count: u32,
|
pub(crate) like_count: u32,
|
||||||
#[default(None::<Timestamp>)]
|
#[default(None::<Timestamp>)]
|
||||||
pub(crate) published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ use crate::*;
|
|||||||
use spacetimedb::AnonymousViewContext;
|
use spacetimedb::AnonymousViewContext;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = custom_world_profile,
|
accessor = custom_world_profile,
|
||||||
index(accessor = by_custom_world_profile_owner_user_id, btree(columns = [owner_user_id])),
|
index(accessor = by_custom_world_profile_owner_user_id, btree(columns = [owner_user_id])),
|
||||||
@@ -12,36 +14,39 @@ use std::collections::{HashMap, HashSet};
|
|||||||
)]
|
)]
|
||||||
pub struct CustomWorldProfile {
|
pub struct CustomWorldProfile {
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
profile_id: String,
|
pub(crate) profile_id: String,
|
||||||
// 当前 profile 承接 library / publish / enter-world 的正式世界工件真相。
|
// 当前 profile 承接 library / publish / enter-world 的正式世界工件真相。
|
||||||
owner_user_id: String,
|
pub(crate) owner_user_id: String,
|
||||||
// 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。
|
// 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。
|
||||||
public_work_code: Option<String>,
|
pub(crate) public_work_code: Option<String>,
|
||||||
// 作者公开陶泥号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
// 作者公开陶泥号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
||||||
author_public_user_code: Option<String>,
|
pub(crate) author_public_user_code: Option<String>,
|
||||||
source_agent_session_id: Option<String>,
|
pub(crate) source_agent_session_id: Option<String>,
|
||||||
publication_status: CustomWorldPublicationStatus,
|
pub(crate) publication_status: CustomWorldPublicationStatus,
|
||||||
world_name: String,
|
pub(crate) world_name: String,
|
||||||
subtitle: String,
|
pub(crate) subtitle: String,
|
||||||
summary_text: String,
|
pub(crate) summary_text: String,
|
||||||
theme_mode: CustomWorldThemeMode,
|
pub(crate) theme_mode: CustomWorldThemeMode,
|
||||||
cover_image_src: Option<String>,
|
pub(crate) cover_image_src: Option<String>,
|
||||||
profile_payload_json: String,
|
pub(crate) profile_payload_json: String,
|
||||||
playable_npc_count: u32,
|
pub(crate) playable_npc_count: u32,
|
||||||
landmark_count: u32,
|
pub(crate) landmark_count: u32,
|
||||||
// 公开消费计数随 profile 真相持久化,发布、编辑和取消发布都不能重置。
|
// 公开消费计数随 profile 真相持久化,发布、编辑和取消发布都不能重置。
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
play_count: u32,
|
pub(crate) play_count: u32,
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
remix_count: u32,
|
pub(crate) remix_count: u32,
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
like_count: u32,
|
pub(crate) like_count: u32,
|
||||||
author_display_name: String,
|
pub(crate) author_display_name: String,
|
||||||
published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
// 软删除后保留 profile 真相,供审计与幂等删除使用。
|
// 软删除后保留 profile 真相,供审计与幂等删除使用。
|
||||||
deleted_at: Option<Timestamp>,
|
pub(crate) deleted_at: Option<Timestamp>,
|
||||||
created_at: Timestamp,
|
pub(crate) created_at: Timestamp,
|
||||||
updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
@@ -170,28 +175,31 @@ pub struct CustomWorldDraftCard {
|
|||||||
)]
|
)]
|
||||||
pub struct CustomWorldGalleryEntry {
|
pub struct CustomWorldGalleryEntry {
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
profile_id: String,
|
pub(crate) profile_id: String,
|
||||||
// 画廊是公开订阅读模型,不再运行时从 profile 即席拼装。
|
// 画廊是公开订阅读模型,不再运行时从 profile 即席拼装。
|
||||||
owner_user_id: String,
|
pub(crate) owner_user_id: String,
|
||||||
public_work_code: String,
|
pub(crate) public_work_code: String,
|
||||||
author_public_user_code: String,
|
pub(crate) author_public_user_code: String,
|
||||||
author_display_name: String,
|
pub(crate) author_display_name: String,
|
||||||
world_name: String,
|
pub(crate) world_name: String,
|
||||||
subtitle: String,
|
pub(crate) subtitle: String,
|
||||||
summary_text: String,
|
pub(crate) summary_text: String,
|
||||||
cover_image_src: Option<String>,
|
pub(crate) cover_image_src: Option<String>,
|
||||||
theme_mode: CustomWorldThemeMode,
|
pub(crate) theme_mode: CustomWorldThemeMode,
|
||||||
playable_npc_count: u32,
|
pub(crate) playable_npc_count: u32,
|
||||||
landmark_count: u32,
|
pub(crate) landmark_count: u32,
|
||||||
// 画廊读模型直接同步互动计数,避免前端临时把评分或游玩数改名成点赞。
|
// 画廊读模型直接同步互动计数,避免前端临时把评分或游玩数改名成点赞。
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
play_count: u32,
|
pub(crate) play_count: u32,
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
remix_count: u32,
|
pub(crate) remix_count: u32,
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
like_count: u32,
|
pub(crate) like_count: u32,
|
||||||
published_at: Timestamp,
|
pub(crate) published_at: Timestamp,
|
||||||
updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
// Agent 会话首版只负责把可持久化创作状态落进 SpacetimeDB,LLM 采集与卡片生成后续再接入。
|
// Agent 会话首版只负责把可持久化创作状态落进 SpacetimeDB,LLM 采集与卡片生成后续再接入。
|
||||||
#[spacetimedb::procedure]
|
#[spacetimedb::procedure]
|
||||||
@@ -1229,6 +1237,7 @@ fn upsert_custom_world_profile_record(
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
visible: existing.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => CustomWorldProfile {
|
None => CustomWorldProfile {
|
||||||
@@ -1254,6 +1263,7 @@ fn upsert_custom_world_profile_record(
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: updated_at,
|
created_at: updated_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
visible: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1401,6 +1411,7 @@ fn publish_custom_world_profile_record(
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: published_at,
|
updated_at: published_at,
|
||||||
|
visible: existing.visible,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
||||||
@@ -1467,6 +1478,7 @@ fn unpublish_custom_world_profile_record(
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
visible: existing.visible,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
||||||
@@ -1529,6 +1541,7 @@ fn delete_custom_world_profile_record(
|
|||||||
deleted_at: Some(deleted_at),
|
deleted_at: Some(deleted_at),
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: deleted_at,
|
updated_at: deleted_at,
|
||||||
|
visible: existing.visible,
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = ctx.db.custom_world_profile().insert(next_row);
|
let _ = ctx.db.custom_world_profile().insert(next_row);
|
||||||
@@ -1651,6 +1664,7 @@ fn get_custom_world_gallery_detail_record(
|
|||||||
row.owner_user_id == input.owner_user_id
|
row.owner_user_id == input.owner_user_id
|
||||||
&& row.publication_status == CustomWorldPublicationStatus::Published
|
&& row.publication_status == CustomWorldPublicationStatus::Published
|
||||||
&& row.deleted_at.is_none()
|
&& row.deleted_at.is_none()
|
||||||
|
&& row.visible
|
||||||
});
|
});
|
||||||
|
|
||||||
let gallery_entry = ctx
|
let gallery_entry = ctx
|
||||||
@@ -1745,6 +1759,7 @@ fn remix_custom_world_profile_record(
|
|||||||
.filter(|row| {
|
.filter(|row| {
|
||||||
row.publication_status == CustomWorldPublicationStatus::Published
|
row.publication_status == CustomWorldPublicationStatus::Published
|
||||||
&& row.deleted_at.is_none()
|
&& row.deleted_at.is_none()
|
||||||
|
&& row.visible
|
||||||
&& row.published_at.is_some()
|
&& row.published_at.is_some()
|
||||||
})
|
})
|
||||||
.ok_or_else(|| "custom_world 已发布源作品不存在,无法改编".to_string())?;
|
.ok_or_else(|| "custom_world 已发布源作品不存在,无法改编".to_string())?;
|
||||||
@@ -1777,6 +1792,7 @@ fn remix_custom_world_profile_record(
|
|||||||
deleted_at: source.deleted_at,
|
deleted_at: source.deleted_at,
|
||||||
created_at: source.created_at,
|
created_at: source.created_at,
|
||||||
updated_at: remixed_at,
|
updated_at: remixed_at,
|
||||||
|
visible: source.visible,
|
||||||
};
|
};
|
||||||
let updated_source = ctx.db.custom_world_profile().insert(next_source);
|
let updated_source = ctx.db.custom_world_profile().insert(next_source);
|
||||||
let source_gallery = sync_custom_world_gallery_entry_from_profile(ctx, &updated_source)?;
|
let source_gallery = sync_custom_world_gallery_entry_from_profile(ctx, &updated_source)?;
|
||||||
@@ -1805,6 +1821,7 @@ fn remix_custom_world_profile_record(
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: remixed_at,
|
created_at: remixed_at,
|
||||||
updated_at: remixed_at,
|
updated_at: remixed_at,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(existing_target) = ctx
|
if let Some(existing_target) = ctx
|
||||||
@@ -1845,6 +1862,7 @@ fn record_custom_world_profile_play_record(
|
|||||||
.filter(|row| {
|
.filter(|row| {
|
||||||
row.publication_status == CustomWorldPublicationStatus::Published
|
row.publication_status == CustomWorldPublicationStatus::Published
|
||||||
&& row.deleted_at.is_none()
|
&& row.deleted_at.is_none()
|
||||||
|
&& row.visible
|
||||||
&& row.published_at.is_some()
|
&& row.published_at.is_some()
|
||||||
})
|
})
|
||||||
.ok_or_else(|| "custom_world 已发布作品不存在,无法记录游玩".to_string())?;
|
.ok_or_else(|| "custom_world 已发布作品不存在,无法记录游玩".to_string())?;
|
||||||
@@ -1887,6 +1905,7 @@ fn record_custom_world_profile_play_record(
|
|||||||
deleted_at: existing.deleted_at,
|
deleted_at: existing.deleted_at,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: played_at,
|
updated_at: played_at,
|
||||||
|
visible: existing.visible,
|
||||||
};
|
};
|
||||||
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
||||||
let gallery_entry = sync_custom_world_gallery_entry_from_profile(ctx, &inserted)?;
|
let gallery_entry = sync_custom_world_gallery_entry_from_profile(ctx, &inserted)?;
|
||||||
@@ -1916,6 +1935,7 @@ fn record_custom_world_profile_like_record(
|
|||||||
.filter(|row| {
|
.filter(|row| {
|
||||||
row.publication_status == CustomWorldPublicationStatus::Published
|
row.publication_status == CustomWorldPublicationStatus::Published
|
||||||
&& row.deleted_at.is_none()
|
&& row.deleted_at.is_none()
|
||||||
|
&& row.visible
|
||||||
&& row.published_at.is_some()
|
&& row.published_at.is_some()
|
||||||
})
|
})
|
||||||
.ok_or_else(|| "custom_world 已发布作品不存在,无法点赞".to_string())?;
|
.ok_or_else(|| "custom_world 已发布作品不存在,无法点赞".to_string())?;
|
||||||
@@ -1967,6 +1987,7 @@ fn record_custom_world_profile_like_record(
|
|||||||
deleted_at: existing.deleted_at,
|
deleted_at: existing.deleted_at,
|
||||||
created_at: existing.created_at,
|
created_at: existing.created_at,
|
||||||
updated_at: liked_at,
|
updated_at: liked_at,
|
||||||
|
visible: existing.visible,
|
||||||
};
|
};
|
||||||
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
let inserted = ctx.db.custom_world_profile().insert(next_row);
|
||||||
let gallery_entry = sync_custom_world_gallery_entry_from_profile(ctx, &inserted)?;
|
let gallery_entry = sync_custom_world_gallery_entry_from_profile(ctx, &inserted)?;
|
||||||
@@ -2582,6 +2603,7 @@ fn is_same_agent_draft_profile_candidate(
|
|||||||
) -> bool {
|
) -> bool {
|
||||||
row.owner_user_id == owner_user_id
|
row.owner_user_id == owner_user_id
|
||||||
&& row.deleted_at.is_none()
|
&& row.deleted_at.is_none()
|
||||||
|
&& row.visible
|
||||||
&& row.publication_status == CustomWorldPublicationStatus::Draft
|
&& row.publication_status == CustomWorldPublicationStatus::Draft
|
||||||
&& row.source_agent_session_id.as_deref() == Some(source_agent_session_id)
|
&& row.source_agent_session_id.as_deref() == Some(source_agent_session_id)
|
||||||
}
|
}
|
||||||
@@ -4841,6 +4863,7 @@ fn sync_custom_world_gallery_entry_from_profile(
|
|||||||
like_count: profile.like_count,
|
like_count: profile.like_count,
|
||||||
published_at,
|
published_at,
|
||||||
updated_at: profile.updated_at,
|
updated_at: profile.updated_at,
|
||||||
|
visible: profile.visible,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted = ctx.db.custom_world_gallery_entry().insert(row);
|
let inserted = ctx.db.custom_world_gallery_entry().insert(row);
|
||||||
@@ -4854,7 +4877,7 @@ fn sync_missing_custom_world_gallery_entries(ctx: &ReducerContext) -> Result<(),
|
|||||||
.custom_world_profile()
|
.custom_world_profile()
|
||||||
.by_custom_world_profile_publication_status()
|
.by_custom_world_profile_publication_status()
|
||||||
.filter(CustomWorldPublicationStatus::Published)
|
.filter(CustomWorldPublicationStatus::Published)
|
||||||
.filter(|profile| profile.deleted_at.is_none())
|
.filter(|profile| profile.deleted_at.is_none() && profile.visible)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for profile in published_profiles {
|
for profile in published_profiles {
|
||||||
@@ -4926,6 +4949,7 @@ fn ensure_custom_world_profile_public_fields(
|
|||||||
deleted_at: profile.deleted_at,
|
deleted_at: profile.deleted_at,
|
||||||
created_at: profile.created_at,
|
created_at: profile.created_at,
|
||||||
updated_at: profile.updated_at,
|
updated_at: profile.updated_at,
|
||||||
|
visible: profile.visible,
|
||||||
};
|
};
|
||||||
|
|
||||||
ctx.db.custom_world_profile().insert(next_row)
|
ctx.db.custom_world_profile().insert(next_row)
|
||||||
@@ -4955,6 +4979,7 @@ fn build_custom_world_profile_row_copy(profile: &CustomWorldProfile) -> CustomWo
|
|||||||
deleted_at: profile.deleted_at,
|
deleted_at: profile.deleted_at,
|
||||||
created_at: profile.created_at,
|
created_at: profile.created_at,
|
||||||
updated_at: profile.updated_at,
|
updated_at: profile.updated_at,
|
||||||
|
visible: profile.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4997,7 +5022,7 @@ pub(crate) fn custom_world_public_profile_snapshots(
|
|||||||
.custom_world_profile()
|
.custom_world_profile()
|
||||||
.by_custom_world_profile_publication_status()
|
.by_custom_world_profile_publication_status()
|
||||||
.filter(CustomWorldPublicationStatus::Published)
|
.filter(CustomWorldPublicationStatus::Published)
|
||||||
.filter(|row| row.deleted_at.is_none())
|
.filter(|row| row.deleted_at.is_none() && row.visible)
|
||||||
.map(|row| build_custom_world_profile_snapshot(&row))
|
.map(|row| build_custom_world_profile_snapshot(&row))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
@@ -5156,6 +5181,7 @@ pub(crate) fn custom_world_public_gallery_snapshots(
|
|||||||
.custom_world_gallery_entry()
|
.custom_world_gallery_entry()
|
||||||
.by_custom_world_gallery_owner_user_id()
|
.by_custom_world_gallery_owner_user_id()
|
||||||
.filter(""..)
|
.filter(""..)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
build_custom_world_gallery_entry_snapshot_with_recent_counts(&row, &HashMap::new())
|
build_custom_world_gallery_entry_snapshot_with_recent_counts(&row, &HashMap::new())
|
||||||
})
|
})
|
||||||
@@ -5377,6 +5403,7 @@ mod tests {
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
let deleted = CustomWorldProfile {
|
let deleted = CustomWorldProfile {
|
||||||
profile_id: "profile-1".to_string(),
|
profile_id: "profile-1".to_string(),
|
||||||
@@ -5401,6 +5428,7 @@ mod tests {
|
|||||||
deleted_at: Some(Timestamp::from_micros_since_unix_epoch(2)),
|
deleted_at: Some(Timestamp::from_micros_since_unix_epoch(2)),
|
||||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
let published = CustomWorldProfile {
|
let published = CustomWorldProfile {
|
||||||
profile_id: "profile-1".to_string(),
|
profile_id: "profile-1".to_string(),
|
||||||
@@ -5425,6 +5453,7 @@ mod tests {
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert!(is_same_agent_draft_profile_candidate(
|
assert!(is_same_agent_draft_profile_candidate(
|
||||||
@@ -5552,6 +5581,7 @@ mod tests {
|
|||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
created_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
let mut active_agent_session_ids = HashSet::new();
|
let mut active_agent_session_ids = HashSet::new();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub fn jump_hop_gallery_view(ctx: &AnonymousViewContext) -> Vec<JumpHopGalleryVi
|
|||||||
.jump_hop_work_profile()
|
.jump_hop_work_profile()
|
||||||
.by_jump_hop_work_publication_status()
|
.by_jump_hop_work_publication_status()
|
||||||
.filter(JUMP_HOP_PUBLICATION_PUBLISHED)
|
.filter(JUMP_HOP_PUBLICATION_PUBLISHED)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -401,6 +402,7 @@ fn compile_jump_hop_draft_tx(
|
|||||||
play_count: 0,
|
play_count: 0,
|
||||||
updated_at: compiled_at,
|
updated_at: compiled_at,
|
||||||
published_at: None,
|
published_at: None,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
upsert_work(ctx, row);
|
upsert_work(ctx, row);
|
||||||
replace_session(
|
replace_session(
|
||||||
@@ -1163,6 +1165,7 @@ fn clone_work(row: &JumpHopWorkProfileRow) -> JumpHopWorkProfileRow {
|
|||||||
play_count: row.play_count,
|
play_count: row.play_count,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = jump_hop_agent_session,
|
accessor = jump_hop_agent_session,
|
||||||
index(accessor = by_jump_hop_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
index(accessor = by_jump_hop_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||||
@@ -51,6 +53,9 @@ pub struct JumpHopWorkProfileRow {
|
|||||||
pub(crate) play_count: u32,
|
pub(crate) play_count: u32,
|
||||||
pub(crate) updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
pub(crate) published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub fn match3d_gallery_view(ctx: &AnonymousViewContext) -> Vec<Match3DGalleryVie
|
|||||||
.match3d_work_profile()
|
.match3d_work_profile()
|
||||||
.by_match3d_work_publication_status()
|
.by_match3d_work_publication_status()
|
||||||
.filter(MATCH3D_PUBLICATION_PUBLISHED)
|
.filter(MATCH3D_PUBLICATION_PUBLISHED)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -571,6 +572,7 @@ fn compile_match3d_draft_tx(
|
|||||||
updated_at: compiled_at,
|
updated_at: compiled_at,
|
||||||
published_at: previous_published_at,
|
published_at: previous_published_at,
|
||||||
generated_item_assets_json,
|
generated_item_assets_json,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
upsert_work(ctx, work);
|
upsert_work(ctx, work);
|
||||||
replace_session(
|
replace_session(
|
||||||
@@ -643,6 +645,7 @@ fn build_updated_match3d_work_row(
|
|||||||
updated_at,
|
updated_at,
|
||||||
published_at: current.published_at,
|
published_at: current.published_at,
|
||||||
generated_item_assets_json: current.generated_item_assets_json.clone(),
|
generated_item_assets_json: current.generated_item_assets_json.clone(),
|
||||||
|
visible: current.visible,
|
||||||
};
|
};
|
||||||
Ok(next)
|
Ok(next)
|
||||||
}
|
}
|
||||||
@@ -1330,6 +1333,7 @@ fn clone_work(row: &Match3DWorkProfileRow) -> Match3DWorkProfileRow {
|
|||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
generated_item_assets_json: row.generated_item_assets_json.clone(),
|
generated_item_assets_json: row.generated_item_assets_json.clone(),
|
||||||
|
visible: row.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1885,6 +1889,7 @@ mod tests {
|
|||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
published_at: None,
|
published_at: None,
|
||||||
generated_item_assets_json: None,
|
generated_item_assets_json: None,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
let snapshot = build_initial_run_snapshot("run-1", &work, 10, None);
|
let snapshot = build_initial_run_snapshot("run-1", &work, 10, None);
|
||||||
assert_eq!(snapshot.total_item_count, 12);
|
assert_eq!(snapshot.total_item_count, 12);
|
||||||
@@ -1924,6 +1929,7 @@ mod tests {
|
|||||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let snapshot = build_work_snapshot(&work).expect("work snapshot should build");
|
let snapshot = build_work_snapshot(&work).expect("work snapshot should build");
|
||||||
@@ -1969,6 +1975,7 @@ mod tests {
|
|||||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let preserved =
|
let preserved =
|
||||||
@@ -2038,6 +2045,7 @@ mod tests {
|
|||||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"#
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
let input = Match3DWorkUpdateInput {
|
let input = Match3DWorkUpdateInput {
|
||||||
profile_id: existing.profile_id.clone(),
|
profile_id: existing.profile_id.clone(),
|
||||||
@@ -2097,6 +2105,7 @@ mod tests {
|
|||||||
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"},{"itemId":"match3d-item-2","itemName":"苹果","imageViews":[{"imageSrc":"/v1.png"},{"imageSrc":"/v2.png"},{"imageSrc":"/v3.png"},{"imageSrc":"/v4.png"},{"imageSrc":"/v5.png"}],"status":"model_ready"},{"itemId":"match3d-item-3","itemName":"香蕉","imageViews":[{"imageSrc":"/v1.png"},{"imageSrc":"/v2.png"},{"imageSrc":"/v3.png"},{"imageSrc":"/v4.png"}],"status":"image_ready"}]"#
|
r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"},{"itemId":"match3d-item-2","itemName":"苹果","imageViews":[{"imageSrc":"/v1.png"},{"imageSrc":"/v2.png"},{"imageSrc":"/v3.png"},{"imageSrc":"/v4.png"},{"imageSrc":"/v5.png"}],"status":"model_ready"},{"itemId":"match3d-item-3","itemName":"香蕉","imageViews":[{"imageSrc":"/v1.png"},{"imageSrc":"/v2.png"},{"imageSrc":"/v3.png"},{"imageSrc":"/v4.png"}],"status":"image_ready"}]"#
|
||||||
.to_string(),
|
.to_string(),
|
||||||
),
|
),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let error = validate_publishable_work(&base_work).unwrap_err();
|
let error = validate_publishable_work(&base_work).unwrap_err();
|
||||||
@@ -2156,6 +2165,7 @@ mod tests {
|
|||||||
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
updated_at: Timestamp::from_micros_since_unix_epoch(1),
|
||||||
published_at: None,
|
published_at: None,
|
||||||
generated_item_assets_json: None,
|
generated_item_assets_json: None,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_game_name = None;
|
let input_game_name = None;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = match3d_agent_session,
|
accessor = match3d_agent_session,
|
||||||
index(accessor = by_match3d_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
index(accessor = by_match3d_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||||
@@ -60,6 +62,9 @@ pub struct Match3DWorkProfileRow {
|
|||||||
pub(crate) published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
#[default(None::<String>)]
|
#[default(None::<String>)]
|
||||||
pub(crate) generated_item_assets_json: Option<String>,
|
pub(crate) generated_item_assets_json: Option<String>,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
|
|||||||
@@ -1243,6 +1243,10 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
|||||||
}
|
}
|
||||||
if table_name == "custom_world_profile" || table_name == "custom_world_gallery_entry" {
|
if table_name == "custom_world_profile" || table_name == "custom_world_gallery_entry" {
|
||||||
if let Some(object) = next_value.as_object_mut() {
|
if let Some(object) = next_value.as_object_mut() {
|
||||||
|
// 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。
|
||||||
|
object
|
||||||
|
.entry("visible".to_string())
|
||||||
|
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||||
// 中文注释:自定义世界公开互动计数字段晚于基础作品表加入,旧迁移包按 0 兼容。
|
// 中文注释:自定义世界公开互动计数字段晚于基础作品表加入,旧迁移包按 0 兼容。
|
||||||
object
|
object
|
||||||
.entry("play_count".to_string())
|
.entry("play_count".to_string())
|
||||||
@@ -1257,6 +1261,10 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
|||||||
}
|
}
|
||||||
if table_name == "puzzle_work_profile" {
|
if table_name == "puzzle_work_profile" {
|
||||||
if let Some(object) = next_value.as_object_mut() {
|
if let Some(object) = next_value.as_object_mut() {
|
||||||
|
// 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。
|
||||||
|
object
|
||||||
|
.entry("visible".to_string())
|
||||||
|
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||||
// 中文注释:拼图公开互动计数晚于基础作品表加入,旧迁移包按 0 兼容。
|
// 中文注释:拼图公开互动计数晚于基础作品表加入,旧迁移包按 0 兼容。
|
||||||
object
|
object
|
||||||
.entry("play_count".to_string())
|
.entry("play_count".to_string())
|
||||||
@@ -1294,8 +1302,34 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
|||||||
.or_insert(fallback_description);
|
.or_insert(fallback_description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if table_name == "big_fish_creation_session" {
|
||||||
|
if let Some(object) = next_value.as_object_mut() {
|
||||||
|
// 中文注释:作品可见性字段晚于大鱼吃小鱼创作会话表加入,旧迁移包按默认显示兼容。
|
||||||
|
object
|
||||||
|
.entry("visible".to_string())
|
||||||
|
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
table_name,
|
||||||
|
"jump_hop_work_profile"
|
||||||
|
| "square_hole_work_profile"
|
||||||
|
| "visual_novel_work_profile"
|
||||||
|
| "bark_battle_published_config"
|
||||||
|
) {
|
||||||
|
if let Some(object) = next_value.as_object_mut() {
|
||||||
|
// 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。
|
||||||
|
object
|
||||||
|
.entry("visible".to_string())
|
||||||
|
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||||
|
}
|
||||||
|
}
|
||||||
if table_name == "match3d_work_profile" {
|
if table_name == "match3d_work_profile" {
|
||||||
if let Some(object) = next_value.as_object_mut() {
|
if let Some(object) = next_value.as_object_mut() {
|
||||||
|
// 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。
|
||||||
|
object
|
||||||
|
.entry("visible".to_string())
|
||||||
|
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||||
// 中文注释:抓大鹅生成素材字段晚于基础作品表加入,旧迁移包按未生成素材兼容。
|
// 中文注释:抓大鹅生成素材字段晚于基础作品表加入,旧迁移包按未生成素材兼容。
|
||||||
object
|
object
|
||||||
.entry("generated_item_assets_json".to_string())
|
.entry("generated_item_assets_json".to_string())
|
||||||
@@ -1304,6 +1338,10 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
|||||||
}
|
}
|
||||||
if table_name == "wooden_fish_work_profile" {
|
if table_name == "wooden_fish_work_profile" {
|
||||||
if let Some(object) = next_value.as_object_mut() {
|
if let Some(object) = next_value.as_object_mut() {
|
||||||
|
// 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。
|
||||||
|
object
|
||||||
|
.entry("visible".to_string())
|
||||||
|
.or_insert_with(|| serde_json::Value::Bool(true));
|
||||||
// 中文注释:敲木鱼背景环境图晚于首版作品表加入,旧迁移包按未生成背景兼容。
|
// 中文注释:敲木鱼背景环境图晚于首版作品表加入,旧迁移包按未生成背景兼容。
|
||||||
object
|
object
|
||||||
.entry("background_asset_json".to_string())
|
.entry("background_asset_json".to_string())
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ use spacetimedb::{
|
|||||||
use crate::auth::user_account;
|
use crate::auth::user_account;
|
||||||
|
|
||||||
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
|
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
/// 拼图 Agent session 真相表。
|
/// 拼图 Agent session 真相表。
|
||||||
/// 当前只保存结构化字段与 JSON 草稿,不提前拆出更多编辑态子表。
|
/// 当前只保存结构化字段与 JSON 草稿,不提前拆出更多编辑态子表。
|
||||||
@@ -84,34 +85,37 @@ pub struct PuzzleAgentMessageRow {
|
|||||||
)]
|
)]
|
||||||
pub struct PuzzleWorkProfileRow {
|
pub struct PuzzleWorkProfileRow {
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
profile_id: String,
|
pub(crate) profile_id: String,
|
||||||
work_id: String,
|
pub(crate) work_id: String,
|
||||||
owner_user_id: String,
|
pub(crate) owner_user_id: String,
|
||||||
source_session_id: Option<String>,
|
pub(crate) source_session_id: Option<String>,
|
||||||
author_display_name: String,
|
pub(crate) author_display_name: String,
|
||||||
work_title: String,
|
pub(crate) work_title: String,
|
||||||
work_description: String,
|
pub(crate) work_description: String,
|
||||||
level_name: String,
|
pub(crate) level_name: String,
|
||||||
summary: String,
|
pub(crate) summary: String,
|
||||||
theme_tags_json: String,
|
pub(crate) theme_tags_json: String,
|
||||||
cover_image_src: Option<String>,
|
pub(crate) cover_image_src: Option<String>,
|
||||||
cover_asset_id: Option<String>,
|
pub(crate) cover_asset_id: Option<String>,
|
||||||
levels_json: String,
|
pub(crate) levels_json: String,
|
||||||
publication_status: PuzzlePublicationStatus,
|
pub(crate) publication_status: PuzzlePublicationStatus,
|
||||||
play_count: u32,
|
pub(crate) play_count: u32,
|
||||||
anchor_pack_json: String,
|
pub(crate) anchor_pack_json: String,
|
||||||
publish_ready: bool,
|
pub(crate) publish_ready: bool,
|
||||||
created_at: Timestamp,
|
pub(crate) created_at: Timestamp,
|
||||||
updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
remix_count: u32,
|
pub(crate) remix_count: u32,
|
||||||
#[default(0)]
|
#[default(0)]
|
||||||
like_count: u32,
|
pub(crate) like_count: u32,
|
||||||
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
||||||
point_incentive_total_half_points: u64,
|
pub(crate) point_incentive_total_half_points: u64,
|
||||||
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
||||||
point_incentive_claimed_points: u64,
|
pub(crate) point_incentive_claimed_points: u64,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 拼图广场公开详情兼容投影。
|
/// 拼图广场公开详情兼容投影。
|
||||||
@@ -125,6 +129,7 @@ pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec<PuzzleWorkProfile>
|
|||||||
.puzzle_work_profile()
|
.puzzle_work_profile()
|
||||||
.by_puzzle_work_publication_status()
|
.by_puzzle_work_publication_status()
|
||||||
.filter(PuzzlePublicationStatus::Published)
|
.filter(PuzzlePublicationStatus::Published)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(
|
.filter_map(
|
||||||
|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
|
|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
|
||||||
Ok(profile) => Some(profile),
|
Ok(profile) => Some(profile),
|
||||||
@@ -154,6 +159,7 @@ pub fn puzzle_gallery_card_view(ctx: &AnonymousViewContext) -> Vec<PuzzleGallery
|
|||||||
.puzzle_work_profile()
|
.puzzle_work_profile()
|
||||||
.by_puzzle_work_publication_status()
|
.by_puzzle_work_publication_status()
|
||||||
.filter(PuzzlePublicationStatus::Published)
|
.filter(PuzzlePublicationStatus::Published)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_puzzle_gallery_card_view_row(&row) {
|
.filter_map(|row| match build_puzzle_gallery_card_view_row(&row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -1578,6 +1584,7 @@ fn update_puzzle_work_tx(
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros),
|
updated_at: Timestamp::from_micros_since_unix_epoch(input.updated_at_micros),
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
};
|
};
|
||||||
replace_puzzle_work_profile(ctx, &row, next_row);
|
replace_puzzle_work_profile(ctx, &row, next_row);
|
||||||
sync_puzzle_source_session_draft_from_work(ctx, &row, &preview_draft, input.updated_at_micros)?;
|
sync_puzzle_source_session_draft_from_work(ctx, &row, &preview_draft, input.updated_at_micros)?;
|
||||||
@@ -1790,6 +1797,7 @@ fn record_puzzle_work_like_tx(
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: liked_at,
|
updated_at: liked_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
};
|
};
|
||||||
replace_puzzle_work_profile(ctx, &row, next_row);
|
replace_puzzle_work_profile(ctx, &row, next_row);
|
||||||
ctx.db
|
ctx.db
|
||||||
@@ -1878,6 +1886,7 @@ fn remix_puzzle_work_tx(
|
|||||||
created_at: source.created_at,
|
created_at: source.created_at,
|
||||||
updated_at: remixed_at,
|
updated_at: remixed_at,
|
||||||
published_at: source.published_at,
|
published_at: source.published_at,
|
||||||
|
visible: source.visible,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1945,6 +1954,7 @@ fn remix_puzzle_work_tx(
|
|||||||
created_at: remixed_at,
|
created_at: remixed_at,
|
||||||
updated_at: remixed_at,
|
updated_at: remixed_at,
|
||||||
published_at: None,
|
published_at: None,
|
||||||
|
visible: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
get_puzzle_agent_session_tx(
|
get_puzzle_agent_session_tx(
|
||||||
@@ -2396,6 +2406,7 @@ fn claim_puzzle_work_point_incentive_tx(
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: claimed_at,
|
updated_at: claimed_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
};
|
};
|
||||||
replace_puzzle_work_profile(ctx, &row, next_row);
|
replace_puzzle_work_profile(ctx, &row, next_row);
|
||||||
|
|
||||||
@@ -3008,6 +3019,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
|||||||
published_at: profile
|
published_at: profile
|
||||||
.published_at_micros
|
.published_at_micros
|
||||||
.map(Timestamp::from_micros_since_unix_epoch),
|
.map(Timestamp::from_micros_since_unix_epoch),
|
||||||
|
visible: existing.visible,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -3040,6 +3052,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
|
|||||||
published_at: profile
|
published_at: profile
|
||||||
.published_at_micros
|
.published_at_micros
|
||||||
.map(Timestamp::from_micros_since_unix_epoch),
|
.map(Timestamp::from_micros_since_unix_epoch),
|
||||||
|
visible: true,
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -3364,6 +3377,7 @@ fn accrue_puzzle_point_incentive(
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -3402,6 +3416,7 @@ fn increment_puzzle_profile_play_count(
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod analytics_date_dimension;
|
pub mod analytics_date_dimension;
|
||||||
|
mod admin_work_visibility;
|
||||||
mod browse_history;
|
mod browse_history;
|
||||||
pub mod creation_entry_config;
|
pub mod creation_entry_config;
|
||||||
mod profile;
|
mod profile;
|
||||||
@@ -6,6 +7,7 @@ mod settings;
|
|||||||
mod snapshots;
|
mod snapshots;
|
||||||
|
|
||||||
pub use analytics_date_dimension::*;
|
pub use analytics_date_dimension::*;
|
||||||
|
pub use admin_work_visibility::*;
|
||||||
pub use browse_history::*;
|
pub use browse_history::*;
|
||||||
pub use creation_entry_config::*;
|
pub use creation_entry_config::*;
|
||||||
pub use profile::*;
|
pub use profile::*;
|
||||||
|
|||||||
@@ -0,0 +1,708 @@
|
|||||||
|
use crate::*;
|
||||||
|
use crate::puzzle::{PuzzleWorkProfileRow, puzzle_work_profile};
|
||||||
|
use module_custom_world::CustomWorldPublicationStatus;
|
||||||
|
use module_puzzle::PuzzlePublicationStatus;
|
||||||
|
|
||||||
|
const SOURCE_TYPE_PUZZLE: &str = "puzzle";
|
||||||
|
const SOURCE_TYPE_CUSTOM_WORLD: &str = "custom-world";
|
||||||
|
const SOURCE_TYPE_JUMP_HOP: &str = "jump-hop";
|
||||||
|
const SOURCE_TYPE_WOODEN_FISH: &str = "wooden-fish";
|
||||||
|
const SOURCE_TYPE_MATCH3D: &str = "match3d";
|
||||||
|
const SOURCE_TYPE_SQUARE_HOLE: &str = "square-hole";
|
||||||
|
const SOURCE_TYPE_VISUAL_NOVEL: &str = "visual-novel";
|
||||||
|
const SOURCE_TYPE_BIG_FISH: &str = "big-fish";
|
||||||
|
const SOURCE_TYPE_BARK_BATTLE: &str = "bark-battle";
|
||||||
|
|
||||||
|
/// 后台作品可见性列表。
|
||||||
|
///
|
||||||
|
/// 中文注释:后台必须能看到 hidden 作品,不能复用 public_work_* view,否则隐藏后无法恢复。
|
||||||
|
#[spacetimedb::procedure]
|
||||||
|
pub fn admin_list_work_visibility(
|
||||||
|
ctx: &mut ProcedureContext,
|
||||||
|
input: AdminWorkVisibilityListInput,
|
||||||
|
) -> AdminWorkVisibilityListProcedureResult {
|
||||||
|
match ctx.try_with_tx(|tx| list_work_visibility_tx(tx, input.clone())) {
|
||||||
|
Ok(entries) => AdminWorkVisibilityListProcedureResult {
|
||||||
|
ok: true,
|
||||||
|
entries,
|
||||||
|
error_message: None,
|
||||||
|
},
|
||||||
|
Err(message) => AdminWorkVisibilityListProcedureResult {
|
||||||
|
ok: false,
|
||||||
|
entries: Vec::new(),
|
||||||
|
error_message: Some(message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 后台修改单个作品可见性。
|
||||||
|
#[spacetimedb::procedure]
|
||||||
|
pub fn admin_update_work_visibility(
|
||||||
|
ctx: &mut ProcedureContext,
|
||||||
|
input: AdminWorkVisibilityUpdateInput,
|
||||||
|
) -> AdminWorkVisibilityProcedureResult {
|
||||||
|
match ctx.try_with_tx(|tx| update_work_visibility_tx(tx, input.clone())) {
|
||||||
|
Ok(record) => AdminWorkVisibilityProcedureResult {
|
||||||
|
ok: true,
|
||||||
|
record: Some(record),
|
||||||
|
error_message: None,
|
||||||
|
},
|
||||||
|
Err(message) => AdminWorkVisibilityProcedureResult {
|
||||||
|
ok: false,
|
||||||
|
record: None,
|
||||||
|
error_message: Some(message),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_work_visibility_tx(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: AdminWorkVisibilityListInput,
|
||||||
|
) -> Result<Vec<AdminWorkVisibilitySnapshot>, String> {
|
||||||
|
require_admin_user_id(&input.admin_user_id)?;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
entries.extend(list_puzzle_work_visibility(ctx));
|
||||||
|
entries.extend(list_custom_world_work_visibility(ctx));
|
||||||
|
entries.extend(list_jump_hop_work_visibility(ctx));
|
||||||
|
entries.extend(list_wooden_fish_work_visibility(ctx));
|
||||||
|
entries.extend(list_match3d_work_visibility(ctx));
|
||||||
|
entries.extend(list_square_hole_work_visibility(ctx));
|
||||||
|
entries.extend(list_visual_novel_work_visibility(ctx));
|
||||||
|
entries.extend(list_big_fish_work_visibility(ctx));
|
||||||
|
entries.extend(list_bark_battle_work_visibility(ctx));
|
||||||
|
sort_work_visibility_entries(&mut entries);
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_work_visibility_tx(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
input: AdminWorkVisibilityUpdateInput,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
require_admin_user_id(&input.admin_user_id)?;
|
||||||
|
let source_type = normalize_source_type(&input.source_type)?;
|
||||||
|
let profile_id = normalize_required_text(&input.profile_id, "profileId")?;
|
||||||
|
|
||||||
|
match source_type.as_str() {
|
||||||
|
SOURCE_TYPE_PUZZLE => update_puzzle_work_visibility(ctx, &profile_id, input.visible),
|
||||||
|
SOURCE_TYPE_CUSTOM_WORLD => {
|
||||||
|
update_custom_world_work_visibility(ctx, &profile_id, input.visible)
|
||||||
|
}
|
||||||
|
SOURCE_TYPE_JUMP_HOP => update_jump_hop_work_visibility(ctx, &profile_id, input.visible),
|
||||||
|
SOURCE_TYPE_WOODEN_FISH => {
|
||||||
|
update_wooden_fish_work_visibility(ctx, &profile_id, input.visible)
|
||||||
|
}
|
||||||
|
SOURCE_TYPE_MATCH3D => update_match3d_work_visibility(ctx, &profile_id, input.visible),
|
||||||
|
SOURCE_TYPE_SQUARE_HOLE => update_square_hole_work_visibility(ctx, &profile_id, input.visible),
|
||||||
|
SOURCE_TYPE_VISUAL_NOVEL => {
|
||||||
|
update_visual_novel_work_visibility(ctx, &profile_id, input.visible)
|
||||||
|
}
|
||||||
|
SOURCE_TYPE_BIG_FISH => update_big_fish_work_visibility(ctx, &profile_id, input.visible),
|
||||||
|
SOURCE_TYPE_BARK_BATTLE => {
|
||||||
|
update_bark_battle_work_visibility(ctx, &profile_id, input.visible)
|
||||||
|
}
|
||||||
|
_ => Err(format!("不支持的作品类型:{source_type}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_puzzle_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.puzzle_work_profile()
|
||||||
|
.by_puzzle_work_publication_status()
|
||||||
|
.filter(PuzzlePublicationStatus::Published)
|
||||||
|
.map(|row| puzzle_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_puzzle_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.puzzle_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "拼图作品不存在".to_string())?;
|
||||||
|
if row.publication_status != PuzzlePublicationStatus::Published {
|
||||||
|
return Err("只能修改已发布拼图作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = PuzzleWorkProfileRow { visible, ..row };
|
||||||
|
ctx.db
|
||||||
|
.puzzle_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&next.profile_id);
|
||||||
|
ctx.db.puzzle_work_profile().insert(next);
|
||||||
|
let updated = ctx
|
||||||
|
.db
|
||||||
|
.puzzle_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "拼图作品可见性更新失败".to_string())?;
|
||||||
|
Ok(puzzle_work_visibility_snapshot(&updated))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn puzzle_work_visibility_snapshot(row: &PuzzleWorkProfileRow) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_PUZZLE.to_string(),
|
||||||
|
work_id: row.work_id.clone(),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: row.source_session_id.clone(),
|
||||||
|
public_work_code: build_prefixed_public_work_code("PZ", &row.profile_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: choose_non_empty(&[row.work_title.as_str(), row.level_name.as_str()]),
|
||||||
|
subtitle: "拼图关卡".to_string(),
|
||||||
|
cover_image_src: row.cover_image_src.clone(),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_custom_world_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.custom_world_profile()
|
||||||
|
.by_custom_world_profile_publication_status()
|
||||||
|
.filter(CustomWorldPublicationStatus::Published)
|
||||||
|
.filter(|row| row.deleted_at.is_none())
|
||||||
|
.map(|row| custom_world_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_custom_world_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.custom_world_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "自定义世界作品不存在".to_string())?;
|
||||||
|
if row.publication_status != CustomWorldPublicationStatus::Published || row.deleted_at.is_some()
|
||||||
|
{
|
||||||
|
return Err("只能修改已发布自定义世界作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = CustomWorldProfile { visible, ..row };
|
||||||
|
let snapshot = custom_world_work_visibility_snapshot(&next);
|
||||||
|
let profile_id = next.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.custom_world_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&profile_id);
|
||||||
|
ctx.db.custom_world_profile().insert(next);
|
||||||
|
|
||||||
|
if let Some(gallery) = ctx
|
||||||
|
.db
|
||||||
|
.custom_world_gallery_entry()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id)
|
||||||
|
{
|
||||||
|
let next_gallery = CustomWorldGalleryEntry { visible, ..gallery };
|
||||||
|
let gallery_profile_id = next_gallery.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.custom_world_gallery_entry()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&gallery_profile_id);
|
||||||
|
ctx.db.custom_world_gallery_entry().insert(next_gallery);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn custom_world_work_visibility_snapshot(row: &CustomWorldProfile) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let public_work_code = row
|
||||||
|
.public_work_code
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| build_custom_world_public_work_code(&row.profile_id));
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_CUSTOM_WORLD.to_string(),
|
||||||
|
work_id: format!("custom-world:{}", row.profile_id),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: row.source_agent_session_id.clone(),
|
||||||
|
public_work_code,
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: row.world_name.clone(),
|
||||||
|
subtitle: row.subtitle.clone(),
|
||||||
|
cover_image_src: row.cover_image_src.clone(),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_jump_hop_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.jump_hop_work_profile()
|
||||||
|
.by_jump_hop_work_publication_status()
|
||||||
|
.filter(JUMP_HOP_PUBLICATION_PUBLISHED)
|
||||||
|
.map(|row| jump_hop_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_jump_hop_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.jump_hop_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "跳一跳作品不存在".to_string())?;
|
||||||
|
if row.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED {
|
||||||
|
return Err("只能修改已发布跳一跳作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = JumpHopWorkProfileRow { visible, ..row };
|
||||||
|
let snapshot = jump_hop_work_visibility_snapshot(&next);
|
||||||
|
let profile_id = next.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.jump_hop_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&profile_id);
|
||||||
|
ctx.db.jump_hop_work_profile().insert(next);
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jump_hop_work_visibility_snapshot(row: &JumpHopWorkProfileRow) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_JUMP_HOP.to_string(),
|
||||||
|
work_id: row.work_id.clone(),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||||
|
public_work_code: build_prefixed_public_work_code("JH", &row.profile_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: row.work_title.clone(),
|
||||||
|
subtitle: "跳一跳".to_string(),
|
||||||
|
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_wooden_fish_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.wooden_fish_work_profile()
|
||||||
|
.by_wooden_fish_work_publication_status()
|
||||||
|
.filter(WOODEN_FISH_PUBLICATION_PUBLISHED)
|
||||||
|
.map(|row| wooden_fish_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_wooden_fish_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.wooden_fish_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "敲木鱼作品不存在".to_string())?;
|
||||||
|
if row.publication_status != WOODEN_FISH_PUBLICATION_PUBLISHED {
|
||||||
|
return Err("只能修改已发布敲木鱼作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = WoodenFishWorkProfileRow { visible, ..row };
|
||||||
|
let snapshot = wooden_fish_work_visibility_snapshot(&next);
|
||||||
|
let profile_id = next.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.wooden_fish_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&profile_id);
|
||||||
|
ctx.db.wooden_fish_work_profile().insert(next);
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wooden_fish_work_visibility_snapshot(
|
||||||
|
row: &WoodenFishWorkProfileRow,
|
||||||
|
) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_WOODEN_FISH.to_string(),
|
||||||
|
work_id: row.work_id.clone(),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||||
|
public_work_code: build_prefixed_public_work_code("WF", &row.profile_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: row.work_title.clone(),
|
||||||
|
subtitle: "敲木鱼".to_string(),
|
||||||
|
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_match3d_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.match3d_work_profile()
|
||||||
|
.by_match3d_work_publication_status()
|
||||||
|
.filter(MATCH3D_PUBLICATION_PUBLISHED)
|
||||||
|
.map(|row| match3d_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_match3d_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.match3d_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "抓大鹅作品不存在".to_string())?;
|
||||||
|
if row.publication_status != MATCH3D_PUBLICATION_PUBLISHED {
|
||||||
|
return Err("只能修改已发布抓大鹅作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = Match3DWorkProfileRow { visible, ..row };
|
||||||
|
let snapshot = match3d_work_visibility_snapshot(&next);
|
||||||
|
let profile_id = next.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.match3d_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&profile_id);
|
||||||
|
ctx.db.match3d_work_profile().insert(next);
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match3d_work_visibility_snapshot(row: &Match3DWorkProfileRow) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_MATCH3D.to_string(),
|
||||||
|
work_id: row.profile_id.clone(),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||||
|
public_work_code: build_prefixed_public_work_code("M3", &row.profile_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: row.game_name.clone(),
|
||||||
|
subtitle: "抓大鹅".to_string(),
|
||||||
|
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_square_hole_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.square_hole_work_profile()
|
||||||
|
.by_square_hole_work_publication_status()
|
||||||
|
.filter(SQUARE_HOLE_PUBLICATION_PUBLISHED)
|
||||||
|
.map(|row| square_hole_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_square_hole_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.square_hole_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "方洞挑战作品不存在".to_string())?;
|
||||||
|
if row.publication_status != SQUARE_HOLE_PUBLICATION_PUBLISHED {
|
||||||
|
return Err("只能修改已发布方洞挑战作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = SquareHoleWorkProfileRow { visible, ..row };
|
||||||
|
let snapshot = square_hole_work_visibility_snapshot(&next);
|
||||||
|
let profile_id = next.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.square_hole_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&profile_id);
|
||||||
|
ctx.db.square_hole_work_profile().insert(next);
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn square_hole_work_visibility_snapshot(
|
||||||
|
row: &SquareHoleWorkProfileRow,
|
||||||
|
) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_SQUARE_HOLE.to_string(),
|
||||||
|
work_id: row.work_id.clone(),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||||
|
public_work_code: build_prefixed_public_work_code("SH", &row.profile_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: row.game_name.clone(),
|
||||||
|
subtitle: "方洞挑战".to_string(),
|
||||||
|
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_visual_novel_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.visual_novel_work_profile()
|
||||||
|
.by_visual_novel_work_publication_status()
|
||||||
|
.filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED)
|
||||||
|
.map(|row| visual_novel_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_visual_novel_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.visual_novel_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "视觉小说作品不存在".to_string())?;
|
||||||
|
if row.publication_status != VISUAL_NOVEL_PUBLICATION_PUBLISHED {
|
||||||
|
return Err("只能修改已发布视觉小说作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = VisualNovelWorkProfileRow { visible, ..row };
|
||||||
|
let snapshot = visual_novel_work_visibility_snapshot(&next);
|
||||||
|
let profile_id = next.profile_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.visual_novel_work_profile()
|
||||||
|
.profile_id()
|
||||||
|
.delete(&profile_id);
|
||||||
|
ctx.db.visual_novel_work_profile().insert(next);
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visual_novel_work_visibility_snapshot(
|
||||||
|
row: &VisualNovelWorkProfileRow,
|
||||||
|
) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_VISUAL_NOVEL.to_string(),
|
||||||
|
work_id: row.work_id.clone(),
|
||||||
|
profile_id: row.profile_id.clone(),
|
||||||
|
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||||
|
public_work_code: build_prefixed_public_work_code("VN", &row.profile_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: row.author_display_name.clone(),
|
||||||
|
title: row.work_title.clone(),
|
||||||
|
subtitle: "视觉小说".to_string(),
|
||||||
|
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: sort_time,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_big_fish_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.big_fish_creation_session()
|
||||||
|
.by_big_fish_session_stage()
|
||||||
|
.filter(BigFishCreationStage::Published)
|
||||||
|
.map(|row| big_fish_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_big_fish_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.big_fish_creation_session()
|
||||||
|
.session_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "大鱼吃小鱼作品不存在".to_string())?;
|
||||||
|
if row.stage != BigFishCreationStage::Published {
|
||||||
|
return Err("只能修改已发布大鱼吃小鱼作品可见性".to_string());
|
||||||
|
}
|
||||||
|
let next = BigFishCreationSession { visible, ..row };
|
||||||
|
let snapshot = big_fish_work_visibility_snapshot(&next);
|
||||||
|
let session_id = next.session_id.clone();
|
||||||
|
ctx.db
|
||||||
|
.big_fish_creation_session()
|
||||||
|
.session_id()
|
||||||
|
.delete(&session_id);
|
||||||
|
ctx.db.big_fish_creation_session().insert(next);
|
||||||
|
Ok(snapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn big_fish_work_visibility_snapshot(
|
||||||
|
row: &BigFishCreationSession,
|
||||||
|
) -> AdminWorkVisibilitySnapshot {
|
||||||
|
let published_at = row.published_at.map(|value| value.to_micros_since_unix_epoch());
|
||||||
|
let updated_at = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_BIG_FISH.to_string(),
|
||||||
|
work_id: row.session_id.clone(),
|
||||||
|
profile_id: row.session_id.clone(),
|
||||||
|
source_session_id: Some(row.session_id.clone()),
|
||||||
|
public_work_code: build_prefixed_public_work_code("BF", &row.session_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: "玩家".to_string(),
|
||||||
|
title: "大鱼吃小鱼".to_string(),
|
||||||
|
subtitle: "成长挑战".to_string(),
|
||||||
|
cover_image_src: None,
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: published_at,
|
||||||
|
updated_at_micros: updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_bark_battle_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||||
|
ctx.db
|
||||||
|
.bark_battle_published_config()
|
||||||
|
.iter()
|
||||||
|
.map(|row| bark_battle_work_visibility_snapshot(&row))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_bark_battle_work_visibility(
|
||||||
|
ctx: &ReducerContext,
|
||||||
|
profile_id: &str,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||||
|
let row = ctx
|
||||||
|
.db
|
||||||
|
.bark_battle_published_config()
|
||||||
|
.work_id()
|
||||||
|
.find(&profile_id.to_string())
|
||||||
|
.ok_or_else(|| "汪汪声浪作品不存在".to_string())?;
|
||||||
|
let next = BarkBattlePublishedConfigRow { visible, ..row };
|
||||||
|
ctx.db.bark_battle_published_config().delete(next.clone());
|
||||||
|
ctx.db.bark_battle_published_config().insert(next.clone());
|
||||||
|
Ok(bark_battle_work_visibility_snapshot(&next))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bark_battle_work_visibility_snapshot(
|
||||||
|
row: &BarkBattlePublishedConfigRow,
|
||||||
|
) -> AdminWorkVisibilitySnapshot {
|
||||||
|
AdminWorkVisibilitySnapshot {
|
||||||
|
source_type: SOURCE_TYPE_BARK_BATTLE.to_string(),
|
||||||
|
work_id: row.work_id.clone(),
|
||||||
|
profile_id: row.work_id.clone(),
|
||||||
|
source_session_id: row.source_draft_id.clone(),
|
||||||
|
public_work_code: build_bark_battle_public_work_code(&row.work_id),
|
||||||
|
owner_user_id: row.owner_user_id.clone(),
|
||||||
|
author_display_name: "玩家".to_string(),
|
||||||
|
title: "汪汪声浪".to_string(),
|
||||||
|
subtitle: row.difficulty_preset.clone(),
|
||||||
|
cover_image_src: None,
|
||||||
|
visible: row.visible,
|
||||||
|
published_at_micros: Some(row.published_at.to_micros_since_unix_epoch()),
|
||||||
|
updated_at_micros: timestamp_sort_micros(Some(row.published_at), row.updated_at),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_admin_user_id(value: &str) -> Result<(), String> {
|
||||||
|
normalize_required_text(value, "adminUserId").map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_source_type(value: &str) -> Result<String, String> {
|
||||||
|
let normalized = normalize_required_text(value, "sourceType")?
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
.replace('_', "-");
|
||||||
|
let source_type = match normalized.as_str() {
|
||||||
|
"match-3-d" | "match-3d" | "match3-d" => SOURCE_TYPE_MATCH3D,
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
Ok(source_type.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_required_text(value: &str, field_name: &str) -> Result<String, String> {
|
||||||
|
let normalized = value.trim();
|
||||||
|
if normalized.is_empty() {
|
||||||
|
return Err(format!("{field_name} 不能为空"));
|
||||||
|
}
|
||||||
|
Ok(normalized.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sort_work_visibility_entries(entries: &mut [AdminWorkVisibilitySnapshot]) {
|
||||||
|
entries.sort_by(|left, right| {
|
||||||
|
right
|
||||||
|
.updated_at_micros
|
||||||
|
.cmp(&left.updated_at_micros)
|
||||||
|
.then_with(|| left.source_type.cmp(&right.source_type))
|
||||||
|
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_sort_micros(published_at: Option<Timestamp>, updated_at: Timestamp) -> i64 {
|
||||||
|
published_at
|
||||||
|
.unwrap_or(updated_at)
|
||||||
|
.to_micros_since_unix_epoch()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_prefixed_public_work_code(prefix: &str, value: &str) -> String {
|
||||||
|
let normalized = normalize_public_code_text(value);
|
||||||
|
let fallback = if normalized.is_empty() {
|
||||||
|
"00000000".to_string()
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
};
|
||||||
|
let suffix = last_eight_padded(&fallback);
|
||||||
|
format!("{prefix}-{suffix}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_bark_battle_public_work_code(work_id: &str) -> String {
|
||||||
|
let normalized = normalize_public_code_text(work_id);
|
||||||
|
let without_prefix = normalized
|
||||||
|
.strip_prefix("BB")
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.unwrap_or_else(|| normalized.clone());
|
||||||
|
let fallback = if without_prefix.is_empty() {
|
||||||
|
if normalized.is_empty() {
|
||||||
|
"00000000".to_string()
|
||||||
|
} else {
|
||||||
|
normalized
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
without_prefix
|
||||||
|
};
|
||||||
|
format!("BB-{}", last_eight_padded(&fallback))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_public_code_text(value: &str) -> String {
|
||||||
|
value
|
||||||
|
.trim()
|
||||||
|
.chars()
|
||||||
|
.filter(|character| character.is_ascii_alphanumeric())
|
||||||
|
.flat_map(char::to_uppercase)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn last_eight_padded(value: &str) -> String {
|
||||||
|
let suffix = value
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take(8)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect::<String>();
|
||||||
|
format!("{suffix:0>8}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn choose_non_empty(values: &[&str]) -> String {
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|value| value.trim())
|
||||||
|
.find(|value| !value.is_empty())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
@@ -39,6 +39,7 @@ pub fn square_hole_gallery_view(ctx: &AnonymousViewContext) -> Vec<SquareHoleGal
|
|||||||
.square_hole_work_profile()
|
.square_hole_work_profile()
|
||||||
.by_square_hole_work_publication_status()
|
.by_square_hole_work_publication_status()
|
||||||
.filter(SQUARE_HOLE_PUBLICATION_PUBLISHED)
|
.filter(SQUARE_HOLE_PUBLICATION_PUBLISHED)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -537,6 +538,7 @@ fn compile_square_hole_draft_tx(
|
|||||||
play_count: 0,
|
play_count: 0,
|
||||||
updated_at: compiled_at,
|
updated_at: compiled_at,
|
||||||
published_at: None,
|
published_at: None,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
upsert_work(ctx, work);
|
upsert_work(ctx, work);
|
||||||
replace_session(
|
replace_session(
|
||||||
@@ -614,6 +616,7 @@ fn update_square_hole_work_tx(
|
|||||||
play_count: current.play_count,
|
play_count: current.play_count,
|
||||||
updated_at,
|
updated_at,
|
||||||
published_at: current.published_at,
|
published_at: current.published_at,
|
||||||
|
visible: current.visible,
|
||||||
};
|
};
|
||||||
let snapshot = build_work_snapshot(&next)?;
|
let snapshot = build_work_snapshot(&next)?;
|
||||||
replace_work(ctx, ¤t, next);
|
replace_work(ctx, ¤t, next);
|
||||||
@@ -1141,6 +1144,7 @@ fn clone_work(row: &SquareHoleWorkProfileRow) -> SquareHoleWorkProfileRow {
|
|||||||
play_count: row.play_count,
|
play_count: row.play_count,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = square_hole_agent_session,
|
accessor = square_hole_agent_session,
|
||||||
index(accessor = by_square_hole_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
index(accessor = by_square_hole_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||||
@@ -59,6 +61,9 @@ pub struct SquareHoleWorkProfileRow {
|
|||||||
pub(crate) play_count: u32,
|
pub(crate) play_count: u32,
|
||||||
pub(crate) updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
pub(crate) published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
use serde::Serialize;
|
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use spacetimedb::AnonymousViewContext;
|
use spacetimedb::AnonymousViewContext;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
pub const VISUAL_NOVEL_SOURCE_IDEA: &str = "idea";
|
pub const VISUAL_NOVEL_SOURCE_IDEA: &str = "idea";
|
||||||
pub const VISUAL_NOVEL_SOURCE_DOCUMENT: &str = "document";
|
pub const VISUAL_NOVEL_SOURCE_DOCUMENT: &str = "document";
|
||||||
pub const VISUAL_NOVEL_SOURCE_BLANK: &str = "blank";
|
pub const VISUAL_NOVEL_SOURCE_BLANK: &str = "blank";
|
||||||
@@ -94,6 +95,9 @@ pub struct VisualNovelWorkProfileRow {
|
|||||||
pub(crate) created_at: Timestamp,
|
pub(crate) created_at: Timestamp,
|
||||||
pub(crate) updated_at: Timestamp,
|
pub(crate) updated_at: Timestamp,
|
||||||
pub(crate) published_at: Option<Timestamp>,
|
pub(crate) published_at: Option<Timestamp>,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 视觉小说运行态 run 表。
|
/// 视觉小说运行态 run 表。
|
||||||
@@ -178,6 +182,7 @@ pub fn visual_novel_gallery_view(ctx: &AnonymousViewContext) -> Vec<VisualNovelG
|
|||||||
.visual_novel_work_profile()
|
.visual_novel_work_profile()
|
||||||
.by_visual_novel_work_publication_status()
|
.by_visual_novel_work_publication_status()
|
||||||
.filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED)
|
.filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -421,13 +426,13 @@ pub struct VisualNovelRuntimeEventProcedureResult {
|
|||||||
pub error_message: Option<String>,
|
pub error_message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
pub struct VisualNovelJsonField {
|
pub struct VisualNovelJsonField {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
pub value: VisualNovelJsonValue,
|
pub value: VisualNovelJsonValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
pub enum VisualNovelJsonValue {
|
pub enum VisualNovelJsonValue {
|
||||||
Null,
|
Null,
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
@@ -437,7 +442,7 @@ pub enum VisualNovelJsonValue {
|
|||||||
Object(Vec<VisualNovelJsonField>),
|
Object(Vec<VisualNovelJsonField>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VisualNovelAgentMessageSnapshot {
|
pub struct VisualNovelAgentMessageSnapshot {
|
||||||
pub message_id: String,
|
pub message_id: String,
|
||||||
@@ -448,7 +453,7 @@ pub struct VisualNovelAgentMessageSnapshot {
|
|||||||
pub created_at_micros: i64,
|
pub created_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VisualNovelAgentSessionSnapshot {
|
pub struct VisualNovelAgentSessionSnapshot {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
@@ -468,7 +473,7 @@ pub struct VisualNovelAgentSessionSnapshot {
|
|||||||
pub updated_at_micros: i64,
|
pub updated_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VisualNovelWorkSnapshot {
|
pub struct VisualNovelWorkSnapshot {
|
||||||
pub work_id: String,
|
pub work_id: String,
|
||||||
@@ -490,7 +495,7 @@ pub struct VisualNovelWorkSnapshot {
|
|||||||
pub published_at_micros: Option<i64>,
|
pub published_at_micros: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VisualNovelRuntimeHistoryEntrySnapshot {
|
pub struct VisualNovelRuntimeHistoryEntrySnapshot {
|
||||||
pub entry_id: String,
|
pub entry_id: String,
|
||||||
@@ -506,7 +511,7 @@ pub struct VisualNovelRuntimeHistoryEntrySnapshot {
|
|||||||
pub created_at_micros: i64,
|
pub created_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VisualNovelRunSnapshot {
|
pub struct VisualNovelRunSnapshot {
|
||||||
pub run_id: String,
|
pub run_id: String,
|
||||||
@@ -526,7 +531,7 @@ pub struct VisualNovelRunSnapshot {
|
|||||||
pub updated_at_micros: i64,
|
pub updated_at_micros: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)]
|
#[derive(Clone, Debug, PartialEq, serde::Serialize, SpacetimeType)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct VisualNovelRuntimeEventSnapshot {
|
pub struct VisualNovelRuntimeEventSnapshot {
|
||||||
pub event_id: String,
|
pub event_id: String,
|
||||||
@@ -1029,6 +1034,7 @@ fn compile_visual_novel_work_profile_tx(
|
|||||||
created_at: compiled_at,
|
created_at: compiled_at,
|
||||||
updated_at: compiled_at,
|
updated_at: compiled_at,
|
||||||
published_at: None,
|
published_at: None,
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
upsert_work(ctx, work);
|
upsert_work(ctx, work);
|
||||||
replace_session(
|
replace_session(
|
||||||
@@ -1731,6 +1737,7 @@ fn clone_work(row: &VisualNovelWorkProfileRow) -> VisualNovelWorkProfileRow {
|
|||||||
created_at: row.created_at,
|
created_at: row.created_at,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1971,7 +1978,7 @@ fn parse_json<T: DeserializeOwned>(value: &str, label: &str) -> Result<T, String
|
|||||||
serde_json::from_str(value).map_err(|error| format!("{label} 非法: {error}"))
|
serde_json::from_str(value).map_err(|error| format!("{label} 非法: {error}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_json_string<T: Serialize>(value: &T) -> String {
|
fn to_json_string<T: serde::Serialize>(value: &T) -> String {
|
||||||
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
|
serde_json::to_string(value).unwrap_or_else(|_| "{}".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub fn wooden_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec<WoodenFishGal
|
|||||||
.wooden_fish_work_profile()
|
.wooden_fish_work_profile()
|
||||||
.by_wooden_fish_work_publication_status()
|
.by_wooden_fish_work_publication_status()
|
||||||
.filter(WOODEN_FISH_PUBLICATION_PUBLISHED)
|
.filter(WOODEN_FISH_PUBLICATION_PUBLISHED)
|
||||||
|
.filter(|row| row.visible)
|
||||||
.filter_map(|row| match build_gallery_view_row(&row) {
|
.filter_map(|row| match build_gallery_view_row(&row) {
|
||||||
Ok(item) => Some(item),
|
Ok(item) => Some(item),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -412,6 +413,7 @@ fn compile_wooden_fish_draft_tx(
|
|||||||
published_at: None,
|
published_at: None,
|
||||||
background_asset_json: background_asset.as_ref().map(to_json_string),
|
background_asset_json: background_asset.as_ref().map(to_json_string),
|
||||||
back_button_asset_json: back_button_asset.as_ref().map(to_json_string),
|
back_button_asset_json: back_button_asset.as_ref().map(to_json_string),
|
||||||
|
visible: true,
|
||||||
};
|
};
|
||||||
upsert_work(ctx, row);
|
upsert_work(ctx, row);
|
||||||
let config = config_from_draft(&draft);
|
let config = config_from_draft(&draft);
|
||||||
@@ -1313,6 +1315,7 @@ fn clone_work(row: &WoodenFishWorkProfileRow) -> WoodenFishWorkProfileRow {
|
|||||||
play_count: row.play_count,
|
play_count: row.play_count,
|
||||||
updated_at: row.updated_at,
|
updated_at: row.updated_at,
|
||||||
published_at: row.published_at,
|
published_at: row.published_at,
|
||||||
|
visible: row.visible,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
|
const WORK_VISIBLE_DEFAULT: bool = true;
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
accessor = wooden_fish_agent_session,
|
accessor = wooden_fish_agent_session,
|
||||||
index(accessor = by_wooden_fish_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
index(accessor = by_wooden_fish_agent_session_owner_user_id, btree(columns = [owner_user_id]))
|
||||||
@@ -49,6 +51,9 @@ pub struct WoodenFishWorkProfileRow {
|
|||||||
pub(crate) background_asset_json: Option<String>,
|
pub(crate) background_asset_json: Option<String>,
|
||||||
#[default(None::<String>)]
|
#[default(None::<String>)]
|
||||||
pub(crate) back_button_asset_json: Option<String>,
|
pub(crate) back_button_asset_json: Option<String>,
|
||||||
|
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||||
|
#[default(WORK_VISIBLE_DEFAULT)]
|
||||||
|
pub(crate) visible: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[spacetimedb::table(
|
#[spacetimedb::table(
|
||||||
|
|||||||
Reference in New Issue
Block a user