feat(account): studio 个人中心补齐市场记录三件套之三「我的发布」+ e2e

P3 真缺口补全(6/7):PersonalCenter 此前只有购买记录+授权记录,缺发布记录(账户台账视角)——补齐三件套对称。useAccountPublishRecords→GET /account/publish-records(AppPublishRecordRespVO:reviewStatus/marketStatus/blockingReason);PersonalCenter 加「我的发布」区。

查重结论:不与 market 模块 useMyPublishRecords 重复——后者是发布工作流操作台(草稿/提交/申诉,my-publish-records),本接口是账户中心历史台账(account/publish-records,member 模块,与购买/授权并列)。

验证:vitest account 3/3(加 publish-records URL+reviewStatus 契约)、tsc 0 错;新增 e2e account-publish-records.spec.ts 真后端绿(GET 200+「我的发布」渲染),全量 29/29 无回归。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-20 20:36:20 -07:00
parent 9c84c05e62
commit 3dff0e1735
4 changed files with 139 additions and 1 deletions

View File

@ -0,0 +1,37 @@
import { expect, test, type Page } from '@playwright/test';
/**
* 账户中心「我的发布」记录 e2e(真实后端,MSW off)。
*
* 前置:vite VITE_API_MOCK=false、真实 muse-server 48080、muse_slice_live(market-publish 旅程已落发布记录)。
* 目的:个人中心补齐市场记录三件套之三——发布记录(账户台账视角,GET /account/publish-records),
* 验真后端读通 + 「我的发布」区渲染(反假绿)。不依赖记录条数,故只断言契约与区块存在。
*/
const TOKEN = 'test1';
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
window.localStorage.setItem('accessToken', t);
window.localStorage.setItem('tenantId', '1');
}, TOKEN);
}
test('账户中心展示我的发布记录(真实后端)', async ({ page }) => {
await seedToken(page);
// 进个人中心,捕获真实发布记录读响应(打真后端,非 mock)。
const [resp] = await Promise.all([
page.waitForResponse(
(r) => /\/account\/publish-records/.test(r.url()) && r.request().method() === 'GET',
{ timeout: 15_000 }
),
page.goto('/account'),
]);
// Yudao 口径:成功为 HTTP 200 + CommonResult{code:0}。
expect(resp.status()).toBe(200);
expect((await resp.json()).code).toBe(0);
// 「我的发布」区(市场记录三件套之三:购买/授权/发布)渲染。
await expect(page.getByRole('heading', { name: '我的发布' })).toBeVisible({ timeout: 15_000 });
});

View File

