feat(file): 材料上传的前端界面 + 列出/删除接口

后端:GET /mp/files/submissions/{id}(列出我的材料)+ DELETE /mp/files/{id}(软删)。
  两者都把归属条件(submitter_id)写进 SQL 的 WHERE —— 与门禁、草稿同一套写法,
  少一个条件就是"能看/能删别人的材料"。

前端(填报页的材料区):
  · 按材料项列出:名称、必传标记、格式白名单与体积上限、来源引导语
  · 已传的显示「原名 → 规范名」与「压缩前 → 压缩后」体积(学生要能看见系统做了什么)
  · 传完**立刻处理**并显示结果

  ⚠️ 为什么上传后马上处理而不是留到提交时:学生要能看到"压到了多大、叫什么名字"
     才算完成。若把处理留到提交时,他上传时会以为"传上去了",提交时才发现体积超标 ——
     而那时他可能已经在回家的路上。

  · 学生能自己删(传错了不该只能找班主任)。已提交的填报单不给增删,与草稿同口径。

⚠️ 本轮**未完成浏览器肉眼验证**:构建产物里确实有材料区的代码
   (产物 grep 到「需要上传的材料」),但浏览器加载的是**缓存的旧 chunk**
   (实测 .fill__wrap 只有 4 个子元素,没有材料卡),我用带时间戳的方式绕缓存没成功。
   代码与产物是对的,需要一次硬刷新(Ctrl+Shift+R)或清缓存后再确认。
   这条记下来,不要当成"已验证"。
This commit is contained in:
2026-09-29 11:59:58 +08:00
parent 80a4050e53
commit 8ef09733b1
4 changed files with 254 additions and 2 deletions
@@ -9,6 +9,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
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.RequestMapping;
@@ -18,6 +20,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
@@ -66,8 +69,30 @@ public class MaterialFileController {
+ "压到材料项配的体积上限内(默认 1024KB),并按命名模板生成规范文件名")
public ApiResult<Map<String, Object>> process(@PathVariable Long id) {
CollectFile f = fileService.process(id);
return ApiResult.ok(toVo(f));
}
@DeleteMapping("/{id}")
@Operation(summary = "删除材料(软删)",
description = "学生传错了要能自己删 —— 否则他只能传一份错的来交,或者去找班主任")
public ApiResult<Void> delete(@PathVariable Long id) {
fileService.delete(id);
return ApiResult.ok();
}
@GetMapping("/submissions/{submissionId}")
@Operation(summary = "列出某份填报单下的材料",
description = "按材料码分组。前端据此显示「已传了什么、压到多大、叫什么名字」")
public ApiResult<List<Map<String, Object>>> list(@PathVariable Long submissionId) {
return ApiResult.ok(fileService.listMine(submissionId).stream()
.map(MaterialFileController::toVo).toList());
}
private static Map<String, Object> toVo(CollectFile f) {
Map<String, Object> out = new LinkedHashMap<>();
out.put("id", f.getId());
out.put("materialCode", f.getMaterialCode());
out.put("version", f.getVersion());
out.put("originalName", f.getOriginalName());
out.put("standardName", f.getStandardName());
out.put("rawSize", f.getRawSize());
@@ -75,6 +100,7 @@ public class MaterialFileController {
out.put("width", f.getWidth());
out.put("height", f.getHeight());
out.put("quality", f.getQuality());
return ApiResult.ok(out);
out.put("validateStatus", f.getValidateStatus());
return out;
}
}
@@ -35,4 +35,23 @@ public interface FileMapper extends BaseMapper<CollectFile> {
Long countCurrent(@Param("tenantId") Long tenantId,
@Param("submissionId") Long submissionId,
@Param("materialCode") String materialCode);
/**
* 列出某份填报单下当前生效的文件。
*
* <p>归属条件({@code submitter_id})写进 WHERE —— 与门禁、草稿同一套写法:
* 少一个条件就是"能列出别人传的材料"。
*/
@InterceptorIgnore(tenantLine = "true")
@Select("""
SELECT f.* FROM collect_file f
JOIN collect_submission s ON s.id = f.submission_id AND s.deleted = 0
WHERE f.tenant_id = #{tenantId} AND f.submission_id = #{submissionId}
AND f.is_current = 1 AND f.deleted = 0
AND s.submitter_id = #{submitterId}
ORDER BY f.material_code, f.version
""")
java.util.List<CollectFile> listMine(@Param("tenantId") Long tenantId,
@Param("submissionId") Long submissionId,
@Param("submitterId") Long submitterId);
}
@@ -193,6 +193,41 @@ public class MaterialFileService {
return f;
}
// ── 列表与删除 ────────────────────────────────
public List<CollectFile> listMine(Long submissionId) {
LoginUser me = UserContext.require();
CollectSubmission sub = submissionMapper.selectMineById(submissionId, me.tenantId(), me.uid());
if (sub == null) {
throw new BizException(ErrorCode.VALIDATION_BLOCKED, "填报单不存在或不属于你");
}
return fileMapper.listMine(me.tenantId(), submissionId, me.uid());
}
/**
* 删除材料(软删)。
*
* <p>学生传错了要能自己删 —— 否则他只能传一份错的来交,或者去找班主任。
* 已提交的填报单不能删(要改先撤回),与草稿保存同一口径。
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Long fileId) {
LoginUser me = UserContext.require();
CollectFile f = fileMapper.selectMine(fileId, me.tenantId(), me.uid());
if (f == null) {
throw new BizException(ErrorCode.VALIDATION_BLOCKED, "文件不存在或不属于你");
}
CollectSubmission sub = submissionMapper.selectMineById(
f.getSubmissionId(), me.tenantId(), me.uid());
if (sub != null && sub.getStatus() != null
&& sub.getStatus() != CollectSubmission.STATUS_DRAFT) {
throw new BizException(ErrorCode.AUDIT_IN_PROGRESS, "已提交的填报单不能删除材料");
}
// 软删:磁盘上的原始件保留(万一删错了、或事后要核对)
fileMapper.deleteById(fileId);
log.info("删除材料:fileId={} {}", fileId, f.getOriginalName());
}
// ── 内部 ──────────────────────────────────────
/** 命名变量。名字要能与学生对上 —— 所以姓名与证件号必须来自名册,不来自学生输入。 */