feat: 公众端作品详情与审核流程对齐(撤回下架、驳回原因、删除限制)
Made-with: Cursor
This commit is contained in:
parent
56bddb5206
commit
44ad1746f3
@ -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 = "请求体 JSON:reason(驳回原因)必填,写入作品 review_note;note 可选。与公众端作品详情展示的审核拒绝原因一致。")
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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());
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取作品页面
|
||||
*/
|
||||
|
||||
@ -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`),
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user