@ -3,6 +3,7 @@ import {
useAccountEntitlements,
useAccountLicenses,
useAccountProfile,
useAccountPublishRecords,
useAccountPurchases,
useAccountSecurityEvents,
useAccountUsage,
@ -25,6 +26,7 @@ export default function PersonalCenter() {
const { data: usage } = useAccountUsage('month');
const { data: purchases } = useAccountPurchases();
const { data: licenses } = useAccountLicenses();
const { data: publishRecords } = useAccountPublishRecords();
const { data: securityEvents } = useAccountSecurityEvents();
const acknowledgeEvent = useAcknowledgeSecurityEvent();
const updateProfile = useUpdateAccountProfile();
@ -164,6 +166,35 @@ export default function PersonalCenter() {
</section>
</div>
<section className="mt-6 rounded-3xl border border-slate-100 bg-white p-6 shadow-sm">
<h2 className="text-base font-bold text-slate-800">我的发布</h2>
{(publishRecords?.list?.length ?? 0) === 0 ? (
<p className="mt-3 text-sm text-slate-400">暂无发布记录</p>
) : (
<ul className="mt-3 divide-y divide-slate-50">
{publishRecords?.list?.map((rec) => (
<li key={rec.recordId} className="flex items-center justify-between gap-3 py-3">
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="rounded-md bg-slate-100 px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider text-slate-500">
{rec.assetType}
</span>
<strong className="truncate text-sm font-semibold text-slate-800">{rec.assetName}</strong>
</div>
<div className="mt-1 text-xs text-slate-400">
市场状态:{rec.marketStatus ?? '—'}
{rec.blockingReason ? ` · 阻塞:${rec.blockingReason}` : ''}
</div>
</div>
<span className="shrink-0 rounded-full bg-amber-50 px-2.5 py-1 text-[11px] font-bold text-amber-700">
{rec.reviewStatus}
</span>
</li>
))}
</ul>
)}
</section>
<section className="mt-6 rounded-3xl border border-slate-100 bg-white p-6 shadow-sm">
<h2 className="text-base font-bold text-slate-800">安全事件</h2>
{(securityEvents?.list?.length ?? 0) === 0 ? (

View File

@ -1,9 +1,17 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { http, HttpResponse } from 'msw';
import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import React from 'react';
import { resetMockDb } from '@/api/mocks/handlers/account';
import { useAccountEntitlements, useAccountProfile, useAccountUsage, useUpdateAccountProfile } from './useAccount';
import { server } from '@/test/setup';
import {
useAccountEntitlements,
useAccountProfile,
useAccountPublishRecords,
useAccountUsage,
useUpdateAccountProfile,
} from './useAccount';
const createTestQueryClient = () =>
new QueryClient({
@ -52,4 +60,38 @@ describe('useAccount API Hooks 单元测试', () => {
expect(entitlementHook.current.data?.planName).toBe('Muse Pro');
expect(usageHook.current.data?.totalInputTokens).toBeGreaterThan(0);
});
it('读账户发布记录(市场记录三件套之三)', async () => {
const urls: string[] = [];
server.use(
http.get('/app-api/muse/account/publish-records', ({ request }) => {
urls.push(request.url);
return HttpResponse.json({
code: 0,
msg: '',
data: {
total: 1,
pageNo: 1,
pageSize: 50,
list: [
{
recordId: 'pr-1',
assetType: 'agent',
assetName: '发布资产',
version: '1',
reviewStatus: 'submitted',
marketStatus: 'not_listed',
},
],
},
});
})
);
const { result } = renderHook(() => useAccountPublishRecords(), { wrapper });
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(result.current.data?.list[0]?.reviewStatus).toBe('submitted');
// 契约:命中账户发布记录端点(账户台账视角,异于 market my-publish-records)。
expect(urls[0]).toContain('/app-api/muse/account/publish-records');
});
});

View File

@ -96,6 +96,34 @@ export function useAccountLicenses() {
});
}
/** 账户发布记录条目(对齐后端 AppPublishRecordRespVO 关键字段;账户台账视角,区别于 market 发布工作流的 my-publish-records)。 */
export interface AccountPublishRecord {
recordId: string;
assetType: string;
assetName: string;
version?: string;
/** 审核态:submitted/approved/rejected… */
reviewStatus: string;
/** 市场态:not_listed/listed/delisted… */
marketStatus?: string;
blockingReason?: string | null;
submittedAt?: number;
lastUpdatedAt?: number;
}
/**
* 获取我的发布记录(账户中心市场记录三件套之三,补齐购买/授权之外的发布台账)。
* 与 market 模块 useMyPublishRecords(发布工作流操作台:草稿/提交/申诉)不同——本接口是账户视角的发布历史台账(含 reviewStatus/marketStatus)。
*/
export function useAccountPublishRecords() {
return useQuery<AccountRecordsPage<AccountPublishRecord>, Error>({
queryKey: ['accountPublishRecords'],
queryFn: () =>
api.get<AccountRecordsPage<AccountPublishRecord>>('/account/publish-records?pageNo=1&pageSize=50'),
staleTime: 30_000,
});
}
/** 账户安全事件摘要条目(对齐后端 SecurityEventSummaryRespVO)。 */
export interface AccountSecurityEvent {
eventId: string;