Browse Source

feat(game-event): 添加赛事微信小程序码生成功能

- 在application.yml中新增小程序配置项,包括页面路径和access-token过期时间-为GameEventController添加更新赛事小程序码的接口/updateQrCode/{eventId}
- 实现GameEventServiceImpl中的微信小程序码生成逻辑,包括获取access_token、 调用微信接口生成小程序码、上传至OSS并更新赛事链接
- 添加ByteArrayMultipartFile类用于处理内存中的二维码字节数据
- 注释掉GameEventMapper中旧的数据权限控制方法
- 更新赛事创建时自动生成小程序码的功能
- 优化部分日志记录和代码格式
zhou 4 weeks ago
parent
commit
ad5fe77b32

+ 3 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -292,6 +292,9 @@ wechat:
   miniapp:
     appid: wx2189666171bf043e
     secret: f97e65576ceb1516e49074a87b608f7b
+    page:
+      - /pages/index/index
+    access-token-expires: 7000
 #    appid: wx017241c84de43b7a
 #    secret: 91ee2725605ba0ae73829cf4538395ac
 

+ 13 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/controller/GameEventController.java

@@ -97,6 +97,19 @@ public class GameEventController extends BaseController {
         return toAjax(gameEventService.updateByBo(bo));
     }
 
+    /**
+     * 更新赛事微信小程序码
+     * 重新生成赛事的小程序码
+     */
+    @SaCheckPermission("system:gameEvent:edit")
+    @Log(title = "更新赛事微信小程序码", businessType = BusinessType.UPDATE)
+    @PutMapping("/updateQrCode/{eventId}")
+    public R<Void> updateEventQrCode(@NotNull(message = "赛事ID不能为空")
+                                    @PathVariable Long eventId) {
+        boolean success = gameEventService.updateEventQrCode(eventId);
+        return success ? R.ok() : R.fail("更新赛事微信小程序码失败");
+    }
+
     /**
      * 删除赛事基本信息
      *

+ 58 - 58
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/mapper/GameEventMapper.java

@@ -22,62 +22,62 @@ import java.util.List;
 @Mapper
 public interface GameEventMapper extends BaseMapperPlus<GameEvent, GameEventVo> {
 
-    /**
-     * 分页查询赛事列表,并进行数据权限控制
-     * 注意:如果赛事表没有部门字段,需要根据业务需求调整
-     * 例如:如果赛事通过创建人关联,可以使用 userName 来控制
-     * 如果赛事通过部门关联,可以使用 deptName 来控制
-     *
-     * @param page         分页参数
-     * @param queryWrapper 查询条件
-     * @return 分页的赛事信息
-     */
-    @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
-    })
-    default Page<GameEventVo> selectPageEventList(Page<GameEvent> page, Wrapper<GameEvent> queryWrapper) {
-        return this.selectVoPage(page, queryWrapper);
-    }
-
-    /**
-     * 查询赛事列表,并进行数据权限控制
-     *
-     * @param queryWrapper 查询条件
-     * @return 赛事信息集合
-     */
-    @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
-    })
-    default List<GameEventVo> selectEventList(Wrapper<GameEvent> queryWrapper) {
-        return this.selectVoList(queryWrapper);
-    }
-
-    /**
-     * 更新赛事数据,并进行数据权限控制
-     *
-     * @param event         要更新的赛事实体
-     * @param updateWrapper 更新条件封装器
-     * @return 更新操作影响的行数
-     */
-    @Override
-    @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
-    })
-    int update(@Param(Constants.ENTITY) GameEvent event, @Param(Constants.WRAPPER) Wrapper<GameEvent> updateWrapper);
-
-    /**
-     * 根据ID更新赛事数据,并进行数据权限控制
-     *
-     * @param event 要更新的赛事实体
-     * @return 更新操作影响的行数
-     */
-    @Override
-    @DataPermission({
-        @DataColumn(key = "deptName", value = "create_dept"),
-        @DataColumn(key = "userName", value = "create_by")
-    })
-    int updateById(@Param(Constants.ENTITY) GameEvent event);
+//    /**
+//     * 分页查询赛事列表,并进行数据权限控制
+//     * 注意:如果赛事表没有部门字段,需要根据业务需求调整
+//     * 例如:如果赛事通过创建人关联,可以使用 userName 来控制
+//     * 如果赛事通过部门关联,可以使用 deptName 来控制
+//     *
+//     * @param page         分页参数
+//     * @param queryWrapper 查询条件
+//     * @return 分页的赛事信息
+//     */
+//    @DataPermission({
+//        @DataColumn(key = "deptName", value = "create_dept"),
+//        @DataColumn(key = "userName", value = "create_by")
+//    })
+//    default Page<GameEventVo> selectPageEventList(Page<GameEvent> page, Wrapper<GameEvent> queryWrapper) {
+//        return this.selectVoPage(page, queryWrapper);
+//    }
+//
+//    /**
+//     * 查询赛事列表,并进行数据权限控制
+//     *
+//     * @param queryWrapper 查询条件
+//     * @return 赛事信息集合
+//     */
+//    @DataPermission({
+//        @DataColumn(key = "deptName", value = "create_dept"),
+//        @DataColumn(key = "userName", value = "create_by")
+//    })
+//    default List<GameEventVo> selectEventList(Wrapper<GameEvent> queryWrapper) {
+//        return this.selectVoList(queryWrapper);
+//    }
+//
+//    /**
+//     * 更新赛事数据,并进行数据权限控制
+//     *
+//     * @param event         要更新的赛事实体
+//     * @param updateWrapper 更新条件封装器
+//     * @return 更新操作影响的行数
+//     */
+//    @Override
+//    @DataPermission({
+//        @DataColumn(key = "deptName", value = "create_dept"),
+//        @DataColumn(key = "userName", value = "create_by")
+//    })
+//    int update(@Param(Constants.ENTITY) GameEvent event, @Param(Constants.WRAPPER) Wrapper<GameEvent> updateWrapper);
+//
+//    /**
+//     * 根据ID更新赛事数据,并进行数据权限控制
+//     *
+//     * @param event 要更新的赛事实体
+//     * @return 更新操作影响的行数
+//     */
+//    @Override
+//    @DataPermission({
+//        @DataColumn(key = "deptName", value = "create_dept"),
+//        @DataColumn(key = "userName", value = "create_by")
+//    })
+//    int updateById(@Param(Constants.ENTITY) GameEvent event);
 }

