diff --git a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/material/controller/MaterialDefController.java b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/material/controller/MaterialDefController.java
new file mode 100644
index 0000000..4ff0fcb
--- /dev/null
+++ b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/material/controller/MaterialDefController.java
@@ -0,0 +1,127 @@
+package cn.tianbaotong.fill.module.material.controller;
+
+import cn.tianbaotong.fill.common.api.ApiResult;
+import cn.tianbaotong.fill.module.material.dto.MaterialDefSaveReq;
+import cn.tianbaotong.fill.module.material.dto.MaterialDefVO;
+import cn.tianbaotong.fill.module.material.service.MaterialCatalog;
+import cn.tianbaotong.fill.module.material.service.MaterialDefService;
+import cn.tianbaotong.fill.security.PermissionDef;
+import cn.tianbaotong.fill.security.RequirePerm;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 材料项定义(《03-接口设计》§7.10)。
+ *
+ *
路径分两段:任务作用域的用 {@code /tasks/{id}/materials},单项操作用
+ * {@code /materials/{id}} —— 与契约一致。一个 Controller 承载两段路径,
+ * 是为了让"材料"这件事在一个文件里能看全,而不是按 URL 前缀硬拆成两个类。
+ */
+@Tag(name = "材料项定义")
+@RestController
+@RequestMapping("/api/v1")
+@RequiredArgsConstructor
+public class MaterialDefController {
+
+ private final MaterialDefService materialDefService;
+
+ @GetMapping("/tasks/{id}/materials")
+ @RequirePerm(PermissionDef.TASK_VIEW)
+ @Operation(summary = "任务的材料项列表")
+ public ApiResult> list(@PathVariable Long id) {
+ return ApiResult.ok(materialDefService.list(id));
+ }
+
+ @PostMapping("/tasks/{id}/materials")
+ @RequirePerm(PermissionDef.TASK_MANAGE)
+ @Operation(summary = "新增材料项",
+ description = "材料码在任务内唯一;不传名称/别名/压缩档时会按 §2.1 的材料目录补齐默认值。"
+ + "压缩档位参数(固定尺寸/分辨率下限/目标DPI/输出格式)由档位决定,客户端不必也不应传")
+ public ApiResult create(@PathVariable Long id,
+ @Valid @RequestBody MaterialDefSaveReq req) {
+ return ApiResult.ok(materialDefService.create(id, req));
+ }
+
+ @PutMapping("/materials/{id}")
+ @RequirePerm(PermissionDef.TASK_MANAGE)
+ @Operation(summary = "修改材料项", description = "材料码不可修改(它是文件的关联键)")
+ public ApiResult update(@PathVariable Long id,
+ @Valid @RequestBody MaterialDefSaveReq req) {
+ materialDefService.update(id, req);
+ return ApiResult.ok();
+ }
+
+ @DeleteMapping("/materials/{id}")
+ @RequirePerm(PermissionDef.TASK_MANAGE)
+ @Operation(summary = "删除材料项",
+ description = "已经上传过文件的材料项不可删除(40027);如果只是不想再收,"
+ + "请设为选传或加条件显示规则隐藏")
+ public ApiResult delete(@PathVariable Long id) {
+ materialDefService.delete(id);
+ return ApiResult.ok();
+ }
+
+ @PostMapping("/tasks/{id}/materials/apply-template")
+ @RequirePerm(PermissionDef.TASK_MANAGE)
+ @Operation(summary = "按内置模板初始化材料清单",
+ description = "幂等:已存在的材料码会跳过。模板见 §7.10 的三个 TPL_*")
+ public ApiResult