feat(studio): 个人中心渲染真实购买/授权记录(account 深页活体切片)

承 account facade 活体订正(单体内 /account/{purchases,licenses,publish-records} 经 market 跨模块
MarketAccountProjectionProvider 本就可用),把已确认可用的后端接进 UI:
- PersonalCenter 新增「我的购买 / 我的授权」两区,渲染 market→account 投影真实记录;
- useAccount 加 useAccountPurchases / useAccountLicenses(GET /account/{purchases,licenses})。
- live-read.spec.ts 加第 5 例(chromium, MSW off, 真连 48080):/account 渲染真实资产「活体市场资产·测试」。

验证:整套 e2e 12/12、vitest 47/47、tsc 绿。进度总账 §五C 个人中心就地更新。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-15 03:11:20 -07:00
parent 4433c96d7d
commit c7c65a97f2
4 changed files with 123 additions and 2 deletions

View File

@ -125,7 +125,7 @@
- ✅ **content/market/account/ai-agent 读旅程(rendered UI 活体证,2026-06-14)**:`live-read.spec.ts` 4/4 绿(MSW off,chromium:作品列表/市场浏览/个人中心/智能体列表均渲染真实后端数据)——证 tenant-id 修复后 studio 直连活体单体跨 BC 取数、真实数据形态与页面渲染契约对齐。
- 🔧 知识库工作台:**✅ 确认草稿(先审后入)rendered-UI 活体证(2026-06-14)**——`KnowledgeDraftPanel`(/knowledge/:workId)列待确认草稿+「确认入库」→ 真 confirm 物化 Canonical 实体(`knowledge-confirm.spec.ts` chromium MSW-off 绿,DB 证 draft confirmed+entity_id);后端补 `SummaryRespVO` 暴露 confirm 并发/源令牌。**仍 ⬜**:bindings/publish/graph UI;**遗留 bug**:confirm 对已存在同名实体抛 500(应干净 conflict,已 flag task_497da70f)。
- 🔧 市场生产侧:**✅ 发布草稿 + 提交审核全链(创作者飞轮)rendered-UI 活体证(2026-06-15)**——`MarketPublish`(/market/publish):①「保存发布草稿」(`POST /marketplace/publish-drafts`)→`my-publish-records` 真实回读;②「提交审核」做 save→运行检查(`POST .../checks`)→检查通过提交申请(`POST /publish-requests`)全链(licenseType+权利声明为检查硬门槛)。`market-publish.spec.ts` chromium MSW-off **3/3 绿**(草稿正 / 缺标题负 / 提交全链),DB 证:`muse_market_publish_draft`=draft、`muse_market_publish_request`=submitted、`muse_market_review_event`=submitted 落库;草稿测幂等自包含、全链测唯一名可重复跑。MarketBrowse 加「我要发布资产」入口。**附带修真实潜伏后端 bug**:publish draft/check/request/review_event 的 (tenant_id,command_id) 为**部分**唯一索引(V15 `WHERE command_id IS NOT NULL`),mapper insertIgnore 却发无谓词 `ON CONFLICT (tenant_id,command_id)`→PG 无法用部分索引作仲裁器→save/check/submit 500(链路从未活体跑过故潜伏);修=部分→完整唯一索引对齐 command/purchase 约定(`V22__fix_market_publish_command_unique_index.sql` 4 表 + muse_slice_live 已应用,零 Java 改动/零重启)。**仍 ⬜**:上架(审核通过后)/申诉 UI;**遗留**:handoff/appeal/appeal_event/appeal_material/authorization_summary 等 market 表同为部分 command 索引、存同类潜伏 ON CONFLICT 不一致,待其流程活体时收口。
- ⬜ 个人中心:21/33 端无 UI(security-events/exports/downloads/licenses/purchases/new-api-binding)。
- 🔧 个人中心:**✅ profile/用量/权益 + 我的购买/我的授权(rendered-UI 活体证,2026-06-15)**——PersonalCenter 新增「我的购买/我的授权」渲染 market→account 投影真实记录(`useAccountPurchases`/`useAccountLicenses`→`/account/{purchases,licenses}`;`live-read.spec.ts` 第 5 例 chromium MSW-off 绿,断言真实资产「活体市场资产·测试」渲染——证后端 account 读端在单体可用即被 UI 消费,见 §五A 活体订正)。**仍 ⬜**:security-events / exports / downloads / new-api-binding 深页(其中 exports/downloads/new-api 后端为**正确 fail-closed 外部依赖**,非接线缺口,见 §五A)。
- 收口口径:每切片"关 MSW→真连活体单体→playwright e2e 绿"才算完成(反假绿)。
### D. gateway 接线(B4,**判定:单体部署范围外**)

View File

