根因:UgcWork.status (Integer) 同时承载「乐读派创作进度」和「本地发布状态」, 前端用字符串筛选时无法匹配。 改动: - 新增 V17 迁移脚本:拆分 status 为 VARCHAR + 新增 leai_status INT - 新增 WorkPublishStatus 枚举 (draft/unpublished/pending_review/published/rejected) - 新增 LeaiCreationStatus 常量类 (FAILED~DUBBED) - LeaiSyncService:写入 leaiStatus,CATALOGED 时自动推 status 到 unpublished - 所有公众端 Service:status 直接使用字符串枚举值,删除 Integer 映射 - 新增 Playwright E2E 测试验证 12 个场景全部通过 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
import { test, expect, request as requestFactory, type APIRequestContext } from '@playwright/test'
|
||
|
||
/**
|
||
* UGC 作品状态字段拆分验证测试
|
||
*
|
||
* 所有 API 请求直接发到后端 localhost:8580,绕过前端代理。
|
||
*/
|
||
|
||
// ── 配置 ──
|
||
const API_BASE = process.env.API_BASE_URL || 'http://localhost:8580/api'
|
||
const AUTH = {
|
||
username: process.env.TEST_USERNAME || 'demo',
|
||
password: process.env.TEST_PASSWORD || 'demo123456',
|
||
tenantCode: process.env.TEST_TENANT_CODE || 'gdlib',
|
||
}
|
||
|
||
const VALID_STATUSES = ['draft', 'unpublished', 'pending_review', 'published', 'rejected']
|
||
|
||
// ── Helper ──
|
||
function url(path: string) {
|
||
return `${API_BASE}${path}`
|
||
}
|
||
|
||
// ── Fixture ──
|
||
type Fixtures = { api: APIRequestContext }
|
||
|
||
const apiTest = test.extend<Fixtures>({
|
||
api: async ({}, use) => {
|
||
// 用 requestFactory(顶层 import)创建独立上下文来登录
|
||
const loginCtx = await requestFactory.newContext({})
|
||
const loginResp = await loginCtx.post(url('/public/auth/login'), {
|
||
data: { username: AUTH.username, password: AUTH.password, tenantCode: AUTH.tenantCode },
|
||
})
|
||
const loginJson = await loginResp.json()
|
||
await loginCtx.dispose()
|
||
|
||
if (loginJson.code !== 200 || !loginJson.data?.token) {
|
||
throw new Error(`登录失败: ${JSON.stringify(loginJson)}`)
|
||
}
|
||
const token = loginJson.data.token
|
||
|
||
// 创建带 auth header 的 API 上下文
|
||
const api = await requestFactory.newContext({
|
||
extraHTTPHeaders: { Authorization: `Bearer ${token}` },
|
||
})
|
||
await use(api)
|
||
await api.dispose()
|
||
},
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 1. 我的作品列表 — status 是字符串枚举
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-01 我的作品列表 — status 字段为字符串', async ({ api }) => {
|
||
const resp = await api.get(url('/public/works?page=1&pageSize=5'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
expect(json.data.list).toBeInstanceOf(Array)
|
||
|
||
for (const work of json.data.list) {
|
||
expect(
|
||
VALID_STATUSES.includes(work.status),
|
||
`作品 id=${work.id} 的 status="${work.status}" 不是合法字符串枚举值`,
|
||
).toBe(true)
|
||
}
|
||
|
||
console.log(`✓ S-01: 返回 ${json.data.list.length} 条作品,status 全部为字符串`)
|
||
})
|
||
|
||
apiTest('S-02 我的作品列表 — 按 status=draft 筛选', async ({ api }) => {
|
||
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=draft'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
for (const work of json.data.list) {
|
||
expect(work.status).toBe('draft')
|
||
}
|
||
console.log(`✓ S-02: draft 筛选返回 ${json.data.list.length} 条`)
|
||
})
|
||
|
||
apiTest('S-03 我的作品列表 — 按 status=unpublished 筛选', async ({ api }) => {
|
||
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=unpublished'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
for (const work of json.data.list) {
|
||
expect(work.status).toBe('unpublished')
|
||
}
|
||
console.log(`✓ S-03: unpublished 筛选返回 ${json.data.list.length} 条`)
|
||
})
|
||
|
||
apiTest('S-04 我的作品列表 — 按 status=published 筛选', async ({ api }) => {
|
||
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=published'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
for (const work of json.data.list) {
|
||
expect(work.status).toBe('published')
|
||
}
|
||
console.log(`✓ S-04: published 筛选返回 ${json.data.list.length} 条`)
|
||
})
|
||
|
||
apiTest('S-05 我的作品列表 — 按 status=pending_review 筛选', async ({ api }) => {
|
||
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=pending_review'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
for (const work of json.data.list) {
|
||
expect(work.status).toBe('pending_review')
|
||
}
|
||
console.log(`✓ S-05: pending_review 筛选返回 ${json.data.list.length} 条`)
|
||
})
|
||
|
||
apiTest('S-06 我的作品列表 — 按 status=rejected 筛选', async ({ api }) => {
|
||
const resp = await api.get(url('/public/works?page=1&pageSize=50&status=rejected'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
for (const work of json.data.list) {
|
||
expect(work.status).toBe('rejected')
|
||
}
|
||
console.log(`✓ S-06: rejected 筛选返回 ${json.data.list.length} 条`)
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 2. 创建作品 — 初始 status 为 draft
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-07 创建作品 — 初始 status 为 draft', async ({ api }) => {
|
||
const resp = await api.post(url('/public/works'), {
|
||
data: {
|
||
title: `[E2E] 状态验证_${Date.now()}`,
|
||
description: 'Playwright 自动创建',
|
||
visibility: 'private',
|
||
},
|
||
})
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
expect(json.data.status).toBe('draft')
|
||
expect(typeof json.data.status).toBe('string')
|
||
console.log(`✓ S-07: 创建作品 id=${json.data.id}, status="${json.data.status}"`)
|
||
|
||
// 清理
|
||
if (json.data.id) {
|
||
await api.delete(url(`/public/works/${json.data.id}`))
|
||
}
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 3. draft 不可发布
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-08 发布作品 — draft 状态不可发布', async ({ api }) => {
|
||
const createResp = await api.post(url('/public/works'), {
|
||
data: { title: `[E2E] 发布测试_${Date.now()}`, visibility: 'private' },
|
||
})
|
||
const createJson = await createResp.json()
|
||
expect(createJson.code).toBe(200)
|
||
const workId = createJson.data.id
|
||
|
||
const publishResp = await api.post(url(`/public/works/${workId}/publish`))
|
||
const publishJson = await publishResp.json()
|
||
expect(publishJson.code).toBe(400)
|
||
console.log(`✓ S-08: draft 发布被拒绝,code=${publishJson.code}, msg="${publishJson.message}"`)
|
||
|
||
await api.delete(url(`/public/works/${workId}`))
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 4. 创作历史
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-09 创作历史 — 作品 status 为字符串', async ({ api }) => {
|
||
const resp = await api.get(url('/public/creation/history?page=1&pageSize=5'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
if (json.data?.list?.length > 0) {
|
||
for (const work of json.data.list) {
|
||
expect(
|
||
VALID_STATUSES.includes(work.status),
|
||
`创作历史 id=${work.id} status="${work.status}" 不合法`,
|
||
).toBe(true)
|
||
}
|
||
}
|
||
console.log(`✓ S-09: 创作历史 ${json.data?.list?.length ?? 0} 条`)
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 5. 作品详情
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-10 作品详情 — status + leaiStatus 字段', async ({ api }) => {
|
||
const listResp = await api.get(url('/public/works?page=1&pageSize=1'))
|
||
const listJson = await listResp.json()
|
||
|
||
if (listJson.data?.list?.length > 0) {
|
||
const workId = listJson.data.list[0].id
|
||
const detailResp = await api.get(url(`/public/works/${workId}`))
|
||
const detailJson = await detailResp.json()
|
||
|
||
expect(detailJson.code).toBe(200)
|
||
const work = detailJson.data.work
|
||
|
||
// status 必须是合法字符串
|
||
expect(VALID_STATUSES.includes(work.status)).toBe(true)
|
||
|
||
// leaiStatus 应为数字
|
||
if (work.leaiStatus != null) {
|
||
expect(typeof work.leaiStatus).toBe('number')
|
||
}
|
||
console.log(`✓ S-10: 作品详情 status="${work.status}", leaiStatus=${work.leaiStatus}`)
|
||
} else {
|
||
console.log('⚠ S-10: 没有作品可测试详情')
|
||
}
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 6. 作品广场
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-11 作品广场 — 只返回已发布作品', async ({ api }) => {
|
||
const resp = await api.get(url('/public/gallery?page=1&pageSize=10'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
if (json.data?.list?.length > 0) {
|
||
for (const work of json.data.list) {
|
||
expect(work.status).toBe('published')
|
||
}
|
||
}
|
||
console.log(`✓ S-11: 广场 ${json.data?.list?.length ?? 0} 条已发布作品`)
|
||
})
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// 7. 收藏列表
|
||
// ══════════════════════════════════════════════════════
|
||
|
||
apiTest('S-12 我的收藏 — status 为字符串', async ({ api }) => {
|
||
const resp = await api.get(url('/public/mine/favorites?page=1&pageSize=5'))
|
||
const json = await resp.json()
|
||
|
||
expect(json.code).toBe(200)
|
||
if (json.data?.list?.length > 0) {
|
||
for (const item of json.data.list) {
|
||
if (item.status) {
|
||
expect(VALID_STATUSES.includes(item.status)).toBe(true)
|
||
}
|
||
}
|
||
}
|
||
console.log(`✓ S-12: 收藏 ${json.data?.list?.length ?? 0} 条`)
|
||
})
|