+ 8 - 0
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/IGameEventService.java

@@ -152,4 +152,12 @@ public interface IGameEventService {
      */
     void generateBibFromTemplateAsync(Long taskId, Long eventId, byte[] templateImage, GenerateBibBo bibParam);
 
+    /**
+     * 更新赛事微信小程序码
+     * 提供给前端更新按钮调用,重新生成赛事的小程序码
+     * @param eventId 赛事ID
+     * @return 更新结果
+     */
+    Boolean updateEventQrCode(Long eventId);
+
 }

+ 304 - 17
ruoyi-modules/ruoyi-game-event/src/main/java/org/dromara/system/service/impl/GameEventServiceImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.img.FontUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.google.zxing.BarcodeFormat;
@@ -44,12 +45,17 @@ import org.dromara.system.mapper.GameEventMapper;
 import org.dromara.system.config.FileUploadConfig;
 import org.dromara.system.service.*;
 import org.dromara.system.domain.vo.SysOssVo;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Lazy;
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.multipart.MultipartFile;
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
 
 import javax.imageio.ImageIO;
 import java.awt.*;
@@ -59,11 +65,13 @@ import java.awt.image.BufferedImage;
 import java.io.*;
 import java.nio.file.Files;
 import java.nio.file.Paths;
+import java.time.Duration;
 import java.util.*;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
@@ -108,6 +116,33 @@ public class GameEventServiceImpl implements IGameEventService {
         "424D"      // BMP
     };
 
+    /**
+     * 更新赛事小程序码
+     * 提供给前端更新按钮调用,重新生成赛事的小程序码
+     * @param eventId 赛事ID
+     * @return 更新结果
+     */
+    @Override
+    public Boolean updateEventQrCode(Long eventId) {
+        try {
+            // 查询赛事信息
+            GameEvent gameEvent = baseMapper.selectById(eventId);
+            if (gameEvent == null) {
+                log.error("赛事不存在,eventId: {}", eventId);
+                return false;
+            }
+
+            // 重新生成二维码
+            generateEventQrCode(gameEvent);
+
+            log.info("成功更新赛事[{}]的微信小程序二维码", gameEvent.getEventName());
+            return true;
+        } catch (Exception e) {
+            log.error("更新赛事微信小程序二维码失败: {}, eventId: {}", e.getMessage(), eventId, e);
+            return false;
+        }
+    }
+
     // 预览框尺寸(固定)
     private static final int previewWidth = 600;
     private static final int previewHeight = 400;
