feat: 公众端作品详情与审核流程对齐(撤回下架、驳回原因、删除限制)

Made-with: Cursor
This commit is contained in:
zhonghua 2026-04-13 17:51:47 +08:00
parent 56bddb5206
commit 44ad1746f3
8 changed files with 162 additions and 16 deletions

View File

@ -3,6 +3,7 @@ package com.lesingle.modules.pub.controller;
import com.lesingle.common.result.PageResult;
import com.lesingle.common.result.Result;
import com.lesingle.common.util.SecurityUtil;
import com.lesingle.modules.pub.dto.ContentReviewRejectDto;
import com.lesingle.modules.pub.service.PublicContentReviewService;
import com.lesingle.modules.ugc.entity.UgcReviewLog;
import io.swagger.v3.oas.annotations.Operation;
@ -65,10 +66,10 @@ public class ContentReviewController {
}
@PostMapping("/works/{id}/reject")
@Operation(summary = "驳回")
public Result<Void> reject(@PathVariable Long id, @RequestBody Map<String, String> body) {
@Operation(summary = "驳回", description = "请求体 JSONreason驳回原因必填写入作品 review_notenote 可选。与公众端作品详情展示的审核拒绝原因一致。")
public Result<Void> reject(@PathVariable Long id, @RequestBody ContentReviewRejectDto body) {
Long operatorId = SecurityUtil.getCurrentUserId();
publicContentReviewService.reject(id, body.get("reason"), body.get("note"), operatorId);
publicContentReviewService.reject(id, body.getReason(), body.getNote(), operatorId);
return Result.success();
}

View File

@ -83,6 +83,22 @@ public class PublicUserWorkController {
return Result.success();
}
@PostMapping("/{id}/withdraw")
@Operation(summary = "撤回审核(审核中 → 未发布,作者本人)")
public Result<Void> withdraw(@PathVariable Long id) {
Long userId = SecurityUtil.getCurrentUserId();
publicUserWorkService.withdrawReview(id, userId);
return Result.success();
}
@PostMapping("/{id}/unpublish")
@Operation(summary = "下架作品(已发布 → 未发布,作者本人)")
public Result<Void> unpublish(@PathVariable Long id) {
Long userId = SecurityUtil.getCurrentUserId();
publicUserWorkService.unpublish(id, userId);
return Result.success();
}
@GetMapping("/{id}/pages")
@Operation(summary = "获取作品页面")
public Result<List<UgcWorkPage>> getPages(@PathVariable Long id) {

View File

@ -0,0 +1,18 @@
package com.lesingle.modules.pub.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 超管驳回作品请求体 {@code PublicContentReviewService#reject} 及作品表 {@code review_note} 对齐
*/
@Data
@Schema(description = "驳回作品POST /content-review/works/{id}/reject")
public class ContentReviewRejectDto {
@Schema(description = "驳回原因,必填;将写入作品 review_note供作者端详情展示", example = "内容质量不符合发布标准")
private String reason;
@Schema(description = "可选补充备注;若有则与 reason 以「;」拼接后写入 review_note")
private String note;
}

View File

@ -152,7 +152,18 @@ public class PublicContentReviewService {
throw new BusinessException(404, "作品不存在");
}
work.setStatus(WorkPublishStatus.REJECTED.getValue());
work.setReviewNote(note);
// 前端驳回只传 reason备注传 note需写入 review_note 供公众端展示
StringBuilder reviewText = new StringBuilder();
if (StringUtils.hasText(reason)) {
reviewText.append(reason.trim());
}
if (StringUtils.hasText(note)) {
if (reviewText.length() > 0) {
reviewText.append('');
}
reviewText.append(note.trim());
}
work.setReviewNote(reviewText.length() > 0 ? reviewText.toString() : null);
work.setReviewerId(operatorId);
work.setReviewTime(LocalDateTime.now());
work.setModifyTime(LocalDateTime.now());

View File

@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.lesingle.common.enums.Visibility;
import com.lesingle.common.exception.BusinessException;
import com.lesingle.common.result.PageResult;
import com.lesingle.common.util.SecurityUtil;
import com.lesingle.modules.sys.entity.SysUser;
import com.lesingle.modules.sys.mapper.SysUserMapper;
import com.lesingle.modules.ugc.entity.UgcWork;
@ -138,6 +139,13 @@ public class PublicGalleryService {
result.put("pages", pages);
result.put("user", userInfo);
result.put("creator", userInfo);
// POST /content-review/works/{id}/reject 写入的 review_note 对齐仅作者本人可见避免公开泄露
if (WorkPublishStatus.REJECTED.getValue().equals(work.getStatus())) {
Long viewerId = SecurityUtil.getCurrentUserIdOrNull();
boolean isOwner = viewerId != null && work.getUserId() != null && viewerId.equals(work.getUserId());
result.put("reviewNote", isOwner ? work.getReviewNote() : null);
}
return result;
}

View File

@ -172,6 +172,11 @@ public class PublicUserWorkService {
if (work == null || work.getIsDeleted() == 1 || !work.getUserId().equals(userId)) {
throw new BusinessException(404, "作品不存在或无权操作");
}
String st = work.getStatus();
if (WorkPublishStatus.PENDING_REVIEW.getValue().equals(st)
|| WorkPublishStatus.PUBLISHED.getValue().equals(st)) {
throw new BusinessException(400, "审核中或已发布的作品不可删除,请先撤回审核或下架");
}
work.setIsDeleted(1);
work.setModifyTime(LocalDateTime.now());
ugcWorkMapper.updateById(work);
@ -195,6 +200,49 @@ public class PublicUserWorkService {
ugcWorkMapper.updateById(work);
}
/**
* 用户撤回审核pending_review unpublished与超管
* {@code POST /content-review/works/{id}/revoke} 不同后者为 published/rejected pending_review
*/
@Transactional
public void withdrawReview(Long id, Long userId) {
UgcWork work = ugcWorkMapper.selectById(id);
if (work == null || work.getIsDeleted() == 1 || !work.getUserId().equals(userId)) {
throw new BusinessException(404, "作品不存在或无权操作");
}
if (!WorkPublishStatus.PENDING_REVIEW.getValue().equals(work.getStatus())) {
throw new BusinessException(400, "当前状态不可撤回审核");
}
LambdaUpdateWrapper<UgcWork> uw = new LambdaUpdateWrapper<>();
uw.eq(UgcWork::getId, id)
.set(UgcWork::getStatus, WorkPublishStatus.UNPUBLISHED.getValue())
.set(UgcWork::getReviewTime, null)
.set(UgcWork::getReviewerId, null)
.set(UgcWork::getReviewNote, null)
.set(UgcWork::getModifyTime, LocalDateTime.now());
ugcWorkMapper.update(null, uw);
}
/**
* 用户主动下架published unpublished发现页不可见可再次提交审核
*/
@Transactional
public void unpublish(Long id, Long userId) {
UgcWork work = ugcWorkMapper.selectById(id);
if (work == null || work.getIsDeleted() == 1 || !work.getUserId().equals(userId)) {
throw new BusinessException(404, "作品不存在或无权操作");
}
if (!WorkPublishStatus.PUBLISHED.getValue().equals(work.getStatus())) {
throw new BusinessException(400, "当前状态不可下架");
}
LambdaUpdateWrapper<UgcWork> uw = new LambdaUpdateWrapper<>();
uw.eq(UgcWork::getId, id)
.set(UgcWork::getStatus, WorkPublishStatus.UNPUBLISHED.getValue())
.set(UgcWork::getPublishTime, null)
.set(UgcWork::getModifyTime, LocalDateTime.now());
ugcWorkMapper.update(null, uw);
}
/**
* 获取作品页面
*/

View File

@ -445,6 +445,10 @@ export interface UserWork {
description: string | null;
visibility: string;
status: WorkStatus;
/**
* `POST /content-review/works/{id}/reject` `reason` `note`
* `review_note`
*/
reviewNote: string | null;
originalImageUrl: string | null;
voiceInputUrl: string | null;
@ -534,6 +538,12 @@ export const publicUserWorksApi = {
// 发布作品(进入审核)
publish: (id: number) => publicApi.post(`/public/works/${id}/publish`),
/** 撤回审核pending_review → unpublished与超管 /content-review/works/{id}/revoke 语义不同 */
withdraw: (id: number) => publicApi.post(`/public/works/${id}/withdraw`),
/** 下架published → unpublished作者本人 */
unpublish: (id: number) => publicApi.post(`/public/works/${id}/unpublish`),
// 获取绘本分页
getPages: (id: number): Promise<UserWorkPage[]> =>
publicApi.get(`/public/works/${id}/pages`),

View File

@ -11,12 +11,14 @@
<span :class="['status-tag', work.status]">{{ statusTextMap[work.status] }}</span>
</div>
<!-- 拒绝原因仅作者 + rejected-->
<div v-if="isOwner && work.status === 'rejected' && work.reviewNote" class="reject-card">
<!-- 审核拒绝原因仅作者 + rejected置于内容区顶部便于阅读-->
<div v-if="isOwner && work.status === 'rejected'" class="reject-card">
<warning-filled class="reject-icon" />
<div class="reject-body">
<div class="reject-title">未通过审核</div>
<div class="reject-content">{{ work.reviewNote }}</div>
<div class="reject-title">审核拒绝原因</div>
<div class="reject-content" :class="{ 'is-placeholder': !rejectReasonText }">
{{ rejectReasonText || '审核方未填写具体原因,如有疑问请联系客服。' }}
</div>
</div>
</div>
@ -144,8 +146,13 @@
<span>下架</span>
</button>
<!-- 删除所有状态-->
<button class="op-btn ghost-danger" :disabled="actionLoading" @click="handleDelete">
<!-- 删除审核中已发布不可删-->
<button
v-if="canDeleteWork"
class="op-btn ghost-danger"
:disabled="actionLoading"
@click="handleDelete"
>
<delete-outlined />
<span>删除</span>
</button>
@ -245,6 +252,21 @@ const isOwner = computed(() => {
return uid === oid
})
/** 审核中、已发布(上架)不可删除,需先撤回审核或下架 */
const canDeleteWork = computed(() => {
const s = work.value?.status
if (!s) return false
return s !== 'pending_review' && s !== 'published'
})
/** 与 POST /content-review/works/{id}/reject 的 `reason`+ 可选 `note`)写入的 review_note 一致 */
const rejectReasonText = computed(() => {
const w = work.value
if (!w || w.status !== 'rejected') return ''
const raw = w.reviewNote ?? (w as unknown as { review_note?: string }).review_note
return typeof raw === 'string' ? raw.trim() : ''
})
const displayLikeCount = computed(() => work.value?.likeCount || 0)
const displayFavoriteCount = computed(() => work.value?.favoriteCount || 0)
@ -335,6 +357,8 @@ function normalizeMyWorkDetail(raw: unknown): UserWork | null {
const rw = pickRemoteWorkId(w)
const base = { ...(w as unknown as UserWork), pages }
if (rw && !base.remoteWorkId) base.remoteWorkId = rw
const rn = w.reviewNote ?? w.review_note
if (typeof rn === 'string' && rn) base.reviewNote = rn
return base
}
return raw as UserWork
@ -456,9 +480,9 @@ function handleWithdraw() {
if (!work.value) return
actionLoading.value = true
try {
// TODO: POST /public/works/{id}/withdraw
message.warning('撤回接口待后端联调')
return
await publicUserWorksApi.withdraw(workId)
work.value.status = 'unpublished'
message.success('已撤回审核')
} catch (e: any) {
message.error(e.message || '撤回失败')
} finally {
@ -478,9 +502,10 @@ function handleUnpublish() {
if (!work.value) return
actionLoading.value = true
try {
// TODO: POST /public/works/{id}/unpublish
message.warning('下架接口待后端联调')
return
await publicUserWorksApi.unpublish(workId)
work.value.status = 'unpublished'
work.value.publishTime = null
message.success('已下架')
} catch (e: any) {
message.error(e.message || '下架失败')
} finally {
@ -492,6 +517,10 @@ function handleUnpublish() {
/** 删除作品 */
function handleDelete() {
if (!work.value || !canDeleteWork.value) {
message.warning('审核中或已上架的作品不可删除,请先撤回审核或下架后再试')
return
}
showConfirm(
'删除作品',
'删除后无法恢复,确认要删除这个作品吗?',
@ -690,6 +719,11 @@ $accent: #ec4899;
font-size: 13px;
color: #4b5563;
line-height: 1.6;
&.is-placeholder {
color: #9ca3af;
font-style: italic;
}
}
.info-card {