
一场演唱会结束后的第二天往往是素材传播最密集的时间段。运营团队如果想做一个粉丝视角的回顾合集把网上出现的现场图、观众视角视频、官方预告片混在一起展示最先遇到的麻烦通常不是剪辑效果而是这些素材从哪里来、是否经过授权、如果有人通过私信要求删除时后台该按什么流程执行。很多内容管理后台只解决“文件上传、列表展示、链接删除”却没有把“来源信息”和“删除请求”当成一等数据来建模结果真正要处理投诉时只能人肉查聊天记录删完文件后无法形成可追溯的下架记录。这篇文章就以视频素材合集管理项目为背景从表结构、接口、审核流程、对象存储操作几个层面实现一套基于 Spring Boot 的内容素材登记与合规下架系统。整套方案跑通后一个文件能够回答四类问题它从哪里采集、授权状态是什么、是否被投诉过、删除流程执行到了哪一步。1. 先想清楚素材合集管理的难点不是文件存储而是来源和状态1.1 为什么“来源”必须和文件一起存储在普通文件上传模块里业务方只需要关心文件名、文件大小、上传者、上传时间。但演唱会回顾类页面不一样。页面里的视频和图片很可能来自不同作者的同一个线上发布渠道有些是作者主动投稿有些是运营团队从平台公开内容里收集有些则来自其他粉丝整理的合集。这些素材的授权边界完全不同。如果不记录来源删除流程就无法判断“该联系谁去确认授权”也无法判断“删除请求是否真的是由原作者或版权权利人发出”。因此材料主表里不能只有存储路径还必须包含原始作者、来源地址、来源平台、授权类型这四类字段。这里的授权类型至少应该区分四档授权类型含义使用建议AUTHORIZED已获得作者或权利人明确授权可以进入合集页面正式展示PUBLIC_SHARE仅在公开平台看到作者主动分享建议用于展示前人工复核UNKNOWN无法确认原始来源不推荐直接进入合集页面NEED_DELETE已收到删除请求或作者表示反对禁止继续展示进入下架流程“来源”之所以要成为核心字段还有一个实际原因当同一个视频被多次转载时删除请求可能针对的是原始作者的视频也可能是管理员收到私信后针对页面上的某一条链接。如果系统只有一条素材记录而没有来源链运营人员很可能删错对象。正确做法是每个文件上传成功后立即登记来源 URL、作者昵称和来源平台后续收到任何投诉都能先通过素材编号定位到这条来源链。1.2 删除不是 delete 一条记录那么简单数据库只要执行一条 UPDATE 或 DELETE 语句记录状态就会变化但这种“删除”离真正下架还很远。一个视频或图片在页面里被引用通常经过三层数据库记录、对象存储文件、CDN 或代理缓存。只有这三层都处理完用户访问时才会完全看不到。在工程实现里处理删除请求至少要经过四个阶段请求登记、运营审核、文件删除、结果通知。其中文件删除又分为先标记再删除再从对象存储移除最后做缓存失效。直接删文件看起来很彻底但如果页面还在通过旧链接引用就会出现图裂或播放失败。更稳妥的是先把素材状态改为 DELETING让前端在展示时先不渲染这条素材再执行存储删除最后把状态修改为 DELETED。这里还有一个容易忽略的问题删除操作不能和数据库事务放在同一个方法里。数据库事务适合保护“订单状态变化”“账户余额扣减”这类操作但对 MinIO 这类外部存储删除文件是一个不可回滚的外部动作。如果先删了文件再更新数据库时抛异常事务回滚会让数据库里仍然显示文件存在而实际文件已经不存在。所以正确流程是把素材状态先改为 DELETING 并提交然后调用对象存储删除接口确认删除成功后再把状态更新为 DELETED。注意收到私聊里的删除请求不等于系统必须立即删文件。删除请求只是触发审核的任务审核通过后执行下架审核不通过时应该记录理由并通知请求方。1.3 来自私聊渠道的删除请求需要被建模用户材料里提到的“建议者私聊删除”在业务上是一个很典型的需求但技术实现不能停留在“运营人员收到一条私信然后手动处理”。私聊只是一个触点真正进入系统后它应该被转换为一条具备唯一编号、请求来源、请求人、联系渠道、请求原因、证明材料、处理状态和审核人的删除工单。这样的建模有两个好处。第一同一个素材被多次投诉时后台可以合并处理避免重复删除一个文件。第二请求状态可查询运营人员可以追踪到每一条私聊对应的工单处理到哪一步。如果只是把“私聊”记录下来没有形成结构化请求后续审计时很难解释为什么某个视频被下架。从系统设计角度看删除请求应有独立的状态机PENDING、PASSED、REJECTED、CANCELLED。Pending 表示待审核Passed 表示审核通过并已进入删除流程Rejected 表示审核不通过Cancelled 表示请求方主动撤销。素材本身则使用 ACTIVE、DELETING、DELETED、DELETE_FAILED 四类状态避免把“请求状态”和“文件状态”混在一张表里。2. 环境准备用 Spring Boot 3 搭建素材下架管理服务2.1 技术选型与依赖这套示例不需要复杂的中间件核心依赖是 Spring Boot、Spring Data JPA、MySQL 和 MinIO。Spring Data JPA 可以快速完成实体与表的映射MinIO 负责存放图片和视频文件MySQL 保存素材元数据和删除工单。如果只是想验证流程数据库可以使用本地 MySQL 8.0对象存储可以使用 Docker 启动一个 MinIO 服务。下面的版本只是示例实际项目落地前需要确认与当前团队的 Spring Boot 版本一致。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.minio/groupId artifactIdminio/artifactId version8.5.12/version /dependency dependency groupIdcommons-codec/groupId artifactIdcommons-codec/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies说明commons-codec 主要用于计算文件 SHA-256用于指纹去重和删除后防重复上传。MinIO 的版本需要在正式项目里核对不同 8.x 小版本的 API 有细微差别。2.2 配置 MySQL 和 MinIO在application.yml中配置数据源、JPA 和 MinIO。MinIO 的 endpoint 在测试环境通常是http://127.0.0.1:9000accessKey 和 secretKey 对应容器启动时设置的值。server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/material_console?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none show-sql: true open-in-view: false minio: endpoint: http://127.0.0.1:9000 access-key: minioadmin secret-key: minioadmin bucket: material-bucketJPA 的ddl-auto设置为none表结构由数据库迁移脚本或手工 SQL 创建。这样做的原因是生产环境很少允许 Hibernate 自动改表结构显式管理 SQL 可以避免误删字段。如果需要启动本地 MinIO 做测试可以执行docker run -d \ --name minio-material \ -p 9000:9000 \ -p 9001:9001 \ -e MINIO_ROOT_USERminioadmin \ -e MINIO_ROOT_PASSWORDminioadmin \ minio/minio server /data --console-address :9001启动后用浏览器打开http://127.0.0.1:9001默认用户名和密码都是minioadmin。先创建material-bucket或者由代码在启动时自动创建。Configuration public class MinioConfig { Value(${minio.endpoint}) private String endpoint; Value(${minio.access-key}) private String accessKey; Value(${minio.secret-key}) private String secretKey; Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } }2.3 项目目录结构与模块划分示例项目按素材、下架请求、存储、通知四个模块拆分。不要把所有 Controller、Service、Entity 都堆在同一个包下否则随着素材类型增加代码会迅速变得难维护。src/main/java/com/example/materialconsole ├── MaterialConsoleApplication.java ├── common │ └── BizException.java ├── config │ └── MinioConfig.java ├── material │ ├── controller │ │ ├── MaterialController.java │ │ └── TakeDownController.java │ ├── dto │ │ ├── CreateTakeDownRequest.java │ │ └── MaterialUploadResponse.java │ ├── entity │ │ ├── ContentMaterial.java │ │ └── TakeDownRequest.java │ ├── repository │ │ ├── ContentMaterialRepository.java │ │ └── TakeDownRequestRepository.java │ └── service │ ├── MaterialService.java │ └── TakeDownService.java ├── storage │ └── MinioStorageService.java └── notify ├── MaterialNotifier.java └── NotifyMessage.java这个结构的关键点在于MaterialService 负责素材上传和状态流转TakeDownService 负责删除工单的创建和审批存储逻辑单独隔离在 MinioStorageService 中。这样做之后如果将来对象存储换成阿里云 OSS 或腾讯云 COS只需要替换 storage 包中的实现。3. 表结构设计让每个文件都能回答“从哪里来、现在是什么状态”3.1 素材主表记录文件本身和来源信息素材主表中的字段可以分成三组文件标识、内容业务信息、来源与授权信息。文件标识包括素材编号、文件类型、存储桶、对象键、SHA-256 值。业务信息包括标题、状态。来源与授权信息包括原创作者、来源地址、来源平台和授权类型。CREATE TABLE content_material ( id BIGINT PRIMARY KEY AUTO_INCREMENT, material_no VARCHAR(64) NOT NULL, title VARCHAR(255) NOT NULL, file_type VARCHAR(20) NOT NULL, bucket_name VARCHAR(100) NOT NULL, object_key VARCHAR(500) NOT NULL, content_sha256 CHAR(64) NOT NULL, original_author VARCHAR(255), source_url VARCHAR(1000), source_platform VARCHAR(100), license_type VARCHAR(30) NOT NULL DEFAULT UNKNOWN, status VARCHAR(20) NOT NULL DEFAULT ACTIVE, delete_reason VARCHAR(1000), version INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_material_no (material_no), KEY idx_sha256_status (content_sha256, status), KEY idx_status (status) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;material_no 是业务编号在接口层暴露避免把数据库自增 ID 直接对外返回。object_key 是对象存储在 MinIO 中的完整路径比如material/2025-06-01/uuid-001.mp4。source_url 保存素材公开平台地址original_author 保存作者昵称。SHA-256 的作用不只是文件去重还是防止已删除文件再次被上传的重要依据。文件删除后如果重新上传时系统通过 SHA-256 发现存在相同的历史 DELETED 记录就需要触发人工审核而不是让文件直接回到合集页面。3.2 删除请求表把私聊反馈变成可流转的工单删除请求表对应收到站内私信、评论区留言或邮箱反馈后的结构化登记。由于请求来源不同不能把所有信息都塞在“反馈内容”一个字段里。CREATE TABLE take_down_request ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_no VARCHAR(64) NOT NULL, material_no VARCHAR(64) NOT NULL, request_biz_key CHAR(64) NOT NULL, request_source VARCHAR(30) NOT NULL, requester_name VARCHAR(255), contact_channel VARCHAR(30), contact_value VARCHAR(255), reason VARCHAR(1000) NOT NULL, proof_url VARCHAR(1000), status VARCHAR(20) NOT NULL DEFAULT PENDING, reject_reason VARCHAR(1000), handled_by VARCHAR(64), handled_at DATETIME, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_request_no (request_no), UNIQUE KEY uk_request_biz_key (request_biz_key), KEY idx_material_no (material_no) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;request_source 可以填写 PRIVATE_MESSAGE、EMAIL、PLATFORM_REPORT 等用来表示这条请求来自哪个渠道。request_biz_key 是一个 SHA-256 值由 material_no、connectValue 和 reason 一起计算作用是幂等同一投诉人针对同一个素材提交相同原因的请求时系统不应该创建第二条工单。status 的流转方向必须明确不能允许从 PASSED 回退到 PENDING否则审核后的记录会被反复修改。3.3 状态机设计和更新时间说明素材状态和删除请求状态需要分开。素材状态针对“文件还能不能展示”删除请求状态针对“请求走到了哪一步”。实体状态含义ContentMaterialACTIVE正常展示ContentMaterialDELETING已进入删除流程等待外部存储删除ContentMaterialDELETED文件已删除或已停止展示ContentMaterialDELETE_FAILED删除失败需要重试或人工介入TakeDownRequestPENDING待运营审核TakeDownRequestPASSED审核通过已经触发删除流程TakeDownRequestREJECTED审核不通过TakeDownRequestCANCELLED请求方撤销created_at 和 updated_at 字段建议在数据库层写入默认值避免每个 Service 方法都重复赋值。ALTER TABLE content_material MODIFY COLUMN created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, MODIFY COLUMN updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;4. 实现上传和删除请求给素材打上可追溯标记4.1 上传素材时校验指纹和是否处于黑名单上传素材时不能“拿到文件直接存到 MinIO 再插一条记录”。先计算 SHA-256然后检查是否存在相同哈希且状态为 DELETED 的历史素材。如果存在说明这个文件之前被下架过需要人工确认原因。Service public class MaterialService { private final ContentMaterialRepository materialRepository; private final MinioStorageService storageService; public MaterialService(ContentMaterialRepository materialRepository, MinioStorageService storageService) { this.materialRepository materialRepository; this.storageService storageService; } public ContentMaterial upload(MultipartFile file, String title, String originalAuthor, String sourceUrl, String sourcePlatform, String licenseType) { String sha256; try { sha256 org.apache.commons.codec.digest.DigestUtils.sha256Hex(file.getInputStream()); } catch (IOException e) { throw new BizException(读取文件失败); } boolean deletedBefore materialRepository.existsByContentSha256AndStatus(sha256, DELETED); if (deletedBefore) { throw new BizException(该文件存在历史删除记录请先确认授权或执行人工审核); } String objectKey material/ LocalDate.now() / UUID.randomUUID() - file.getOriginalFilename(); storageService.put(file, objectKey); ContentMaterial material new ContentMaterial(); material.setMaterialNo(CM System.currentTimeMillis()); material.setTitle(title); material.setFileType(file.getContentType()); material.setBucketName(storageService.getDefaultBucket()); material.setObjectKey(objectKey); material.setContentSha256(sha256); material.setOriginalAuthor(originalAuthor); material.setSourceUrl(sourceUrl); material.setSourcePlatform(sourcePlatform); material.setLicenseType(licenseType); material.setStatus(ACTIVE); return materialRepository.save(material); } }这里需要特别解释一个顺序问题先计算 SHA-256再上传到 MinIO。如果先上传再计算哈希遇到文件被删除过或校验失败的情况对象存储里已经多了一个无用文件还要补做一次清理。计算哈希会消耗一定 CPU但对图片和短视频来说可以接受。MinioStorageService.put方法示例Service public class MinioStorageService { private final MinioClient minioClient; Value(${minio.bucket}) private String defaultBucket; public MinioStorageService(MinioClient minioClient) { this.minioClient minioClient; } public void put(MultipartFile file, String objectKey) { try { boolean bucketExists minioClient.bucketExists( BucketExistsArgs.builder().bucket(defaultBucket).build()); if (!bucketExists) { minioClient.makeBucket(MakeBucketArgs.builder().bucket(defaultBucket).build()); } minioClient.putObject(PutObjectArgs.builder() .bucket(defaultBucket) .object(objectKey) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); } catch (Exception e) { throw new BizException(对象存储上传失败); } } public void delete(Material material) { try { minioClient.removeObject(RemoveObjectArgs.builder() .bucket(material.getBucketName()) .object(material.getObjectKey()) .build()); } catch (Exception e) { throw new BizException(对象存储删除失败); } } }注意MinIO 的客户端 API 版本不同时removeObject的包名和参数类可能不同实际项目中需要先查询当前依赖版本的接口签名。4.2 创建删除请求先审核再执行删除删除请求创建接口接收的并不只是“投诉文本”。ContactChannel 和 contactValue 必须结构化否则后续要通过站内私信回复请求者时系统不知道应该往哪里发消息。public class CreateTakeDownRequest { private String requesterName; private String contactChannel; private String contactValue; private String reason; private String proofUrl; }创建删除工单的逻辑在 TakeDownService 中Service public class TakeDownService { private final ContentMaterialRepository materialRepository; private final TakeDownRequestRepository requestRepository; private final MaterialNotifier notifier; public TakeDownRequest create(String materialNo, CreateTakeDownRequest request) { ContentMaterial material materialRepository.findByMaterialNo(materialNo) .orElseThrow(() - new BizException(素材不存在)); if (DELETED.equals(material.getStatus())) { throw new BizException(素材已经处于删除状态); } String bizKey DigestUtils.sha256Hex( materialNo : request.getContactValue() : request.getReason()); if (requestRepository.existsByRequestBizKey(bizKey)) { throw new BizException(相同删除请求已存在请勿重复提交); } TakeDownRequest entity new TakeDownRequest(); entity.setRequestNo(TD System.currentTimeMillis()); entity.setMaterialNo(materialNo); entity.setRequestBizKey(bizKey); entity.setRequestSource(PRIVATE_MESSAGE); entity.setRequesterName(request.getRequesterName()); entity.setContactChannel(request.getContactChannel()); entity.setContactValue(request.getContactValue()); entity.setReason(request.getReason()); entity.setProofUrl(request.getProofUrl()); entity.setStatus(PENDING); return requestRepository.save(entity); } }创建工单后不应该立即调用删除文件操作。原因在于“私聊删除”场景里的请求者不一定就是素材原作者。如果某个用户看到不喜欢的内容就提交删除请求系统直接删除会带来风险。正确做法是创建 PENDING 工单再调用pass方法由运营人员审核。4.3 删除的幂等处理和通知落库审核通过后的pass方法参考下面的代码Transactional public void pass(String requestNo, String operator) { TakeDownRequest request requestRepository.findByRequestNo(requestNo) .orElseThrow(() - new BizException(删除请求不存在)); if (!PENDING.equals(request.getStatus())) { throw new BizException(当前请求状态不允许审核通过); } ContentMaterial material materialRepository.findByMaterialNo(request.getMaterialNo()) .orElseThrow(() - new BizException(素材不存在)); request.setStatus(PASSED); request.setHandledBy(operator); request.setHandledAt(LocalDateTime.now()); requestRepository.save(request); material.setStatus(DELETING); material.setDeleteReason(request.getReason()); materialRepository.save(material); }这个阶段不要直接删除对象存储文件。你可以再增加一个DoDeleteService扫描所有 DELETING 超过 1 分钟的素材先确认请求审核已经通过再执行 MinIO 删除操作这样删除流程被拆成多个独立步骤数据库事务不会因为外部存储调用而长时间打开。如果希望在一个请求里完成全部流程测试环境可以使用下面的简版方法但生产环境建议按任务表方式异步执行public void executeDelete(ContentMaterial material) { material.setStatus(DELETING); materialRepository.save(material); try { storageService.delete(material); material.setStatus(DELETED); materialRepository.save(material); } catch (Exception e) { material.setStatus(DELETE_FAILED); material.setDeleteReason(e.getMessage()); materialRepository.save(material); throw e; } }执行删除后需要通知请求方并落一条通知日志。通知日志不是可有可无的辅助功能它是“私聊删除”闭环里最重要的审计证据。建议新建notification_log表request_no、channel、target、content_template、status、error_message、created_at。每次发送前先按 request_no 和 channel 查询是否已经存在成功记录如果已经发送过直接跳过。5. 用接口把流程串起来上传、投诉、审核、删除5.1 需要暴露的 API 与请求参数一个最小闭环需要四类接口素材上传、创建删除请求、审核通过、查询素材状态。方法路径作用POST/api/material/upload上传素材POST/api/material/{materialNo}/take-down对素材发起删除请求POST/api/take-down/{requestNo}/reject驳回删除请求POST/api/take-down/{requestNo}/pass审核通过并执行删除GET/api/material/{materialNo}查询素材状态Controller 层尽量保持薄只负责参数转换和异常包装。5.2 Controller 层代码示例MaterialController 示例RestController RequestMapping(/api/material) public class MaterialController { private final MaterialService materialService; public MaterialController(MaterialService materialService) { this.materialService materialService; } PostMapping(/upload) public ContentMaterial upload(RequestParam(file) MultipartFile file, RequestParam String title, RequestParam(required false) String originalAuthor, RequestParam(required false) String sourceUrl, RequestParam(required false) String sourcePlatform, RequestParam(defaultValue UNKNOWN) String licenseType) { return materialService.upload(file, title, originalAuthor, sourceUrl, sourcePlatform, licenseType); } GetMapping(/{materialNo}) public ContentMaterial detail(PathVariable String materialNo) { return materialService.detail(materialNo); } }TakeDownController 示例RestController RequestMapping(/api) public class TakeDownController { private final TakeDownService takeDownService; public TakeDownController(TakeDownService takeDownService) { this.takeDownService takeDownService; } PostMapping(/material/{materialNo}/take-down) public TakeDownRequest create(PathVariable String materialNo, RequestBody CreateTakeDownRequest request) { return takeDownService.create(materialNo, request); } PostMapping(/take-down/{requestNo}/pass) public String pass(PathVariable String requestNo, RequestParam String operator) { takeDownService.pass(requestNo, operator); return ok; } }这里的 pass 方法可以包含一个参数needDeleteNow测试环境直接同步删除生产环境则异步执行。把 API 设计成同步还是异步取决于页面对下架时效的要求。如果是运营后台内部的审核操作通常不需要用户等待文件删除完成可以先返回“已通过”后台任务继续删除。5.3 消息通知与“私聊”场景对接在真实项目里“私聊删除”中的私聊可能发生在站内私信、微信公众号后台、邮件或客服系统。系统需要抽象一个 Notifier 接口而不是每个 Service 都直接调用某一个 IM 客户端。public interface MaterialNotifier { boolean support(String channel); void send(NotifyMessage message); }NotifyMessage 中至少包含 requestNo、contactChannel、contactValue、title、content 五个字段。示例NotifyMessage message new NotifyMessage(); message.setRequestNo(request.getRequestNo()); message.setContactChannel(request.getContactChannel()); message.setContactValue(request.getContactValue()); message.setTitle(素材删除结果通知); message.setContent(您反馈的素材已处理 detail);如果 contactChannel 是站内私信contactValue 就是用户 ID如果是邮件contactValue 就是邮箱。将来接入新的渠道时只需要增加一个实现 MaterialNotifier 的类并通过 support 方法判断。6. 本地验证从上传到删除的全链路测试6.1 准备测试数据和启动服务按前面的 SQL 在 MySQL 中创建content_material表和take_down_request表然后启动 Spring Boot 应用和 MinIO。第一次启动验证时重点检查三个点应用能否连上 MySQL、MinIO bucket 是否创建成功、上传接口能否正确写入素材记录。准备一个测试图片test-photo.jpg执行上传请求curl -X POST http://127.0.0.1:8080/api/material/upload \ -F filetest-photo.jpg \ -F title现场舞台图 \ -F originalAuthor网友小A \ -F sourceUrlhttps://example.com/post/1001 \ -F sourcePlatformexample \ -F licenseTypePUBLIC_SHARE正常情况下返回内容包含 materialNo 和 statusACTIVE。6.2 验证删除请求与重复提交场景先发起一条删除请求curl -X POST http://127.0.0.1:8080/api/material/CM1717000000000/take-down \ -H Content-Type: application/json \ -d { requesterName: 原作者小A, contactChannel: PRIVATE_MESSAGE, contactValue: user_1001, reason: 未授权使用我的照片, proofUrl: https://example.com/author-info }返回结果中应有 requestNo并且 statusPENDING。再次提交完全相同的 JSON系统应该抛异常或返回“相同删除请求已存在”这是幂等校验的预期结果。执行审核通过curl -X POST http://127.0.0.1:8080/api/take-down/TD1717000000000/pass?operatoradmin如果实现中直接调用 executeDelete那么随后查询素材状态应该变成 DELETED。curl http://127.0.0.1:8080/api/material/CM1717000000000返回的 status 字段为 DELETED。6.3 验证 MinIO 文件确实已删除素材状态变成 DELETED 只说明数据库层面不再