云
5 天以前 cccbfadcb6e96c23a33c40c0687a27c2f6efa1db
feat(seal): 添加印章详情及图片预览功能

- 后端新增获取印章图片接口并设置安全响应头
- 服务层添加图片查询方法并补充对应单元测试
- 前端封装获取印章图片的API下载请求方法
- 列表页新增详情操作按钮及印章详情弹窗组件
- 实现图片异步加载、状态展示及失败重试机制
- 补充详情弹窗样式并在组件卸载时释放内存
已修改3个文件
29 ■■■■■ 文件已修改
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/seal/PdSealController.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/seal/PdSealService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pd/seal/PdSealServiceTest.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/seal/PdSealController.java
@@ -20,6 +20,14 @@
    @Resource private MesPdProjectController projectController;
    @GetMapping("/list") @PreAuthorize("@ss.hasPermission('mes:pd-seal:manage')")
    public CommonResult<List<Map<String,Object>>> list(){ return success(service.seals(false)); }
    @GetMapping("/image") @PreAuthorize("@ss.hasPermission('mes:pd-seal:manage')")
    public void image(@RequestParam long id, HttpServletResponse response) throws java.io.IOException {
        byte[] image = service.getImage(id);
        response.setContentType("image/png");
        response.setHeader("Cache-Control", "no-store, private");
        response.setHeader("X-Content-Type-Options", "nosniff");
        response.getOutputStream().write(image);
    }
    @GetMapping("/available") @PreAuthorize("@ss.hasPermission('mes:pd-project:export')")
    public CommonResult<List<Map<String,Object>>> available(){ return success(service.seals(true)); }
    @PostMapping("/upload") @PreAuthorize("@ss.hasPermission('mes:pd-seal:manage')")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/seal/PdSealService.java
@@ -30,6 +30,11 @@
    public List<Map<String,Object>> seals(boolean activeOnly) {
        return jdbc.queryForList("SELECT id,name,enabled,create_time FROM pd_seal WHERE tenant_id=? AND deleted=0" + (activeOnly ? " AND enabled=1" : "") + " ORDER BY id DESC", tenant());
    }
    public byte[] getImage(long id) {
        var rows = jdbc.queryForList("SELECT image_data FROM pd_seal WHERE id=? AND tenant_id=? AND deleted=0", id, tenant());
        if (rows.isEmpty()) throw fail("印章不存在");
        return (byte[]) rows.get(0).get("image_data");
    }
    public long upload(String name, MultipartFile file, Long userId) throws Exception {
        if (name == null || name.isBlank() || name.length() > 100) throw fail("印章名称不能为空且不能超过100字");
        if (file == null || file.isEmpty() || file.getSize() > 2 * 1024 * 1024) throw fail("请上传不超过2MB的PNG印章图片");
yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pd/seal/PdSealServiceTest.java
@@ -21,6 +21,22 @@
    @InjectMocks PdSealService service;
    @Mock JdbcTemplate jdbc;
    @Mock BpmTaskService tasks;
    @Test void imageLookupIsScopedToCurrentTenant() {
        cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.setTenantId(42L);
        try {
            byte[] image = {1, 2};
            when(jdbc.queryForList(anyString(), any(Object[].class)))
                    .thenReturn(List.of(Map.of("image_data", image)));
            assertArrayEquals(image, service.getImage(7));
            verify(jdbc).queryForList(contains("tenant_id=? AND deleted=0"), eq(7L), eq(42L));
        } finally {
            cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder.clear();
        }
    }
    @Test void refusesImageWhenSealIsNotFound() {
        when(jdbc.queryForList(anyString(), any(Object[].class))).thenReturn(List.of());
        assertThrows(ServiceException.class, () -> service.getImage(7));
    }
    @Test void refusesDownloadWithoutApprovedApplication() {
        when(jdbc.queryForList(anyString(), any(Object[].class))).thenReturn(List.of());
        assertThrows(ServiceException.class, () -> service.download(1, 7));