feat(education): define content export redaction contract

This commit is contained in:
2026-07-31 12:47:06 +08:00
parent 2a53e8e2c0
commit 3744c8c3be
8 changed files with 357 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.education.controller.admin.contentexport;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.security.core.service.SecurityFrameworkService;
import cn.iocoder.yudao.module.education.controller.admin.contentexport.vo.ContentExportRequestReqVO;
import cn.iocoder.yudao.module.education.service.contentexport.ContentExportAnswerMode;
import cn.iocoder.yudao.module.education.service.contentexport.ContentExportPolicy;
import cn.iocoder.yudao.module.education.service.contentexport.ContentExportPolicyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - 教育内容导出请求")
@RestController
@RequestMapping("/education/content-exports")
@Validated
@ConditionalOnProperty(prefix = "yudao.education", name = "enabled", havingValue = "true")
public class ContentExportController {
private static final String ANSWERS_PERMISSION = "education:content-export:answers";
private final ContentExportPolicyService policyService;
private final SecurityFrameworkService securityFrameworkService;
public ContentExportController(ContentExportPolicyService policyService,
SecurityFrameworkService securityFrameworkService) {
this.policyService = policyService;
this.securityFrameworkService = securityFrameworkService;
}
@PostMapping
@Operation(summary = "评估内容导出请求的字段策略")
@PreAuthorize("@ss.hasPermission('education:content-export')")
public CommonResult<ContentExportPolicy> requestExport(
@Valid @RequestBody ContentExportRequestReqVO reqVO) {
if (reqVO.getAnswerMode() == ContentExportAnswerMode.INCLUDE
&& !securityFrameworkService.hasPermission(ANSWERS_PERMISSION)) {
throw new AccessDeniedException("Including answers requires " + ANSWERS_PERMISSION);
}
return success(policyService.evaluate(reqVO.getAnswerMode()));
}
}

View File

@@ -0,0 +1,13 @@
package cn.iocoder.yudao.module.education.controller.admin.contentexport.vo;
import cn.iocoder.yudao.module.education.service.contentexport.ContentExportAnswerMode;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 内容导出请求")
@Data
public class ContentExportRequestReqVO {
@Schema(description = "答案处理方式;省略时默认脱敏", example = "REDACT")
private ContentExportAnswerMode answerMode;
}

View File

@@ -0,0 +1,6 @@
package cn.iocoder.yudao.module.education.service.contentexport;
public enum ContentExportAnswerMode {
REDACT,
INCLUDE
}

View File

@@ -0,0 +1,8 @@
package cn.iocoder.yudao.module.education.service.contentexport;
import java.util.Set;
public record ContentExportPolicy(ContentExportAnswerMode answerMode,
Set<String> includedFields,
Set<String> excludedFields) {
}

View File

@@ -0,0 +1,6 @@
package cn.iocoder.yudao.module.education.service.contentexport;
public interface ContentExportPolicyService {
ContentExportPolicy evaluate(ContentExportAnswerMode requestedAnswerMode);
}

View File

@@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.education.service.contentexport;
import org.springframework.stereotype.Service;
import java.util.LinkedHashSet;
import java.util.Set;
@Service
public class ContentExportPolicyServiceImpl implements ContentExportPolicyService {
private static final Set<String> CONTENT_FIELDS = Set.of(
"id", "contentVersion", "stem", "type", "typeLabel", "difficulty",
"questionContent", "options.label", "options.content", "options.order",
"status", "isPublished", "subjectId", "nodeId", "tags", "sortOrder");
private static final Set<String> ANSWER_FIELDS = Set.of(
"correctAnswer", "explanation", "analysis", "options.isCorrect");
private static final Set<String> PRIVATE_ADMIN_FIELDS = Set.of(
"tenantId", "scope", "metadata", "creator", "updater", "createTime", "updateTime", "deleted");
@Override
public ContentExportPolicy evaluate(ContentExportAnswerMode requestedAnswerMode) {
ContentExportAnswerMode answerMode = requestedAnswerMode == null
? ContentExportAnswerMode.REDACT : requestedAnswerMode;
Set<String> includedFields = new LinkedHashSet<>(CONTENT_FIELDS);
Set<String> excludedFields = new LinkedHashSet<>(PRIVATE_ADMIN_FIELDS);
if (answerMode == ContentExportAnswerMode.INCLUDE) {
includedFields.addAll(ANSWER_FIELDS);
} else {
excludedFields.addAll(ANSWER_FIELDS);
}
return new ContentExportPolicy(answerMode, Set.copyOf(includedFields), Set.copyOf(excludedFields));
}
}