feat: 套餐审核支持通过时同时发布

- 后端 PackageReviewRequest 新增 publish 字段
- 后端 CoursePackageService.reviewPackage 支持审核通过后直接发布
- 前端审核弹窗拆分为"通过"和"通过并发布"两个按钮

状态流转:
- 驳回: status → REJECTED
- 仅通过: status → APPROVED
- 通过并发布: status → PUBLISHED

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
En 2026-03-17 15:00:49 +08:00
parent 459fa434ac
commit d57affd2ee
5 changed files with 106 additions and 54 deletions

View File

@ -97,7 +97,7 @@ export function submitPackage(id: number | string) {
}
// 审核套餐
export function reviewPackage(id: number | string, data: { approved: boolean; comment?: string }) {
export function reviewPackage(id: number | string, data: { approved: boolean; comment?: string; publish?: boolean }) {
return http.post(`/v1/admin/packages/${id}/review`, data);
}

View File

@ -5,7 +5,7 @@
<template #extra>
<a-space>
<a-select v-model:value="filters.status" placeholder="全部状态" style="width: 120px" @change="fetchPackages">
<a-select-option value="PENDING_REVIEW">待审核</a-select-option>
<a-select-option value="PENDING">待审核</a-select-option>
<a-select-option value="REJECTED">已驳回</a-select-option>
</a-select>
<a-button @click="fetchPackages">
@ -39,8 +39,8 @@
</a-tag>
</template>
<template v-else-if="column.key === 'status'">
<a-tag :color="record.status === 'PENDING_REVIEW' ? 'processing' : 'error'">
{{ record.status === 'PENDING_REVIEW' ? '待审核' : '已驳回' }}
<a-tag :color="record.status === 'PENDING' ? 'processing' : 'error'">
{{ record.status === 'PENDING' ? '待审核' : '已驳回' }}
</a-tag>
</template>
<template v-else-if="column.key === 'submittedAt'">
@ -48,7 +48,7 @@
</template>
<template v-else-if="column.key === 'actions'">
<a-space>
<a-button v-if="record.status === 'PENDING_REVIEW'" type="primary" size="small" @click="showReviewModal(record)">
<a-button v-if="record.status === 'PENDING'" type="primary" size="small" @click="showReviewModal(record)">
审核
</a-button>
<a-button v-if="record.status === 'REJECTED'" size="small" @click="viewRejectReason(record)">
@ -107,12 +107,15 @@
</a-form>
<div class="modal-footer">
<a-space v-if="currentPackage.status === 'PENDING_REVIEW'">
<a-space v-if="currentPackage.status === 'PENDING'">
<a-button @click="closeReviewModal">取消</a-button>
<a-button type="default" danger :loading="reviewing" @click="rejectPackage">
驳回
</a-button>
<a-button type="primary" :loading="reviewing" @click="approvePackage">
<a-button :loading="reviewing" @click="approveOnly">
通过
</a-button>
<a-button type="primary" :loading="reviewing" @click="approveAndPublish">
通过并发布
</a-button>
</a-space>
@ -144,7 +147,7 @@ const loadingDetail = ref(false);
const packages = ref<CoursePackage[]>([]);
const filters = reactive<{ status?: string }>({
status: 'PENDING_REVIEW',
status: 'PENDING',
});
const pagination = reactive({
@ -227,7 +230,8 @@ const closeReviewModal = () => {
currentPackage.value = null;
};
const approvePackage = async () => {
//
const approveOnly = async () => {
if (!currentPackage.value) return;
reviewing.value = true;
@ -235,6 +239,28 @@ const approvePackage = async () => {
await reviewPackage(currentPackage.value.id, {
approved: true,
comment: reviewComment.value || '审核通过',
publish: false,
});
message.success('审核通过');
closeReviewModal();
fetchPackages();
} catch (error: any) {
message.error(error.response?.data?.message || '审核失败');
} finally {
reviewing.value = false;
}
};
//
const approveAndPublish = async () => {
if (!currentPackage.value) return;
reviewing.value = true;
try {
await reviewPackage(currentPackage.value.id, {
approved: true,
comment: reviewComment.value || '审核通过',
publish: true,
});
message.success('审核通过,套餐已发布');
closeReviewModal();

View File

@ -1,12 +1,13 @@
package com.reading.platform.controller.admin;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.reading.platform.common.annotation.RequireRole;
import com.reading.platform.common.enums.UserRole;
import com.reading.platform.common.security.SecurityUtils;
import com.reading.platform.common.response.PageResult;
import com.reading.platform.common.response.Result;
import com.reading.platform.dto.request.PackageCreateRequest;
import com.reading.platform.dto.request.PackageGrantRequest;
import com.reading.platform.dto.request.PackageReviewRequest;
import com.reading.platform.dto.response.CoursePackageResponse;
import com.reading.platform.entity.CoursePackage;
import com.reading.platform.service.CoursePackageService;
@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
/**
* 课程套餐控制器超管端
@ -49,24 +51,25 @@ public class AdminPackageController {
@PostMapping
@Operation(summary = "创建套餐")
@RequireRole(UserRole.ADMIN)
public Result<CoursePackage> create(@Valid @RequestBody PackageCreateRequest request) {
return Result.success(packageService.createPackage(
public Result<CoursePackageResponse> create(@Valid @RequestBody PackageCreateRequest request) {
CoursePackage pkg = packageService.createPackage(
request.getName(),
request.getDescription(),
request.getPrice(),
request.getDiscountPrice(),
request.getDiscountType(),
request.getGradeLevels()
));
);
return Result.success(packageService.findOnePackage(pkg.getId()));
}
@PutMapping("/{id}")
@Operation(summary = "更新套餐")
@RequireRole(UserRole.ADMIN)
public Result<CoursePackage> update(
public Result<CoursePackageResponse> update(
@PathVariable Long id,
@RequestBody PackageCreateRequest request) {
return Result.success(packageService.updatePackage(
packageService.updatePackage(
id,
request.getName(),
request.getDescription(),
@ -74,7 +77,8 @@ public class AdminPackageController {
request.getDiscountPrice(),
request.getDiscountType(),
request.getGradeLevels()
));
);
return Result.success(packageService.findOnePackage(id));
}
@DeleteMapping("/{id}")
@ -108,8 +112,14 @@ public class AdminPackageController {
@RequireRole(UserRole.ADMIN)
public Result<Void> review(
@PathVariable Long id,
@RequestBody ReviewRequest request) {
packageService.reviewPackage(id, SecurityUtils.getCurrentUserId(), request.getApproved(), request.getComment());
@Valid @RequestBody PackageReviewRequest request) {
packageService.reviewPackage(
id,
SecurityUtils.getCurrentUserId(),
request.getApproved(),
request.getComment(),
request.getPublish()
);
return Result.success();
}
@ -131,8 +141,12 @@ public class AdminPackageController {
@GetMapping("/all")
@Operation(summary = "查询所有已发布的套餐列表")
public Result<List<CoursePackage>> getPublishedPackages() {
return Result.success(packageService.findPublishedPackages());
public Result<List<CoursePackageResponse>> getPublishedPackages() {
List<CoursePackage> packages = packageService.findPublishedPackages();
List<CoursePackageResponse> responses = packages.stream()
.map(pkg -> packageService.findOnePackage(pkg.getId()))
.collect(Collectors.toList());
return Result.success(responses);
}
@PostMapping("/{id}/grant")
@ -140,7 +154,7 @@ public class AdminPackageController {
@RequireRole(UserRole.ADMIN)
public Result<Void> grantToTenant(
@PathVariable Long id,
@RequestBody GrantRequest request) {
@Valid @RequestBody PackageGrantRequest request) {
LocalDate endDate = LocalDate.parse(request.getEndDate(), DateTimeFormatter.ISO_DATE);
packageService.renewTenantPackage(
request.getTenantId(),
@ -150,33 +164,4 @@ public class AdminPackageController {
);
return Result.success();
}
/**
* 审核请求
*/
public static class ReviewRequest {
private Boolean approved;
private String comment;
public Boolean getApproved() { return approved; }
public void setApproved(Boolean approved) { this.approved = approved; }
public String getComment() { return comment; }
public void setComment(String comment) { this.comment = comment; }
}
/**
* 授权请求
*/
public static class GrantRequest {
private Long tenantId;
private String endDate;
private Long pricePaid;
public Long getTenantId() { return tenantId; }
public void setTenantId(Long tenantId) { this.tenantId = tenantId; }
public String getEndDate() { return endDate; }
public void setEndDate(String endDate) { this.endDate = endDate; }
public Long getPricePaid() { return pricePaid; }
public void setPricePaid(Long pricePaid) { this.pricePaid = pricePaid; }
}
}

View File

@ -0,0 +1,21 @@
package com.reading.platform.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 套餐审核请求
*/
@Data
@Schema(description = "套餐审核请求")
public class PackageReviewRequest {
@Schema(description = "是否通过")
private Boolean approved;
@Schema(description = "审核意见")
private String comment;
@Schema(description = "是否同时发布(仅当 approved=true 时有效)")
private Boolean publish;
}

View File

@ -300,10 +300,15 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
/**
* 审核套餐
* @param id 套餐ID
* @param userId 审核人ID
* @param approved 是否通过
* @param comment 审核意见
* @param publish 是否同时发布仅当 approved=true 时有效
*/
@Transactional(rollbackFor = Exception.class)
public void reviewPackage(Long id, Long userId, Boolean approved, String comment) {
log.info("审核套餐id={}, userId={}, approved={}", id, userId, approved);
public void reviewPackage(Long id, Long userId, Boolean approved, String comment, Boolean publish) {
log.info("审核套餐id={}, userId={}, approved={}, publish={}", id, userId, approved, publish);
CoursePackage pkg = packageMapper.selectById(id);
if (pkg == null) {
log.warn("套餐不存在id={}", id);
@ -315,12 +320,27 @@ public class CoursePackageService extends ServiceImpl<CoursePackageMapper, Cours
throw new BusinessException("只有待审核状态的套餐可以审核");
}
pkg.setStatus(approved ? CourseStatus.APPROVED.getCode() : CourseStatus.REJECTED.getCode());
// 如果驳回
if (!Boolean.TRUE.equals(approved)) {
pkg.setStatus(CourseStatus.REJECTED.getCode());
}
// 如果通过且同时发布
else if (Boolean.TRUE.equals(publish)) {
pkg.setStatus(CourseStatus.PUBLISHED.getCode());
pkg.setPublishedAt(LocalDateTime.now());
}
// 仅通过
else {
pkg.setStatus(CourseStatus.APPROVED.getCode());
}
pkg.setReviewedAt(LocalDateTime.now());
pkg.setReviewedBy(userId);
pkg.setReviewComment(comment);
packageMapper.updateById(pkg);
log.info("套餐审核成功id={}, result={}", id, approved ? "approved" : "rejected");
log.info("套餐审核成功id={}, result={}", id,
!Boolean.TRUE.equals(approved) ? "rejected" :
(Boolean.TRUE.equals(publish) ? "approved_and_published" : "approved"));
}
/**