diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/ContentReviewController.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/ContentReviewController.java index 4967eb9..ba405bd 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/ContentReviewController.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/ContentReviewController.java @@ -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 reject(@PathVariable Long id, @RequestBody Map body) { + @Operation(summary = "驳回", description = "请求体 JSON:reason(驳回原因)必填,写入作品 review_note;note 可选。与公众端作品详情展示的审核拒绝原因一致。") + public Result 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(); } diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicUserWorkController.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicUserWorkController.java index fad3754..756f7ef 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicUserWorkController.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/controller/PublicUserWorkController.java @@ -83,6 +83,22 @@ public class PublicUserWorkController { return Result.success(); } + @PostMapping("/{id}/withdraw") + @Operation(summary = "撤回审核(审核中 → 未发布,作者本人)") + public Result withdraw(@PathVariable Long id) { + Long userId = SecurityUtil.getCurrentUserId(); + publicUserWorkService.withdrawReview(id, userId); + return Result.success(); + } + + @PostMapping("/{id}/unpublish") + @Operation(summary = "下架作品(已发布 → 未发布,作者本人)") + public Result unpublish(@PathVariable Long id) { + Long userId = SecurityUtil.getCurrentUserId(); + publicUserWorkService.unpublish(id, userId); + return Result.success(); + } + @GetMapping("/{id}/pages") @Operation(summary = "获取作品页面") public Result> getPages(@PathVariable Long id) { diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/dto/ContentReviewRejectDto.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/dto/ContentReviewRejectDto.java new file mode 100644 index 0000000..7984812 --- /dev/null +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/dto/ContentReviewRejectDto.java @@ -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; +} diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicContentReviewService.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicContentReviewService.java index 75fe15a..fd808fe 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicContentReviewService.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicContentReviewService.java @@ -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()); diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicGalleryService.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicGalleryService.java index 94eafc4..e103509 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicGalleryService.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicGalleryService.java @@ -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; } diff --git a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicUserWorkService.java b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicUserWorkService.java index 520a232..fe5620b 100644 --- a/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicUserWorkService.java +++ b/lesingle-creation-backend/src/main/java/com/lesingle/modules/pub/service/PublicUserWorkService.java @@ -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 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 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); + } + /** * 获取作品页面 */ diff --git a/lesingle-creation-frontend/src/api/public.ts b/lesingle-creation-frontend/src/api/public.ts index 47e9e9a..ae17057 100644 --- a/lesingle-creation-frontend/src/api/public.ts +++ b/lesingle-creation-frontend/src/api/public.ts @@ -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 => publicApi.get(`/public/works/${id}/pages`), diff --git a/lesingle-creation-frontend/src/views/public/works/Detail.vue b/lesingle-creation-frontend/src/views/public/works/Detail.vue index e855036..359e6cb 100644 --- a/lesingle-creation-frontend/src/views/public/works/Detail.vue +++ b/lesingle-creation-frontend/src/views/public/works/Detail.vue @@ -11,12 +11,14 @@ {{ statusTextMap[work.status] }} - -
+ +
-
未通过审核
-
{{ work.reviewNote }}
+
审核拒绝原因
+
+ {{ rejectReasonText || '审核方未填写具体原因,如有疑问请联系客服。' }} +
@@ -144,8 +146,13 @@ 下架 - - @@ -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 {