@ -42,4 +42,11 @@ test.describe('活体只读旅程(真实后端,MSW off)', () => {
await page.goto('/agents');
await expect(page.getByText('活体测试智能体').first()).toBeVisible({ timeout: 15_000 });
});
test('个人中心渲染真实购买/授权记录(account 深页)', async ({ page }) => {
await seedToken(page);
await page.goto('/account');
// 个人中心「我的购买 / 我的授权」渲染 market→account 投影的真实资产(后端经 market 跨模块 Facade 提供,curl 已证)。
await expect(page.getByText('活体市场资产·测试').first()).toBeVisible({ timeout: 15_000 });
});
});

View File

@ -1,5 +1,12 @@
import { useState } from 'react';
import { useAccountEntitlements, useAccountProfile, useAccountUsage, useUpdateAccountProfile } from '../hooks/useAccount';
import {
useAccountEntitlements,
useAccountLicenses,
useAccountProfile,
useAccountPurchases,
useAccountUsage,
useUpdateAccountProfile,
} from '../hooks/useAccount';
import UsageStats from './UsageStats';
/** 个人中心首页,聚合资料、用量、权益和账户状态。 */
@ -7,6 +14,8 @@ export default function PersonalCenter() {
const { data: profile, isLoading: isProfileLoading } = useAccountProfile();
const { data: entitlements } = useAccountEntitlements();
const { data: usage } = useAccountUsage('month');
const { data: purchases } = useAccountPurchases();
const { data: licenses } = useAccountLicenses();
const updateProfile = useUpdateAccountProfile();
const [nickname, setNickname] = useState('');
const [publicPenName, setPublicPenName] = useState('');
@ -87,6 +96,62 @@ export default function PersonalCenter() {
<div className="mt-6">
<UsageStats usage={usage} entitlements={entitlements} />
</div>
<div className="mt-6 grid grid-cols-1 gap-6 lg:grid-cols-2">
<section className="rounded-3xl border border-slate-100 bg-white p-6 shadow-sm">
<h2 className="text-base font-bold text-slate-800">我的购买</h2>
{(purchases?.list?.length ?? 0) === 0 ? (
<p className="mt-3 text-sm text-slate-400">暂无购买记录</p>
) : (
<ul className="mt-3 divide-y divide-slate-50">
{purchases?.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.authorizationResultSummary || rec.status}</div>
</div>
<span className="shrink-0 rounded-full bg-emerald-50 px-2.5 py-1 text-[11px] font-bold text-emerald-700">
{rec.status}
</span>
</li>
))}
</ul>
)}
</section>
<section className="rounded-3xl border border-slate-100 bg-white p-6 shadow-sm">
<h2 className="text-base font-bold text-slate-800">我的授权</h2>
{(licenses?.list?.length ?? 0) === 0 ? (
<p className="mt-3 text-sm text-slate-400">暂无授权记录</p>
) : (
<ul className="mt-3 divide-y divide-slate-50">
{licenses?.list?.map((rec) => (
<li key={rec.licenseId} 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.installSummary || rec.licenseStatus}{rec.publisherName ? ` · ${rec.publisherName}` : ''}
</div>
</div>
<span className="shrink-0 rounded-full bg-indigo-50 px-2.5 py-1 text-[11px] font-bold text-indigo-700">
{rec.licenseStatus}
</span>
</li>
))}
</ul>
)}
</section>
</div>
</div>
</div>
);

View File

@ -46,3 +46,52 @@ export function useAccountUsage(period: UsagePeriod = 'month') {
staleTime: 30_000,
});
}
/** 账户购买记录条目(对齐后端 AppPurchaseRecordRespVO 关键字段)。 */
export interface AccountPurchaseRecord {
recordId: string;
assetType: string;
assetName: string;
status: string;
amount: number | null;
purchaseType?: string;
authorizationResultSummary?: string;
createdAt?: number;
}
/** 账户授权记录条目(对齐后端 AppLicenseRecordRespVO 关键字段)。 */
export interface AccountLicenseRecord {
licenseId: string;
assetType: string;
assetName: string;
licenseStatus: string;
publisherName?: string;
version?: string;
installSummary?: string;
bindingSummary?: string;
}
interface AccountRecordsPage<T> {
total: number;
pageNo: number;
pageSize: number;
list: T[];
}
/** 获取我的市场购买记录(market→account 投影,由 market 跨模块 Facade 提供)。 */
export function useAccountPurchases() {
return useQuery<AccountRecordsPage<AccountPurchaseRecord>, Error>({
queryKey: ['accountPurchases'],
queryFn: () => api.get<AccountRecordsPage<AccountPurchaseRecord>>('/account/purchases?pageNo=1&pageSize=50'),
staleTime: 30_000,
});
}
/** 获取我的资产授权记录。 */
export function useAccountLicenses() {
return useQuery<AccountRecordsPage<AccountLicenseRecord>, Error>({
queryKey: ['accountLicenses'],
queryFn: () => api.get<AccountRecordsPage<AccountLicenseRecord>>('/account/licenses?pageNo=1&pageSize=50'),
staleTime: 30_000,
});
}