feat: 拆分 UgcWork.status 为 status(String) + leaiStatus(Integer),修复作品库 Tab 筛选失败
根因: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>
This commit is contained in:
parent
322fd2c4ad
commit
400fc97ebb
@ -0,0 +1,19 @@
|
||||
package com.competition.modules.leai.enums;
|
||||
|
||||
/**
|
||||
* 乐读派 AI 创作进度常量
|
||||
* <p>
|
||||
* 对应 t_ugc_work.leai_status 字段(INT)
|
||||
*/
|
||||
public final class LeaiCreationStatus {
|
||||
|
||||
public static final int FAILED = -1;
|
||||
public static final int DRAFT = 0;
|
||||
public static final int PENDING = 1;
|
||||
public static final int PROCESSING = 2;
|
||||
public static final int COMPLETED = 3;
|
||||
public static final int CATALOGED = 4;
|
||||
public static final int DUBBED = 5;
|
||||
|
||||
private LeaiCreationStatus() {}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.modules.leai.config.LeaiConfig;
|
||||
import com.competition.modules.leai.enums.LeaiCreationStatus;
|
||||
import com.competition.modules.leai.util.LeaiUtil;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -38,14 +39,14 @@ public class LeaiApiClient {
|
||||
private final LeaiConfig leaiConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
// ── 状态常量(V4.0 数值状态机) ──
|
||||
public static final int STATUS_FAILED = -1;
|
||||
public static final int STATUS_DRAFT = 0;
|
||||
public static final int STATUS_PENDING = 1;
|
||||
public static final int STATUS_PROCESSING = 2;
|
||||
public static final int STATUS_COMPLETED = 3;
|
||||
public static final int STATUS_CATALOGED = 4;
|
||||
public static final int STATUS_DUBBED = 5;
|
||||
// ── 状态常量(引用 LeaiCreationStatus,保持向后兼容) ──
|
||||
public static final int STATUS_FAILED = LeaiCreationStatus.FAILED;
|
||||
public static final int STATUS_DRAFT = LeaiCreationStatus.DRAFT;
|
||||
public static final int STATUS_PENDING = LeaiCreationStatus.PENDING;
|
||||
public static final int STATUS_PROCESSING = LeaiCreationStatus.PROCESSING;
|
||||
public static final int STATUS_COMPLETED = LeaiCreationStatus.COMPLETED;
|
||||
public static final int STATUS_CATALOGED = LeaiCreationStatus.CATALOGED;
|
||||
public static final int STATUS_DUBBED = LeaiCreationStatus.DUBBED;
|
||||
|
||||
/**
|
||||
* 换取 Session Token
|
||||
|
||||
@ -3,11 +3,13 @@ package com.competition.modules.leai.service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.competition.common.enums.Visibility;
|
||||
import com.competition.modules.leai.enums.LeaiCreationStatus;
|
||||
import com.competition.modules.leai.util.LeaiUtil;
|
||||
import com.competition.modules.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import com.competition.modules.ugc.entity.UgcWork;
|
||||
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||
import com.competition.modules.ugc.enums.WorkPublishStatus;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -61,16 +63,16 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
return;
|
||||
}
|
||||
|
||||
int localStatus = localWork.getStatus() != null ? localWork.getStatus() : 0;
|
||||
int localLeaiStatus = localWork.getLeaiStatus() != null ? localWork.getLeaiStatus() : 0;
|
||||
|
||||
if (remoteStatus == LeaiApiClient.STATUS_FAILED) {
|
||||
if (remoteStatus == LeaiCreationStatus.FAILED) {
|
||||
// ★ FAILED(-1): 创作失败,强制更新
|
||||
updateFailed(localWork, remoteData);
|
||||
log.info("[{}] 强制更新(FAILED) remoteWorkId={}", source, remoteWorkId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteStatus == LeaiApiClient.STATUS_PROCESSING) {
|
||||
if (remoteStatus == LeaiCreationStatus.PROCESSING) {
|
||||
// ★ PROCESSING(2): 创作进行中,强制更新进度
|
||||
updateProcessing(localWork, remoteData);
|
||||
log.info("[{}] 强制更新(PROCESSING) remoteWorkId={}, progress={}",
|
||||
@ -78,23 +80,23 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
return;
|
||||
}
|
||||
|
||||
if (remoteStatus > localStatus) {
|
||||
if (remoteStatus > localLeaiStatus) {
|
||||
// ★ 状态前进: 全量更新
|
||||
// status=3(COMPLETED): pageList 包含图片 URL
|
||||
// status=4(CATALOGED): title/author 已更新
|
||||
// status=5(DUBBED): pageList 中 audioUrl 已填充
|
||||
// leaiStatus=3(COMPLETED): pageList 包含图片 URL
|
||||
// leaiStatus=4(CATALOGED): title/author 已更新
|
||||
// leaiStatus=5(DUBBED): pageList 中 audioUrl 已填充
|
||||
updateStatusForward(localWork, remoteData, remoteStatus);
|
||||
log.info("[{}] 状态更新 remoteWorkId={}: {} -> {}", source, remoteWorkId, localStatus, remoteStatus);
|
||||
log.info("[{}] 状态更新 remoteWorkId={}: {} -> {}", source, remoteWorkId, localLeaiStatus, remoteStatus);
|
||||
return;
|
||||
}
|
||||
|
||||
// 旧数据或重复推送,忽略状态更新
|
||||
// 但如果 remoteStatus >= 3 且本地缺少页面数据,需要补充拉取
|
||||
if (remoteStatus >= LeaiApiClient.STATUS_COMPLETED && !hasPages(localWork.getId())) {
|
||||
if (remoteStatus >= LeaiCreationStatus.COMPLETED && !hasPages(localWork.getId())) {
|
||||
log.info("[{}] 状态未变但页面缺失,补充拉取: remoteWorkId={}, status={}", source, remoteWorkId, remoteStatus);
|
||||
ensurePagesSaved(localWork.getId(), remoteWorkId, remoteData.get("pageList"));
|
||||
} else {
|
||||
log.debug("[{}] 跳过 remoteWorkId={}, remote={} <= local={}", source, remoteWorkId, remoteStatus, localStatus);
|
||||
log.debug("[{}] 跳过 remoteWorkId={}, remote={} <= local={}", source, remoteWorkId, remoteStatus, localLeaiStatus);
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +107,12 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
UgcWork work = new UgcWork();
|
||||
work.setRemoteWorkId(remoteWorkId);
|
||||
work.setTitle(LeaiUtil.toString(remoteData.get("title"), "未命名作品"));
|
||||
work.setStatus(LeaiUtil.toInt(remoteData.get("status"), LeaiApiClient.STATUS_PENDING));
|
||||
int leaiStatus = LeaiUtil.toInt(remoteData.get("status"), LeaiCreationStatus.PENDING);
|
||||
work.setLeaiStatus(leaiStatus);
|
||||
// 本地发布状态:创作进度 >= CATALOGED 时自动设为 unpublished,否则为 draft
|
||||
work.setStatus(leaiStatus >= LeaiCreationStatus.CATALOGED
|
||||
? WorkPublishStatus.UNPUBLISHED.getValue()
|
||||
: WorkPublishStatus.DRAFT.getValue());
|
||||
work.setVisibility(Visibility.PRIVATE.getValue());
|
||||
work.setIsDeleted(0);
|
||||
work.setIsRecommended(false);
|
||||
@ -156,8 +163,8 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
|
||||
ugcWorkMapper.insert(work);
|
||||
|
||||
// 如果 status >= 3,确保页面数据已保存
|
||||
if (work.getStatus() != null && work.getStatus() >= LeaiApiClient.STATUS_COMPLETED) {
|
||||
// 如果 leaiStatus >= 3,确保页面数据已保存
|
||||
if (work.getLeaiStatus() != null && work.getLeaiStatus() >= LeaiCreationStatus.COMPLETED) {
|
||||
ensurePagesSaved(work.getId(), remoteWorkId, remoteData.get("pageList"));
|
||||
}
|
||||
}
|
||||
@ -168,7 +175,7 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
private void updateFailed(UgcWork work, Map<String, Object> remoteData) {
|
||||
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(UgcWork::getId, work.getId())
|
||||
.set(UgcWork::getStatus, LeaiApiClient.STATUS_FAILED)
|
||||
.set(UgcWork::getLeaiStatus, LeaiCreationStatus.FAILED)
|
||||
.set(UgcWork::getFailReason, LeaiUtil.toString(remoteData.get("failReason"), "未知错误"))
|
||||
.set(UgcWork::getModifyTime, LocalDateTime.now());
|
||||
ugcWorkMapper.update(null, wrapper);
|
||||
@ -180,7 +187,7 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
private void updateProcessing(UgcWork work, Map<String, Object> remoteData) {
|
||||
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(UgcWork::getId, work.getId())
|
||||
.set(UgcWork::getStatus, LeaiApiClient.STATUS_PROCESSING);
|
||||
.set(UgcWork::getLeaiStatus, LeaiCreationStatus.PROCESSING);
|
||||
|
||||
if (remoteData.containsKey("progress")) {
|
||||
wrapper.set(UgcWork::getProgress, LeaiUtil.toInt(remoteData.get("progress"), 0));
|
||||
@ -215,11 +222,17 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
* 状态前进:全量更新
|
||||
*/
|
||||
private void updateStatusForward(UgcWork work, Map<String, Object> remoteData, int remoteStatus) {
|
||||
// CAS 乐观锁:确保并发安全,只有当前 status < remoteStatus 时才更新
|
||||
// CAS 乐观锁:确保并发安全,只有当前 leaiStatus < remoteStatus 时才更新
|
||||
LambdaUpdateWrapper<UgcWork> wrapper = new LambdaUpdateWrapper<>();
|
||||
wrapper.eq(UgcWork::getId, work.getId())
|
||||
.lt(UgcWork::getStatus, remoteStatus)
|
||||
.set(UgcWork::getStatus, remoteStatus);
|
||||
.lt(UgcWork::getLeaiStatus, remoteStatus)
|
||||
.set(UgcWork::getLeaiStatus, remoteStatus);
|
||||
|
||||
// 当 leaiStatus 推进到 CATALOGED 且当前 status 仍为 draft → 自动设 status 为 unpublished
|
||||
if (remoteStatus >= LeaiCreationStatus.CATALOGED
|
||||
&& WorkPublishStatus.DRAFT.getValue().equals(work.getStatus())) {
|
||||
wrapper.set(UgcWork::getStatus, WorkPublishStatus.UNPUBLISHED.getValue());
|
||||
}
|
||||
|
||||
// 更新可变字段
|
||||
if (remoteData.containsKey("title")) {
|
||||
@ -267,8 +280,8 @@ public class LeaiSyncService implements ILeaiSyncService {
|
||||
return;
|
||||
}
|
||||
|
||||
// status >= 3 时确保页面数据已保存
|
||||
if (remoteStatus >= LeaiApiClient.STATUS_COMPLETED) {
|
||||
// leaiStatus >= 3 时确保页面数据已保存
|
||||
if (remoteStatus >= LeaiCreationStatus.COMPLETED) {
|
||||
ensurePagesSaved(work.getId(), work.getRemoteWorkId(), remoteData.get("pageList"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import com.competition.modules.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import com.competition.modules.ugc.entity.UgcReviewLog;
|
||||
import com.competition.modules.ugc.entity.UgcWork;
|
||||
import com.competition.modules.ugc.enums.WorkPublishStatus;
|
||||
import com.competition.modules.ugc.mapper.UgcReviewLogMapper;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -33,26 +34,16 @@ public class PublicContentReviewService {
|
||||
private final UgcReviewLogMapper ugcReviewLogMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
|
||||
/** 与 UgcWork.status 整型及写库逻辑一致 */
|
||||
private static final int ST_DRAFT = 0;
|
||||
private static final int ST_PENDING = 1;
|
||||
private static final int ST_PROCESSING = 2;
|
||||
private static final int ST_COMPLETED = 3;
|
||||
private static final int ST_CATALOGED = 4;
|
||||
private static final int ST_DUBBED = 5;
|
||||
private static final int ST_REJECTED = -1;
|
||||
private static final int ST_TAKEN_DOWN = -2;
|
||||
|
||||
/**
|
||||
* 获取各状态统计(作品审核页:待审核数 + 今日审核维度)
|
||||
*/
|
||||
public Map<String, Long> getStats() {
|
||||
Map<String, Long> stats = new LinkedHashMap<>();
|
||||
stats.put("pending", countByStatusInt(ST_PENDING));
|
||||
stats.put("pending_review", countByStatusInt(ST_PENDING));
|
||||
stats.put("published", countPublishedStatuses());
|
||||
stats.put("rejected", countByStatusInt(ST_REJECTED));
|
||||
stats.put("taken_down", countByStatusInt(ST_TAKEN_DOWN));
|
||||
stats.put("pending", countByStatus(WorkPublishStatus.PENDING_REVIEW.getValue()));
|
||||
stats.put("pending_review", countByStatus(WorkPublishStatus.PENDING_REVIEW.getValue()));
|
||||
stats.put("published", countByStatus(WorkPublishStatus.PUBLISHED.getValue()));
|
||||
stats.put("rejected", countByStatus(WorkPublishStatus.REJECTED.getValue()));
|
||||
stats.put("taken_down", countByStatus(WorkPublishStatus.UNPUBLISHED.getValue()));
|
||||
LocalDateTime dayStart = LocalDate.now().atStartOfDay();
|
||||
stats.put("todayReviewed", countTodayReviewLogs(dayStart));
|
||||
stats.put("todayApproved", countTodayLogsByAction("approve", dayStart));
|
||||
@ -70,11 +61,11 @@ public class PublicContentReviewService {
|
||||
LambdaQueryWrapper<UgcWork> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(UgcWork::getIsDeleted, 0);
|
||||
if (StringUtils.hasText(status)) {
|
||||
List<Integer> codes = parseStatusFilter(status);
|
||||
if (codes.isEmpty()) {
|
||||
wrapper.eq(UgcWork::getStatus, Integer.MIN_VALUE);
|
||||
List<String> statusValues = parseStatusFilter(status);
|
||||
if (statusValues.isEmpty()) {
|
||||
wrapper.eq(UgcWork::getStatus, "__never_match__");
|
||||
} else {
|
||||
wrapper.in(UgcWork::getStatus, codes);
|
||||
wrapper.in(UgcWork::getStatus, statusValues);
|
||||
}
|
||||
}
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
@ -139,7 +130,7 @@ public class PublicContentReviewService {
|
||||
if (work == null) {
|
||||
throw new BusinessException(404, "作品不存在");
|
||||
}
|
||||
work.setStatus(3); // 3=COMPLETED/PUBLISHED
|
||||
work.setStatus(WorkPublishStatus.PUBLISHED.getValue());
|
||||
work.setReviewNote(note);
|
||||
work.setReviewerId(operatorId);
|
||||
work.setReviewTime(LocalDateTime.now());
|
||||
@ -158,7 +149,7 @@ public class PublicContentReviewService {
|
||||
if (work == null) {
|
||||
throw new BusinessException(404, "作品不存在");
|
||||
}
|
||||
work.setStatus(-1); // -1=FAILED/REJECTED
|
||||
work.setStatus(WorkPublishStatus.REJECTED.getValue());
|
||||
work.setReviewNote(note);
|
||||
work.setReviewerId(operatorId);
|
||||
work.setReviewTime(LocalDateTime.now());
|
||||
@ -196,7 +187,7 @@ public class PublicContentReviewService {
|
||||
if (work == null) {
|
||||
throw new BusinessException(404, "作品不存在");
|
||||
}
|
||||
work.setStatus(1); // 1=PENDING
|
||||
work.setStatus(WorkPublishStatus.UNPUBLISHED.getValue());
|
||||
work.setModifyTime(LocalDateTime.now());
|
||||
ugcWorkMapper.updateById(work);
|
||||
createLog(id, "revoke", null, null, operatorId);
|
||||
@ -211,7 +202,7 @@ public class PublicContentReviewService {
|
||||
if (work == null) {
|
||||
throw new BusinessException(404, "作品不存在");
|
||||
}
|
||||
work.setStatus(-2); // -2=TAKEN_DOWN
|
||||
work.setStatus(WorkPublishStatus.UNPUBLISHED.getValue());
|
||||
work.setModifyTime(LocalDateTime.now());
|
||||
ugcWorkMapper.updateById(work);
|
||||
createLog(id, "takedown", reason, null, operatorId);
|
||||
@ -226,7 +217,7 @@ public class PublicContentReviewService {
|
||||
if (work == null) {
|
||||
throw new BusinessException(404, "作品不存在");
|
||||
}
|
||||
work.setStatus(3); // 3=COMPLETED/PUBLISHED
|
||||
work.setStatus(WorkPublishStatus.PUBLISHED.getValue());
|
||||
work.setModifyTime(LocalDateTime.now());
|
||||
ugcWorkMapper.updateById(work);
|
||||
createLog(id, "restore", null, null, operatorId);
|
||||
@ -257,20 +248,18 @@ public class PublicContentReviewService {
|
||||
*/
|
||||
public Map<String, Object> getManagementStats() {
|
||||
Map<String, Object> stats = new LinkedHashMap<>();
|
||||
// 与作品管理列表默认筛选 status=published,taken_down 一致(已上架/已编目/已配音 + 已下架),不含草稿/待审等
|
||||
stats.put("total", countPublishedStatuses() + countByStatusInt(ST_TAKEN_DOWN));
|
||||
stats.put("total", countByStatus(WorkPublishStatus.PUBLISHED.getValue()) + countByStatus(WorkPublishStatus.UNPUBLISHED.getValue()));
|
||||
LocalDateTime dayStart = LocalDate.now().atStartOfDay();
|
||||
// 与作品管理「今日新增」Tab 列表一致:当日创建且为已上架(3/4/5)或已下架(-2)
|
||||
stats.put("todayNew", ugcWorkMapper.selectCount(
|
||||
new LambdaQueryWrapper<UgcWork>()
|
||||
.eq(UgcWork::getIsDeleted, 0)
|
||||
.ge(UgcWork::getCreateTime, dayStart)
|
||||
.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED, ST_TAKEN_DOWN)));
|
||||
.in(UgcWork::getStatus, WorkPublishStatus.PUBLISHED.getValue(), WorkPublishStatus.UNPUBLISHED.getValue())));
|
||||
stats.put("totalViews", sumAllViewCounts());
|
||||
stats.put("pendingReview", countByStatusInt(ST_PENDING));
|
||||
stats.put("published", countPublishedStatuses());
|
||||
stats.put("rejected", countByStatusInt(ST_REJECTED));
|
||||
stats.put("takenDown", countByStatusInt(ST_TAKEN_DOWN));
|
||||
stats.put("pendingReview", countByStatus(WorkPublishStatus.PENDING_REVIEW.getValue()));
|
||||
stats.put("published", countByStatus(WorkPublishStatus.PUBLISHED.getValue()));
|
||||
stats.put("rejected", countByStatus(WorkPublishStatus.REJECTED.getValue()));
|
||||
stats.put("takenDown", countByStatus(WorkPublishStatus.UNPUBLISHED.getValue()));
|
||||
stats.put("recommended", ugcWorkMapper.selectCount(
|
||||
new LambdaQueryWrapper<UgcWork>()
|
||||
.eq(UgcWork::getIsDeleted, 0)
|
||||
@ -310,50 +299,43 @@ public class PublicContentReviewService {
|
||||
|
||||
/**
|
||||
* 解析筛选参数:单状态或逗号分隔多状态(如 published,taken_down)
|
||||
* status 字段已改为 VARCHAR,直接使用字符串枚举值
|
||||
*/
|
||||
private List<Integer> parseStatusFilter(String status) {
|
||||
private List<String> parseStatusFilter(String status) {
|
||||
String[] parts = status.split(",");
|
||||
return Stream.of(parts)
|
||||
.map(String::trim)
|
||||
.filter(StringUtils::hasText)
|
||||
.flatMap(token -> resolveStatusCodes(token).stream())
|
||||
.flatMap(token -> resolveStatusValues(token).stream())
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Integer> resolveStatusCodes(String token) {
|
||||
private List<String> resolveStatusValues(String token) {
|
||||
switch (token) {
|
||||
case "pending_review":
|
||||
return List.of(ST_PENDING);
|
||||
return List.of(WorkPublishStatus.PENDING_REVIEW.getValue());
|
||||
case "published":
|
||||
return List.of(ST_COMPLETED, ST_CATALOGED, ST_DUBBED);
|
||||
return List.of(WorkPublishStatus.PUBLISHED.getValue());
|
||||
case "rejected":
|
||||
return List.of(ST_REJECTED);
|
||||
return List.of(WorkPublishStatus.REJECTED.getValue());
|
||||
case "taken_down":
|
||||
return List.of(ST_TAKEN_DOWN);
|
||||
case "unpublished":
|
||||
return List.of(WorkPublishStatus.UNPUBLISHED.getValue());
|
||||
case "draft":
|
||||
return List.of(ST_DRAFT);
|
||||
case "processing":
|
||||
return List.of(ST_PROCESSING);
|
||||
return List.of(WorkPublishStatus.DRAFT.getValue());
|
||||
default:
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
private long countByStatusInt(int status) {
|
||||
private long countByStatus(String status) {
|
||||
return ugcWorkMapper.selectCount(
|
||||
new LambdaQueryWrapper<UgcWork>()
|
||||
.eq(UgcWork::getIsDeleted, 0)
|
||||
.eq(UgcWork::getStatus, status));
|
||||
}
|
||||
|
||||
private long countPublishedStatuses() {
|
||||
return ugcWorkMapper.selectCount(
|
||||
new LambdaQueryWrapper<UgcWork>()
|
||||
.eq(UgcWork::getIsDeleted, 0)
|
||||
.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED));
|
||||
}
|
||||
|
||||
private long countTodayReviewLogs(LocalDateTime dayStart) {
|
||||
return ugcReviewLogMapper.selectCount(
|
||||
new LambdaQueryWrapper<UgcReviewLog>()
|
||||
@ -381,33 +363,6 @@ public class PublicContentReviewService {
|
||||
return ((Number) maps.get(0).get("sv")).longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将作品状态整型转为前端使用的字符串码(与 WorkReview / WorkManagement 一致)
|
||||
*/
|
||||
private String mapWorkStatusToString(Integer code) {
|
||||
if (code == null) {
|
||||
return "draft";
|
||||
}
|
||||
switch (code) {
|
||||
case ST_DRAFT:
|
||||
return "draft";
|
||||
case ST_PENDING:
|
||||
return "pending_review";
|
||||
case ST_PROCESSING:
|
||||
return "processing";
|
||||
case ST_COMPLETED:
|
||||
case ST_CATALOGED:
|
||||
case ST_DUBBED:
|
||||
return "published";
|
||||
case ST_REJECTED:
|
||||
return "rejected";
|
||||
case ST_TAKEN_DOWN:
|
||||
return "taken_down";
|
||||
default:
|
||||
return "draft";
|
||||
}
|
||||
}
|
||||
|
||||
private void createLog(Long workId, String action, String reason, String note, Long operatorId) {
|
||||
UgcReviewLog logEntry = new UgcReviewLog();
|
||||
logEntry.setTargetType("work");
|
||||
@ -427,9 +382,8 @@ public class PublicContentReviewService {
|
||||
vo.put("title", work.getTitle());
|
||||
vo.put("coverUrl", work.getCoverUrl());
|
||||
vo.put("description", work.getDescription());
|
||||
Integer statusCode = work.getStatus();
|
||||
vo.put("statusCode", statusCode);
|
||||
vo.put("status", mapWorkStatusToString(statusCode));
|
||||
vo.put("statusCode", work.getLeaiStatus());
|
||||
vo.put("status", work.getStatus());
|
||||
vo.put("visibility", work.getVisibility());
|
||||
vo.put("isRecommended", work.getIsRecommended());
|
||||
vo.put("viewCount", work.getViewCount());
|
||||
|
||||
@ -6,8 +6,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.competition.common.enums.Visibility;
|
||||
import com.competition.common.exception.BusinessException;
|
||||
import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.leai.enums.LeaiCreationStatus;
|
||||
import com.competition.modules.ugc.entity.UgcWork;
|
||||
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||
import com.competition.modules.ugc.enums.WorkPublishStatus;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -33,7 +35,8 @@ public class PublicCreationService {
|
||||
UgcWork work = new UgcWork();
|
||||
work.setUserId(userId);
|
||||
work.setTitle("未命名作品");
|
||||
work.setStatus(0); // DRAFT
|
||||
work.setLeaiStatus(LeaiCreationStatus.DRAFT);
|
||||
work.setStatus(WorkPublishStatus.DRAFT.getValue());
|
||||
work.setOriginalImageUrl(originalImageUrl);
|
||||
work.setVoiceInputUrl(voiceInputUrl);
|
||||
work.setTextInput(textInput);
|
||||
@ -62,7 +65,7 @@ public class PublicCreationService {
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("id", work.getId());
|
||||
result.put("status", work.getStatus());
|
||||
result.put("status", work.getLeaiStatus());
|
||||
result.put("progress", work.getProgress());
|
||||
result.put("progressMessage", work.getProgressMessage());
|
||||
result.put("remoteWorkId", work.getRemoteWorkId());
|
||||
@ -85,7 +88,7 @@ public class PublicCreationService {
|
||||
result.put("title", work.getTitle());
|
||||
result.put("coverUrl", work.getCoverUrl());
|
||||
result.put("description", work.getDescription());
|
||||
result.put("status", work.getStatus());
|
||||
result.put("status", work.getLeaiStatus());
|
||||
result.put("progress", work.getProgress());
|
||||
result.put("progressMessage", work.getProgressMessage());
|
||||
result.put("originalImageUrl", work.getOriginalImageUrl());
|
||||
|
||||
@ -11,6 +11,7 @@ import com.competition.modules.sys.entity.SysUser;
|
||||
import com.competition.modules.sys.mapper.SysUserMapper;
|
||||
import com.competition.modules.ugc.entity.UgcWork;
|
||||
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||
import com.competition.modules.ugc.enums.WorkPublishStatus;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -25,11 +26,6 @@ import java.util.*;
|
||||
@RequiredArgsConstructor
|
||||
public class PublicGalleryService {
|
||||
|
||||
/** 与 PublicContentReviewService、UgcWork.status 一致:已上架可对外的作品 */
|
||||
private static final int ST_COMPLETED = 3;
|
||||
private static final int ST_CATALOGED = 4;
|
||||
private static final int ST_DUBBED = 5;
|
||||
|
||||
private final UgcWorkMapper ugcWorkMapper;
|
||||
private final UgcWorkPageMapper ugcWorkPageMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
@ -43,7 +39,7 @@ public class PublicGalleryService {
|
||||
public PageResult<Map<String, Object>> getGalleryList(int page, int pageSize, Long tagId,
|
||||
String category, String sortBy, String keyword) {
|
||||
LambdaQueryWrapper<UgcWork> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED)
|
||||
wrapper.eq(UgcWork::getStatus, WorkPublishStatus.PUBLISHED.getValue())
|
||||
.eq(UgcWork::getVisibility, Visibility.PUBLIC.getValue())
|
||||
.eq(UgcWork::getIsDeleted, 0);
|
||||
if (tagId != null) {
|
||||
@ -80,7 +76,7 @@ public class PublicGalleryService {
|
||||
public List<Map<String, Object>> getRecommended() {
|
||||
LambdaQueryWrapper<UgcWork> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(UgcWork::getIsRecommended, true)
|
||||
.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED)
|
||||
.eq(UgcWork::getStatus, WorkPublishStatus.PUBLISHED.getValue())
|
||||
.eq(UgcWork::getVisibility, Visibility.PUBLIC.getValue())
|
||||
.eq(UgcWork::getIsDeleted, 0)
|
||||
.orderByDesc(UgcWork::getPublishTime)
|
||||
@ -151,7 +147,7 @@ public class PublicGalleryService {
|
||||
public PageResult<Map<String, Object>> getUserPublicWorks(Long userId, int page, int pageSize) {
|
||||
LambdaQueryWrapper<UgcWork> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(UgcWork::getUserId, userId)
|
||||
.in(UgcWork::getStatus, ST_COMPLETED, ST_CATALOGED, ST_DUBBED)
|
||||
.eq(UgcWork::getStatus, WorkPublishStatus.PUBLISHED.getValue())
|
||||
.eq(UgcWork::getVisibility, Visibility.PUBLIC.getValue())
|
||||
.eq(UgcWork::getIsDeleted, 0)
|
||||
.orderByDesc(UgcWork::getPublishTime);
|
||||
|
||||
@ -10,6 +10,7 @@ import com.competition.common.result.PageResult;
|
||||
import com.competition.modules.ugc.entity.UgcWork;
|
||||
import com.competition.modules.ugc.entity.UgcWorkPage;
|
||||
import com.competition.modules.ugc.entity.UgcWorkTag;
|
||||
import com.competition.modules.ugc.enums.WorkPublishStatus;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkMapper;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkPageCountRow;
|
||||
import com.competition.modules.ugc.mapper.UgcWorkPageMapper;
|
||||
@ -45,7 +46,7 @@ public class PublicUserWorkService {
|
||||
work.setCoverUrl(coverUrl);
|
||||
work.setDescription(description);
|
||||
work.setVisibility(visibility != null ? visibility : Visibility.PRIVATE.getValue());
|
||||
work.setStatus(0); // 0=DRAFT
|
||||
work.setStatus(WorkPublishStatus.DRAFT.getValue());
|
||||
work.setViewCount(0);
|
||||
work.setLikeCount(0);
|
||||
work.setFavoriteCount(0);
|
||||
@ -184,10 +185,12 @@ public class PublicUserWorkService {
|
||||
if (work == null || work.getIsDeleted() == 1 || !work.getUserId().equals(userId)) {
|
||||
throw new BusinessException(404, "作品不存在或无权操作");
|
||||
}
|
||||
if (work.getStatus() != 0 && work.getStatus() != -1) {
|
||||
String currentStatus = work.getStatus();
|
||||
if (!WorkPublishStatus.UNPUBLISHED.getValue().equals(currentStatus)
|
||||
&& !WorkPublishStatus.REJECTED.getValue().equals(currentStatus)) {
|
||||
throw new BusinessException(400, "当前状态不可发布");
|
||||
}
|
||||
work.setStatus(1); // 1=PENDING
|
||||
work.setStatus(WorkPublishStatus.PENDING_REVIEW.getValue());
|
||||
work.setModifyTime(LocalDateTime.now());
|
||||
ugcWorkMapper.updateById(work);
|
||||
}
|
||||
|
||||
@ -42,8 +42,12 @@ public class UgcWork implements Serializable {
|
||||
@Schema(description = "可见范围", allowableValues = {"public", "designated", "internal", "private"})
|
||||
private String visibility;
|
||||
|
||||
@Schema(description = "作品状态:-1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED")
|
||||
private Integer status;
|
||||
@Schema(description = "本地发布状态: draft/unpublished/pending_review/published/rejected")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "乐读派创作进度: -1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED")
|
||||
@TableField("leai_status")
|
||||
private Integer leaiStatus;
|
||||
|
||||
@Schema(description = "审核备注")
|
||||
@TableField("review_note")
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
package com.competition.modules.ugc.enums;
|
||||
|
||||
/**
|
||||
* 作品本地发布状态枚举
|
||||
* <p>
|
||||
* 与前端 WorkStatus 类型定义完全一致:'draft' | 'unpublished' | 'pending_review' | 'published' | 'rejected'
|
||||
*/
|
||||
public enum WorkPublishStatus {
|
||||
|
||||
DRAFT("draft", "草稿"),
|
||||
UNPUBLISHED("unpublished", "未发布"),
|
||||
PENDING_REVIEW("pending_review", "审核中"),
|
||||
PUBLISHED("published", "已发布"),
|
||||
REJECTED("rejected", "被拒绝");
|
||||
|
||||
private final String value;
|
||||
private final String label;
|
||||
|
||||
WorkPublishStatus(String value, String label) {
|
||||
this.value = value;
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
-- V17: 拆分 UgcWork.status 字段
|
||||
-- 原 status (INT) 同时承载「乐读派创作进度」和「本地发布状态」,导致前端筛选失败
|
||||
-- 拆为:leai_status (INT) = 乐读派创作进度,status (VARCHAR) = 本地发布状态
|
||||
|
||||
-- 1. 新增 leai_status 字段
|
||||
ALTER TABLE t_ugc_work ADD COLUMN leai_status INT NOT NULL DEFAULT 0
|
||||
COMMENT '乐读派创作进度: -1=FAILED, 0=DRAFT, 1=PENDING, 2=PROCESSING, 3=COMPLETED, 4=CATALOGED, 5=DUBBED';
|
||||
|
||||
-- 2. 把现有 status (INT) 值复制到 leai_status
|
||||
UPDATE t_ugc_work SET leai_status = status;
|
||||
|
||||
-- 3. 改 status 字段类型为 VARCHAR
|
||||
ALTER TABLE t_ugc_work MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'draft'
|
||||
COMMENT '本地发布状态: draft/unpublished/pending_review/published/rejected';
|
||||
|
||||
-- 4. 按 leai_status 回填本地 status(保守策略)
|
||||
UPDATE t_ugc_work SET status = CASE
|
||||
WHEN leai_status = -2 THEN 'unpublished'
|
||||
WHEN leai_status = -1 THEN 'draft'
|
||||
WHEN leai_status = 0 THEN 'draft'
|
||||
WHEN leai_status = 1 THEN 'unpublished'
|
||||
WHEN leai_status = 2 THEN 'draft'
|
||||
WHEN leai_status = 3 THEN 'unpublished'
|
||||
WHEN leai_status = 4 THEN 'unpublished'
|
||||
WHEN leai_status = 5 THEN 'unpublished'
|
||||
ELSE 'draft'
|
||||
END;
|
||||
|
||||
-- 5. 索引
|
||||
CREATE INDEX idx_ugc_work_status ON t_ugc_work(status);
|
||||
CREATE INDEX idx_ugc_work_leai_status ON t_ugc_work(leai_status);
|
||||
255
frontend/e2e/public/work-status-split.spec.ts
Normal file
255
frontend/e2e/public/work-status-split.spec.ts
Normal file
@ -0,0 +1,255 @@
|
||||
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} 条`)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user