@@ -133,8 +168,9 @@ public class GameEventServiceImpl implements IGameEventService {
     @Override
     public TableDataInfo<GameEventVo> queryPageList(GameEventBo bo, PageQuery pageQuery) {
         LambdaQueryWrapper<GameEvent> lqw = buildQueryWrapper(bo);
+        Page<GameEventVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
         // 使用带数据权限注解的方法
-        Page<GameEventVo> result = baseMapper.selectPageEventList(pageQuery.build(), lqw);
+//        Page<GameEventVo> result = baseMapper.selectPageEventList(pageQuery.build(), lqw);
         return TableDataInfo.build(result);
     }
 
@@ -148,7 +184,8 @@ public class GameEventServiceImpl implements IGameEventService {
     public List<GameEventVo> queryList(GameEventBo bo) {
         LambdaQueryWrapper<GameEvent> lqw = buildQueryWrapper(bo);
         // 使用带数据权限注解的方法
-        return baseMapper.selectEventList(lqw);
+//        return baseMapper.selectEventList(lqw);
+        return baseMapper.selectVoList(lqw);
     }
 
     private LambdaQueryWrapper<GameEvent> buildQueryWrapper(GameEventBo bo) {
@@ -177,11 +214,202 @@ public class GameEventServiceImpl implements IGameEventService {
         validEntityBeforeSave(add);
         boolean flag = baseMapper.insert(add) > 0;
         if (flag) {
+            // 生成微信小程序二维码并设置到赛事链接
+            generateEventQrCode(add);
             return add.getEventId();
         }
         return null;
     }
 
+    // 微信相关常量
+    private static final String ACCESS_TOKEN_KEY = "wechat:access_token";
+    @Value("${wechat.miniapp.access-token-expires:}")
+    private long ACCESS_TOKEN_EXPIRE; // access_token过期时间,7000秒(比微信的7200秒少200秒)
+    @Value("${wechat.miniapp.appid:}")
+    private String APP_ID; // 实际使用时替换为真实AppID
+    @Value("${wechat.miniapp.secret:}")
+    private String APP_SECRET; // 实际使用时替换为真实AppSecret
+    @Value("${wechat.miniapp.page:}")
+    private String page;
+    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+    private static final String GET_WXACODE_UNLIMIT_URL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=%s";
+
+    /**
+     * 生成赛事微信小程序二维码
+     * @param gameEvent 赛事信息
+     */
+    private void generateEventQrCode(GameEvent gameEvent) {
+        try {
+            // 获取微信access_token
+            String accessToken = getAccessToken();
+            if (accessToken == null) {
+                log.error("获取微信access_token失败,无法生成小程序码");
+                return;
+            }
+
+            // 构建scene参数,包含赛事ID
+            String scene = "eventId=" + gameEvent.getEventId();
+
+            // 页面路径,实际使用时需要替换为真实路径
+//            String page = "pages/index/index";
+
+            // 调用微信接口生成小程序码
+            byte[] qrCodeBytes = getWxaCodeUnlimit(accessToken, scene, page);
+            if (qrCodeBytes == null) {
+                log.error("调用微信接口生成小程序码失败");
+                return;
+            }
+
+            // 直接使用自定义的ByteArrayMultipartFile包装二维码字节数组
+            String fileName = "event_" + gameEvent.getEventId() + "_" + System.currentTimeMillis() + ".png";
+            MultipartFile multipartFile = new ByteArrayMultipartFile("file", fileName, "image/png", qrCodeBytes);
+
+            // 使用sysOssService上传文件并获取URL
+            SysOssVo ossVo = sysOssService.upload(multipartFile);
+            String qrCodeUrl = ossVo.getUrl();
+
+            // 更新赛事链接
+            gameEvent.setEventUrl(qrCodeUrl);
+            baseMapper.updateById(gameEvent);
+
+            log.info("成功为赛事[{}]生成微信小程序二维码,URL: {}", gameEvent.getEventName(), qrCodeUrl);
+        } catch (Exception e) {
+            log.error("生成赛事微信小程序二维码失败: {}", e.getMessage(), e);
+            // 不抛出异常,避免影响赛事创建
+        }
+    }
+
+    /**
+     * 获取微信access_token(带缓存)
+     * 注意:access_token是调用微信接口的凭证,与生成的小程序码有效期无关
+     * 微信小程序码默认是永久有效的,除非手动失效
+     * @return access_token
+     */
+    private String getAccessToken() {
+        try {
+            // 先尝试从Redis获取缓存的access_token
+            String accessToken = RedisUtils.getCacheObject(ACCESS_TOKEN_KEY);
+            if (StringUtils.isNotBlank(accessToken)) {
+                log.info("从缓存获取access_token成功");
+                return accessToken;
+            }
+
+            // 缓存未命中,调用微信接口获取
+            String url = String.format(ACCESS_TOKEN_URL, APP_ID, APP_SECRET);
+            String response = sendGetRequest(url);
+
+            // 解析响应
+            Map<String, Object> result = JsonUtils.parseMap(response);
+            if (result.containsKey("access_token")) {
+                accessToken = (String) result.get("access_token");
+                // 缓存access_token,设置过期时间
+                long expireInSeconds = TimeUnit.SECONDS.convert(ACCESS_TOKEN_EXPIRE, TimeUnit.MILLISECONDS);
+                RedisUtils.setCacheObject(ACCESS_TOKEN_KEY, accessToken, Duration.ofSeconds(expireInSeconds));
+                log.info("获取并缓存access_token成功");
+                return accessToken;
+            } else {
+                log.error("获取access_token失败: {}", response);
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("获取access_token异常: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 调用微信接口生成小程序码
+     * @param accessToken access_token
+     * @param scene 场景值
+     * @param page 页面路径
+     * @return 小程序码图片字节数组
+     */
+    private byte[] getWxaCodeUnlimit(String accessToken, String scene, String page) {
+        try {
+            String url = String.format(GET_WXACODE_UNLIMIT_URL, accessToken);
+
+            // 构建请求参数
+            Map<String, Object> params = new HashMap<>();
+            params.put("scene", scene);
+            params.put("page", page);
+            params.put("width", 430);
+            params.put("auto_color", false);
+            Map<String, Object> lineColor = new HashMap<>();
+            lineColor.put("r", 0);
+            lineColor.put("g", 0);
+            lineColor.put("b", 0);
+            params.put("line_color", lineColor);
+            params.put("is_hyaline", false); // 是否透明底色
+            params.put("env_version", "release"); // 默认使用正式版
+
+            // 发送POST请求
+            return sendPostRequest(url, JsonUtils.toJsonString(params));
+        } catch (Exception e) {
+            log.error("调用微信接口生成小程序码异常: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 发送HTTP GET请求
+     * @param url 请求URL
+     * @return 响应结果
+     */
+    private String sendGetRequest(String url) throws Exception {
+        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+        connection.setRequestMethod("GET");
+        connection.setConnectTimeout(5000);
+        connection.setReadTimeout(5000);
+        connection.setDoInput(true);
+
+        try (InputStream inputStream = connection.getInputStream();
+             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[1024];
+            int len;
+            while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+            }
+            return outputStream.toString("UTF-8");
+        } finally {
+            connection.disconnect();
+        }
+    }
+
+    /**
+     * 发送HTTP POST请求
+     * @param url 请求URL
+     * @param jsonData JSON数据
+     * @return 响应字节数组
+     */
+    private byte[] sendPostRequest(String url, String jsonData) throws Exception {
+        HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+        connection.setRequestMethod("POST");
+        connection.setConnectTimeout(5000);
+        connection.setReadTimeout(5000);
+        connection.setDoOutput(true);
+        connection.setDoInput(true);
+        connection.setRequestProperty("Content-Type", "application/json");
+
+        // 发送请求体
+        try (OutputStream outputStream = connection.getOutputStream()) {
+            outputStream.write(jsonData.getBytes("UTF-8"));
+            outputStream.flush();
+        }
+
+        // 获取响应
+        try (InputStream inputStream = connection.getInputStream();
+             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
+            byte[] buffer = new byte[1024];
+            int len;
+            while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+            }
+            return outputStream.toByteArray();
+        } finally {
+            connection.disconnect();
+        }
+    }
+
     /**
      * 修改赛事基本信息
      *
@@ -231,7 +459,7 @@ public class GameEventServiceImpl implements IGameEventService {
         if (defaultEvent != null) {
             RedisUtils.deleteObject(GameEventConstant.DEFAULT_EVENT);
         }
-        return baseMapper.deleteByIds(ids) > 0;
+        return result;
     }
 
     /**
@@ -1650,14 +1878,14 @@ public class GameEventServiceImpl implements IGameEventService {
                 if (zipFile.exists()) {
                     long fileSize = zipFile.length();
                     log.info("ZIP文件大小: {} bytes ({} MB)", fileSize, fileSize / 1024 / 1024);
-                    
+
                     // 使用智能上传(自动选择直接上传或分片上传)
                     SysOssVo ossVo = sysOssService.smartUpload(zipFile);
                     log.info("ZIP文件已上传到OSS,ossId: {}, url: {}", ossVo.getOssId(), ossVo.getUrl());
-                    
+
                     // 使用OSS版本完成任务
                     gameBibTaskService.completeTaskWithOss(taskId, zipFilePath, ossVo.getOssId());
-                    
+
                     // 删除本地ZIP文件以节省空间
                     if (zipFile.delete()) {
                         log.info("本地ZIP文件已删除: {}", zipFilePath);
@@ -1817,11 +2045,11 @@ public class GameEventServiceImpl implements IGameEventService {
     private String generateQRCodeData(GameAthleteVo athlete) {
         StringBuilder joinProject = new StringBuilder();
         StringJoiner joiner = new StringJoiner(",");
-        
+
         // 添加调试日志
-        log.debug("生成二维码数据 - 运动员: {}, projectValue: {}, projectList: {}", 
+        log.debug("生成二维码数据 - 运动员: {}, projectValue: {}, projectList: {}",
             athlete.getName(), athlete.getProjectValue(), athlete.getProjectList());
-        
+
         // 检查projectList是否为null或空
         if (athlete.getProjectList() == null || athlete.getProjectList().isEmpty()) {
             log.warn("运动员 {} 的参与项目列表为空,projectValue: {}", athlete.getName(), athlete.getProjectValue());
@@ -1844,24 +2072,24 @@ public class GameEventServiceImpl implements IGameEventService {
                 athlete.getAge(),
                 athlete.getTeamName() != null ? athlete.getTeamName() : "未知队伍");
         }
-        
+
         Map<Long, String> projectMap = gameEventProjectService.queryNameByEventIdAndProjectIds(athlete.getEventId(), athlete.getProjectList());
         log.debug("项目映射结果: {}", projectMap);
-        
+
         // 添加额外的null检查
         if (projectMap == null) {
             log.warn("项目映射结果为空,返回空字符串,逻辑上不可能为空,如果有空项目,应该在运动员表中删除");
             projectMap = new HashMap<>();
         }
-        
+
         for (Long projectId : athlete.getProjectList()) {
             String projectName = projectMap.get(projectId) != null ? projectMap.get(projectId).toString() : "未知项目";
             joiner.add(projectName);
         }
-        
+
         String projectNames = joiner.toString();
         log.debug("最终项目名称: {}", projectNames);
-        
+
         return String.format(
             """
                 {
@@ -1886,13 +2114,13 @@ public class GameEventServiceImpl implements IGameEventService {
     /**
      * 从内存中创建ZIP文件(不保存单个图片文件)
      */
-    private void createZipFileFromMemory(byte[] templateImage, List<GameAthleteVo> athleteVoList, 
+    private void createZipFileFromMemory(byte[] templateImage, List<GameAthleteVo> athleteVoList,
                                        GenerateBibBo bibParam, String zipFilePath, Long taskId) {
         try (FileOutputStream fos = new FileOutputStream(zipFilePath);
              ZipOutputStream zos = new ZipOutputStream(fos)) {
 
             log.info("开始从内存创建ZIP文件 - ZIP文件: {}", zipFilePath);
-            
+
             int totalCount = athleteVoList.size();
             int completedCount = 0;
 
@@ -1912,7 +2140,7 @@ public class GameEventServiceImpl implements IGameEventService {
                     completedCount++;
                     int progress = (completedCount * 100) / totalCount;
                     gameBibTaskService.updateTaskProgress(taskId, progress, completedCount);
-                    
+
                     log.debug("添加图片到ZIP: {}", fileName);
 
                 } catch (Exception e) {
@@ -2004,4 +2232,63 @@ public class GameEventServiceImpl implements IGameEventService {
         }
     }
 
+    /**
+     * 自定义MultipartFile实现,用于处理内存中的字节数组
+     */
+    private static class ByteArrayMultipartFile implements MultipartFile {
+        private final String name;
+        private final String originalFilename;
+        private final String contentType;
+        private final byte[] content;
+
+        public ByteArrayMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
+            this.name = name;
+            this.originalFilename = originalFilename;
+            this.contentType = contentType;
+            this.content = content != null ? content : new byte[0];
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getOriginalFilename() {
+            return originalFilename;
+        }
+
+        @Override
+        public String getContentType() {
+            return contentType;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return content.length == 0;
+        }
+
+        @Override
+        public long getSize() {
+            return content.length;
+        }
+
+        @Override
+        public byte[] getBytes() throws IOException {
+            return content.clone();
+        }
+
+        @Override
+        public InputStream getInputStream() throws IOException {
+            return new ByteArrayInputStream(content);
+        }
+
+        @Override
+        public void transferTo(File dest) throws IOException, IllegalStateException {
+            try (FileOutputStream fos = new FileOutputStream(dest)) {
+                fos.write(content);
+            }
+        }
+    }
+
 }