@@ -35,6 +35,16 @@ | |||||
<artifactId>spring-boot-starter-freemarker</artifactId> | <artifactId>spring-boot-starter-freemarker</artifactId> | ||||
</dependency> | </dependency> | ||||
<dependency> | <dependency> | ||||
<groupId>net.coobird</groupId> | |||||
<artifactId>thumbnailator</artifactId> | |||||
<version>[0.4, 0.5)</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>org.jclarion</groupId> | |||||
<artifactId>image4j</artifactId> | |||||
<version>0.7</version> | |||||
</dependency> | |||||
<dependency> | |||||
<groupId>com.fasterxml.jackson.dataformat</groupId> | <groupId>com.fasterxml.jackson.dataformat</groupId> | ||||
<artifactId>jackson-dataformat-yaml</artifactId> | <artifactId>jackson-dataformat-yaml</artifactId> | ||||
<version>2.11.0</version> | <version>2.11.0</version> | ||||
@@ -0,0 +1,26 @@ | |||||
package cc.smtweb.system.bpm.spring.config; | |||||
import cc.smtweb.system.bpm.util.FilePathGenerator; | |||||
import org.springframework.beans.factory.annotation.Value; | |||||
import org.springframework.context.annotation.Bean; | |||||
import org.springframework.context.annotation.Configuration; | |||||
import cc.smtweb.framework.core.db.jdbc.IdGenerator; | |||||
/** | |||||
* 微服务框架封装自动配置类 | |||||
*/ | |||||
@Configuration | |||||
public class FileConfig { | |||||
// 文件本地存储配置 | |||||
@Value("${smtweb.file.local-path}") | |||||
private String fileLocalPath; | |||||
// 文件请求URL路径配置 http://127.0.0.1:${server.port}/${server.servlet.context-path}/files/ | |||||
@Value("${smtweb.file.url}") | |||||
private String fileUrl; | |||||
@Bean | |||||
public FilePathGenerator filePathGenerator(IdGenerator idGenerator) { | |||||
return new FilePathGenerator(fileLocalPath, fileUrl, idGenerator); | |||||
} | |||||
} |
@@ -0,0 +1,185 @@ | |||||
package cc.smtweb.system.bpm.spring.controller; | |||||
import cc.smtweb.framework.core.cache.redis.RedisManager; | |||||
import cc.smtweb.framework.core.session.SessionUtil; | |||||
import cc.smtweb.system.bpm.util.FilePathGenerator; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.apache.commons.lang3.time.DateUtils; | |||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import org.springframework.beans.factory.annotation.Value; | |||||
import org.springframework.core.io.InputStreamResource; | |||||
import org.springframework.http.*; | |||||
import org.springframework.web.bind.annotation.*; | |||||
import javax.servlet.http.HttpServletRequest; | |||||
import java.io.File; | |||||
import java.io.FileInputStream; | |||||
import java.io.FileNotFoundException; | |||||
import java.io.InputStream; | |||||
import java.nio.charset.StandardCharsets; | |||||
import java.time.Instant; | |||||
import java.util.concurrent.TimeUnit; | |||||
@RestController | |||||
public class FileDownloadController { | |||||
private static final MediaType APPLICATION_JAVASCRIPT = new MediaType("application", "javascript"); | |||||
@Value("${smtweb.static.local-path:}") | |||||
private String staticLocalPath; | |||||
@Autowired | |||||
private FilePathGenerator filePathGenerator; | |||||
@Autowired | |||||
private RedisManager redisManager; | |||||
/** path方式下载文件 */ | |||||
@GetMapping("/fs/files/**") | |||||
public ResponseEntity<InputStreamResource> files(@RequestParam(value="name", required=false) String name, | |||||
@RequestParam(value="noCache", required=false) Boolean noCache, | |||||
HttpServletRequest request | |||||
) throws FileNotFoundException { | |||||
String filePath = request.getRequestURI().substring(10); | |||||
return download(filePath, name, noCache, request); | |||||
} | |||||
/** 参数方式下载文件 */ | |||||
@GetMapping("/fs/download") | |||||
public ResponseEntity<InputStreamResource> download(@RequestParam(value="path") String path, | |||||
@RequestParam(value="name", required=false) String name, | |||||
@RequestParam(value="noCache", required=false) Boolean noCache, | |||||
HttpServletRequest request | |||||
) throws FileNotFoundException { | |||||
SessionUtil.checkSession(request, redisManager); | |||||
File file = new File(filePathGenerator.getFileDiskPath(path)); | |||||
if (!file.exists()) { | |||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); | |||||
} | |||||
if (StringUtils.isBlank(name)) { | |||||
name = file.getName(); | |||||
} | |||||
HttpHeaders headers = new HttpHeaders(); | |||||
if (Boolean.TRUE.equals(noCache)) { | |||||
headers.setCacheControl("no-cache, no-store, must-revalidate"); | |||||
headers.setPragma("no-cache"); | |||||
headers.setExpires(0); | |||||
} | |||||
headers.setLastModified(file.lastModified()); | |||||
headers.add("Content-Disposition", | |||||
String.format("attachment; filename=\"%s\"", new String(name.getBytes(StandardCharsets.UTF_8),StandardCharsets.ISO_8859_1))); | |||||
return ResponseEntity.ok() | |||||
.headers(headers) | |||||
.contentLength(file.length()) | |||||
.contentType(MediaType.APPLICATION_OCTET_STREAM) | |||||
.body(new InputStreamResource(new FileInputStream(file))); | |||||
} | |||||
/** path方式读取静态目录文件 */ | |||||
@GetMapping("/fs/static/**") | |||||
public ResponseEntity<InputStreamResource> resource(@RequestParam(value="default", required=false) String defaultPath, | |||||
@RequestParam(value="noCache", required=false) Boolean noCache, | |||||
@RequestHeader(value="If-Modified-Since", required = false) String ifModifiedSince, | |||||
HttpServletRequest request) throws FileNotFoundException { | |||||
String filePath = request.getRequestURI().substring(11); | |||||
HttpHeaders headers = new HttpHeaders(); | |||||
if (Boolean.TRUE.equals(noCache)) { | |||||
headers.setCacheControl("no-cache, no-store, must-revalidate"); | |||||
headers.setPragma("no-cache"); | |||||
headers.setExpires(0); | |||||
} else { | |||||
// 暂时缓存1天 | |||||
headers.setCacheControl(CacheControl.maxAge(1, TimeUnit.DAYS)); | |||||
headers.setExpires(Instant.ofEpochMilli(System.currentTimeMillis() + DateUtils.MILLIS_PER_DAY)); | |||||
} | |||||
String name = getFileName(filePath); | |||||
headers.add("Content-Disposition", | |||||
String.format("attachment; filename=\"%s\"", new String(name.getBytes(StandardCharsets.UTF_8),StandardCharsets.ISO_8859_1))); | |||||
MediaType contentType = getContentType(filePath); | |||||
// 先找文件 | |||||
if (StringUtils.isNotBlank(staticLocalPath)) { | |||||
File file = new File(staticLocalPath + filePath); | |||||
if (file.exists()) { | |||||
headers.setLastModified(file.lastModified()); | |||||
return ResponseEntity.ok() | |||||
.headers(headers) | |||||
.contentLength(file.length()) | |||||
.contentType(contentType) | |||||
.body(new InputStreamResource(new FileInputStream(file))); | |||||
} | |||||
} | |||||
// 再找资源目录 | |||||
InputStream inputStream = getClass().getResourceAsStream("/static/" + filePath); | |||||
if (inputStream != null) { | |||||
return buildResource(inputStream, contentType, headers); | |||||
} else if (StringUtils.isNotBlank(defaultPath)) { | |||||
inputStream = getClass().getResourceAsStream("/static/" + defaultPath); | |||||
if (inputStream != null) { | |||||
return buildResource(inputStream, contentType, headers); | |||||
} | |||||
} | |||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); | |||||
} | |||||
private String getFileName(String filePath) { | |||||
int pos = filePath.lastIndexOf("/"); | |||||
if (pos >= 0) { | |||||
return filePath.substring(pos + 1); | |||||
} | |||||
return filePath; | |||||
} | |||||
private ResponseEntity<InputStreamResource> buildResource(InputStream inputStream, MediaType contentType, HttpHeaders headers) { | |||||
return ResponseEntity.ok() | |||||
.headers(headers) | |||||
// .contentLength(file.length()) | |||||
.contentType(contentType) | |||||
.body(new InputStreamResource(inputStream)); | |||||
} | |||||
private static MediaType getContentType(String filePath) { | |||||
int pos = filePath.lastIndexOf("."); | |||||
if (pos >= 0) { | |||||
String fileExt = filePath.substring(pos + 1).toLowerCase(); | |||||
switch (fileExt) { | |||||
case "htm": | |||||
case "html": | |||||
case "css": | |||||
return MediaType.TEXT_HTML; | |||||
case "js": | |||||
return APPLICATION_JAVASCRIPT; | |||||
case "txt": | |||||
return MediaType.TEXT_PLAIN; | |||||
case "pdf": | |||||
return MediaType.APPLICATION_PDF; | |||||
case "xml": | |||||
return MediaType.TEXT_XML; | |||||
case "gif": | |||||
return MediaType.IMAGE_GIF; | |||||
case "jpeg": | |||||
case "jpg": | |||||
return MediaType.IMAGE_JPEG; | |||||
case "png": | |||||
return MediaType.IMAGE_PNG; | |||||
default: | |||||
return MediaType.APPLICATION_OCTET_STREAM; | |||||
} | |||||
} | |||||
return MediaType.APPLICATION_OCTET_STREAM; | |||||
} | |||||
} |
@@ -0,0 +1,157 @@ | |||||
package cc.smtweb.system.bpm.spring.controller; | |||||
import cc.smtweb.framework.core.common.R; | |||||
import cc.smtweb.framework.core.db.DbEngine; | |||||
import cc.smtweb.framework.core.cache.redis.RedisManager; | |||||
import cc.smtweb.framework.core.session.SessionUtil; | |||||
import cc.smtweb.system.bpm.spring.dao.ImageAttachDao; | |||||
import cc.smtweb.system.bpm.util.FilePathGenerator; | |||||
import cc.smtweb.system.bpm.util.FilePathInfo; | |||||
import cc.smtweb.system.bpm.util.MemMultipartFile; | |||||
import cc.smtweb.system.bpm.util.ThumbImage; | |||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import org.springframework.util.FileCopyUtils; | |||||
import org.springframework.web.bind.annotation.*; | |||||
import org.springframework.web.multipart.MultipartFile; | |||||
import cc.smtweb.system.bpm.spring.entity.FileDataVO; | |||||
import cc.smtweb.system.bpm.spring.entity.UploadDataVO; | |||||
import javax.servlet.http.HttpServletRequest; | |||||
import java.io.*; | |||||
import java.sql.Timestamp; | |||||
import java.text.SimpleDateFormat; | |||||
@RestController | |||||
public class FileUploadController { | |||||
@Autowired | |||||
private FilePathGenerator filePathGenerator; | |||||
@Autowired | |||||
private DbEngine dbEngine; | |||||
@Autowired | |||||
private RedisManager redisManager; | |||||
@Autowired | |||||
private ImageAttachDao imageAttachDao; | |||||
// TODO: 权限处理,临时文件处理 | |||||
@PostMapping("/fs/upload/{path}") | |||||
public R upload(@RequestParam("file") MultipartFile file, @PathVariable("path") String path, | |||||
@RequestParam(value="thumb", required=false) String thumb, | |||||
@RequestParam(value="thumbHeight", required=false) Integer thumbHeight, | |||||
@RequestParam(value="commit", required=false) Boolean insert, | |||||
@RequestParam(value="keepName", required=false) Boolean keepName, | |||||
HttpServletRequest request | |||||
) { | |||||
SessionUtil.checkSession(request, redisManager); | |||||
return uploadFile(path, file, ThumbImage.type(thumb), thumbHeight, insert, keepName); | |||||
} | |||||
@PostMapping("/fs/uploadImage/{path}") | |||||
public R upload(@RequestBody FileDataVO data, @PathVariable("path") String path, | |||||
@RequestParam(value="thumb", required=false) String thumb, | |||||
@RequestParam(value="thumbHeight", required=false) Integer thumbHeight, | |||||
@RequestParam(value="commit", required=false) Boolean insert, | |||||
HttpServletRequest request) { | |||||
SessionUtil.checkSession(request, redisManager); | |||||
MultipartFile file = MemMultipartFile.build(data.getData()); | |||||
if (file == null) { | |||||
return R.error("数据内容格式有错"); | |||||
} | |||||
return uploadFile(path, file, ThumbImage.type(thumb), thumbHeight, insert, false); | |||||
} | |||||
@PostMapping("/fs/uploadAvatar/{path}") | |||||
public R uploadAvatar(@RequestParam("file") MultipartFile file, @PathVariable("path") String path, | |||||
@RequestParam(value="size", required=false) Integer size, | |||||
@RequestParam(value="commit", required=false) Boolean insert, | |||||
@RequestParam(value="keepName", required=false) Boolean keepName, | |||||
HttpServletRequest request) { | |||||
SessionUtil.checkSession(request, redisManager); | |||||
return uploadFile(path, file, ThumbImage.TYPE_AVATAR, size, insert, keepName); | |||||
} | |||||
// 保存文件和插入数据库数据 | |||||
@PostMapping("/fs/commit/{path}") | |||||
public R commit(@RequestParam("file") MultipartFile file, @PathVariable("path") String path, | |||||
@RequestParam(value="thumb", required=false) String thumb, | |||||
@RequestParam(value="thumbHeight", required=false) Integer thumbHeight, | |||||
@RequestParam(value="keepName", required=false) Boolean keepName, | |||||
HttpServletRequest request) { | |||||
SessionUtil.checkSession(request, redisManager); | |||||
return uploadFile(path, file, ThumbImage.type(thumb), thumbHeight, true, keepName); | |||||
} | |||||
private R uploadFile(String path, MultipartFile file, int type, Integer size, Boolean insert, Boolean keepName) { | |||||
//获取上传时的文件名 | |||||
String fileName = file.getOriginalFilename(); | |||||
//判断文件是否为空 | |||||
if(file.isEmpty() && fileName != null){ | |||||
return R.error("文件为空"); | |||||
} | |||||
// 判断保持文件名不变 | |||||
FilePathInfo fileInfo = filePathGenerator.make(path, fileName, Boolean.TRUE.equals(keepName)); | |||||
// 注意是路径+文件名 | |||||
File targetFile = new File(fileInfo.getFullFileName()); | |||||
try(InputStream inputStream = file.getInputStream(); OutputStream outputStream = new FileOutputStream(targetFile)) { | |||||
// 最后使用资源访问器FileCopyUtils的copy方法拷贝文件 | |||||
FileCopyUtils.copy(inputStream, outputStream); | |||||
} catch (IOException e) { | |||||
//出现异常,则告诉页面失败 | |||||
return R.error("上传失败", e); | |||||
} | |||||
// 生成缩略图 | |||||
// String contentType = file.getContentType(); | |||||
UploadDataVO data = new UploadDataVO(); | |||||
data.setPath(fileInfo.getMysqlFilePath()); | |||||
data.setName(fileName); | |||||
data.setSize(file.getSize()); | |||||
data.setContentType(file.getContentType()); | |||||
data.setUrl(filePathGenerator.getFileUrl(fileInfo.getMysqlFilePath())); | |||||
if (type == ThumbImage.TYPE_THUMB || type == ThumbImage.TYPE_AVATAR) { | |||||
try { | |||||
imageAttachDao.makeThumb(data, type == ThumbImage.TYPE_THUMB, targetFile, size); | |||||
} catch (IOException e) { | |||||
return R.error("生成缩略图失败", e); | |||||
} | |||||
} | |||||
if (Boolean.TRUE.equals(insert)) { | |||||
Long id = dbEngine.nextId(); | |||||
Timestamp now = new Timestamp(System.currentTimeMillis()); | |||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmssSSS"); | |||||
dbEngine.update("insert into sw_user.sys_attach(attach_id, attach_name, attach_path, attach_content_type, attach_size, attach_create_time) values(?, ?, ?, ?, ?, ?)", | |||||
id, data.getName(), data.getPath(), data.getContentType(), data.getSize(), sdf.format(now)); | |||||
data.setId(id); | |||||
} | |||||
return R.success(data); | |||||
} | |||||
// TODO: 修改为安全的后台删除方式 | |||||
@PostMapping("/fs/remove") | |||||
public R remove(@RequestParam(value="filePath") String filePath, HttpServletRequest request) { | |||||
SessionUtil.checkSession(request, redisManager); | |||||
File file = new File(filePathGenerator.getFileDiskPath(filePath)); | |||||
if (file.exists() && file.isFile()) { | |||||
if (file.delete()) { | |||||
R.success(filePath); | |||||
} | |||||
} | |||||
return R.success(); | |||||
} | |||||
} |
@@ -0,0 +1,63 @@ | |||||
package cc.smtweb.system.bpm.spring.dao; | |||||
import cc.smtweb.system.bpm.util.ThumbImage; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.springframework.stereotype.Service; | |||||
import cc.smtweb.system.bpm.spring.entity.UploadDataVO; | |||||
import java.io.File; | |||||
import java.io.IOException; | |||||
@Service | |||||
public class ImageAttachDao { | |||||
public static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; | |||||
public void makeThumb(UploadDataVO data, boolean isThumb, File targetFile, Integer size) throws IOException { | |||||
boolean imageType = false; | |||||
String fileName = data.getName(); | |||||
String contentType = data.getContentType(); | |||||
if (contentType.startsWith("image/")) { | |||||
imageType = true; | |||||
} else if (contentType.equals(APPLICATION_OCTET_STREAM)) { | |||||
String fileExt = fileName.substring(fileName.lastIndexOf(".")); | |||||
if (StringUtils.isNotEmpty(fileExt)) { | |||||
switch (fileExt.toLowerCase()) { | |||||
case ".jpg": | |||||
case ".jpeg": | |||||
contentType = "image/jpg"; | |||||
imageType = true; | |||||
break; | |||||
case ".gif": | |||||
contentType = "image/gif"; | |||||
imageType = true; | |||||
break; | |||||
case ".png": | |||||
contentType = "image/png"; | |||||
imageType = true; | |||||
break; | |||||
default: | |||||
break; | |||||
} | |||||
if (imageType) { | |||||
data.setContentType(contentType); | |||||
} | |||||
} | |||||
} | |||||
if (imageType) { | |||||
int thumbHeight = 80; | |||||
if (size != null) { | |||||
thumbHeight = (size > 500) ? 500 : size; | |||||
} | |||||
ThumbImage thumbImage = new ThumbImage(); | |||||
thumbImage.makeThumb(isThumb, targetFile, thumbHeight); | |||||
data.setWidth(thumbImage.getImageWidth()); | |||||
data.setHeight(thumbImage.getImageHeight()); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,114 @@ | |||||
package cc.smtweb.system.bpm.spring.dao; | |||||
import cc.smtweb.framework.core.db.DbEngine; | |||||
import cc.smtweb.system.bpm.spring.entity.AttachPathPO; | |||||
import cc.smtweb.system.bpm.util.FilePathGenerator; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
import org.springframework.beans.factory.annotation.Autowired; | |||||
import org.springframework.stereotype.Service; | |||||
import org.yaml.snakeyaml.util.UriEncoder; | |||||
import java.util.HashMap; | |||||
import java.util.List; | |||||
import java.util.Map; | |||||
@Service | |||||
public class SysAttachDao { | |||||
@Autowired | |||||
private FilePathGenerator filePathGenerator; | |||||
@Autowired | |||||
private DbEngine dbEngine; | |||||
/** | |||||
* 获取文件本地文件路径 | |||||
* | |||||
* @param filePath 相对路径 | |||||
* @return 本地文件全路径 | |||||
*/ | |||||
public String getDiskPath(String filePath) { | |||||
return filePathGenerator.getFileDiskPath(filePath); | |||||
} | |||||
/** | |||||
* 获取访问文件的URL地址 | |||||
* | |||||
* @param filePath 文件相对路径 | |||||
* @return 文件URL地址 | |||||
*/ | |||||
public String getFileUrl(String filePath) { | |||||
return filePathGenerator.getFileUrl(filePath); | |||||
} | |||||
/** | |||||
* 获取访问文件的URL地址 | |||||
* | |||||
* @param filePath 文件相对路径 | |||||
* @param filePath 文件名 | |||||
* @return 文件URL地址 | |||||
*/ | |||||
public String getFileUrl(String filePath, String fileName) { | |||||
return "/fs/download?path=" + UriEncoder.encode(filePath) + "&name=" + UriEncoder.encode(fileName); | |||||
} | |||||
public AttachPathPO get(Long id) { | |||||
if (id != null) { | |||||
return dbEngine.queryEntity("select attach_id, attach_name, attach_path, attach_content_type, attach_size, attach_create_time from sw_user.sys_attach where attach_id=?", | |||||
AttachPathPO.class, id); | |||||
} | |||||
return null; | |||||
} | |||||
// 删除文件记录和文件 | |||||
public void remove(Long fileId) { | |||||
// if (id != null) { | |||||
// return dbEngine.queryEntity("select attach_id, attach_name, attach_path, attach_content_type, attach_size, attach_create_time from sw_user.sys_attach where attach_id=?", | |||||
// AttachPathPO.class, id); | |||||
// } | |||||
// | |||||
// return null; | |||||
} | |||||
// 删除文件 | |||||
public void remove(String filePath) { | |||||
// if (id != null) { | |||||
// return dbEngine.queryEntity("select attach_id, attach_name, attach_path, attach_content_type, attach_size, attach_create_time from sw_user.sys_attach where attach_id=?", | |||||
// AttachPathPO.class, id); | |||||
// } | |||||
// | |||||
// return null; | |||||
} | |||||
public List<AttachPathPO> list(Long[] ids) { | |||||
if (ids != null && ids.length > 0) { | |||||
return dbEngine.query("select attach_id, attach_name, attach_path, attach_content_type, attach_size, attach_create_time from sw_user.sys_attach where attach_id in( " | |||||
+ StringUtils.join(ids, ",") + ")", | |||||
AttachPathPO.class); | |||||
} | |||||
return null; | |||||
} | |||||
public Map<Long, AttachPathPO> map(Long[] ids) { | |||||
List<AttachPathPO> list = list(ids); | |||||
if (list != null && !list.isEmpty()) { | |||||
Map<Long, AttachPathPO> map = new HashMap<>(list.size()); | |||||
list.forEach((item) -> map.put(item.getAttachId(), item)); | |||||
return map; | |||||
} | |||||
return null; | |||||
} | |||||
// 保持文件,删除临时文件记录,避免被定时删除 | |||||
public void retain(String filePath) { | |||||
} | |||||
// 保持文件,删除临时文件记录,避免被定时删除 | |||||
public void retain(Long fileId) { | |||||
} | |||||
} |
@@ -0,0 +1,13 @@ | |||||
package cc.smtweb.system.bpm.spring.entity; | |||||
import lombok.Data; | |||||
@Data | |||||
public class AttachPathPO { | |||||
private Long attachId; | |||||
private String attachName; | |||||
private String attachPath; | |||||
private String attachContentType; | |||||
private Long attachSize; | |||||
private Long attachCreate; | |||||
} |
@@ -0,0 +1,8 @@ | |||||
package cc.smtweb.system.bpm.spring.entity; | |||||
import lombok.Data; | |||||
@Data | |||||
public class FileDataVO { | |||||
private String data; | |||||
} |
@@ -0,0 +1,15 @@ | |||||
package cc.smtweb.system.bpm.spring.entity; | |||||
import lombok.Data; | |||||
@Data | |||||
public class UploadDataVO { | |||||
private Long id; | |||||
private Integer height; | |||||
private Integer width; | |||||
private long size; | |||||
private String path; | |||||
private String name; | |||||
private String contentType; | |||||
private String url; | |||||
} |
@@ -0,0 +1,106 @@ | |||||
package cc.smtweb.system.bpm.util; | |||||
import cc.smtweb.framework.core.util.DateUtil; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.apache.commons.lang3.time.DateUtils; | |||||
import java.io.File; | |||||
import java.sql.Timestamp; | |||||
import java.text.SimpleDateFormat; | |||||
import java.util.Random; | |||||
/** | |||||
* 动态文件〈文件路径〉 | |||||
* | |||||
* @author kevin | |||||
* @since 1.0.0 | |||||
*/ | |||||
@Slf4j | |||||
public class FileDynPath extends FileFixPath { | |||||
// 目录允许的最大文件数量,避免批量导入文件时文件太多 | |||||
private static final int MAX_FILE_COUNT = 2000; | |||||
private static final int MAX_DIR_COUNT = 100000; | |||||
private long startTime; | |||||
private long endTime; | |||||
private final SimpleDateFormat sdf; | |||||
// 文件数量 | |||||
private int fileCount; | |||||
// 目录子索引 | |||||
private int pathIndex; | |||||
public FileDynPath(String rootPath, String typeDir, SimpleDateFormat sdf) { | |||||
super(rootPath, typeDir); | |||||
this.sdf = sdf; | |||||
} | |||||
/** | |||||
* 返回日期路径字符串 | |||||
*/ | |||||
@Override | |||||
public FilePathInfo makeDatePath(long fileId, String fileExt) { | |||||
long now = System.currentTimeMillis(); | |||||
String fileName; | |||||
// 如果不在就需要重新创建子目录 | |||||
if (now < startTime || now >= endTime) { | |||||
startTime = DateUtil.getTimesmorning(now); | |||||
endTime = startTime + DateUtils.MILLIS_PER_DAY; | |||||
this.path = this.typeDir + "/" + sdf.format(new Timestamp(now)); | |||||
createFolder(rootPath + this.path); | |||||
} | |||||
// 如果文件数量太大就需要创建新子目录 | |||||
while (this.fileCount >= MAX_FILE_COUNT) { | |||||
this.pathIndex++; | |||||
if(this.pathIndex > MAX_DIR_COUNT) { | |||||
throw new RuntimeException("dir is two many"); | |||||
} | |||||
createFolder(rootPath + getSubPath()); | |||||
} | |||||
Random random = new Random(); | |||||
int randomId = random.nextInt(Integer.MAX_VALUE); | |||||
fileName = Long.toHexString(fileId) + "_" + Integer.toHexString(randomId) + fileExt; | |||||
return new FilePathInfo(rootPath, getSubPath(), now, fileName, fileId); | |||||
} | |||||
private String getSubPath() { | |||||
if (this.pathIndex > 0) { | |||||
return String.format("%s%02d/%04d", this.path, MAX_DIR_COUNT / 1000, this.pathIndex % 1000); | |||||
} | |||||
return this.path; | |||||
} | |||||
private boolean createFolder(String path) { | |||||
File file = new File(path); | |||||
if (file.exists()) { | |||||
if (!file.isDirectory()) { | |||||
return false; | |||||
} | |||||
File[] list = file.listFiles(); | |||||
if (list != null) { | |||||
this.fileCount = list.length; | |||||
} else { | |||||
this.fileCount = 0; | |||||
} | |||||
return true; | |||||
} | |||||
if (!file.mkdirs()) { | |||||
log.error("unable to create folders {}.", rootPath + this.path); | |||||
return false; | |||||
} | |||||
log.debug("create folders {}.", file); | |||||
this.fileCount = 0; | |||||
return true; | |||||
} | |||||
} |
@@ -0,0 +1,51 @@ | |||||
package cc.smtweb.system.bpm.util; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import java.io.File; | |||||
/** | |||||
* 〈文件路径〉 | |||||
* | |||||
* @author kevin | |||||
* @since 1.0.0 | |||||
*/ | |||||
@Slf4j | |||||
public class FileFixPath { | |||||
protected String path; | |||||
protected String rootPath; | |||||
protected String typeDir; | |||||
public FileFixPath(String rootPath, String typeDir) { | |||||
this.rootPath = rootPath; | |||||
this.typeDir = typeDir; | |||||
} | |||||
public FilePathInfo makeDatePath(long fileId, String fileName) { | |||||
long now = System.currentTimeMillis(); | |||||
this.path = this.typeDir + "/"; | |||||
createFolder(rootPath + this.path); | |||||
return new FilePathInfo(rootPath, this.path, now, fileName, fileId); | |||||
} | |||||
private boolean createFolder(String path) { | |||||
File file = new File(path); | |||||
if (file.exists()) { | |||||
if (!file.isDirectory()) { | |||||
return false; | |||||
} | |||||
return true; | |||||
} | |||||
if (!file.mkdirs()) { | |||||
log.error("unable to create folders {}.", rootPath + this.path); | |||||
return false; | |||||
} | |||||
log.debug("create folders {}.", file); | |||||
return true; | |||||
} | |||||
} |
@@ -0,0 +1,132 @@ | |||||
package cc.smtweb.system.bpm.util; | |||||
import java.text.SimpleDateFormat; | |||||
import java.util.HashMap; | |||||
import java.util.Map; | |||||
import lombok.Getter; | |||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.apache.tika.mime.MimeType; | |||||
import org.apache.tika.mime.MimeTypeException; | |||||
import org.apache.tika.mime.MimeTypes; | |||||
import org.springframework.web.multipart.MultipartFile; | |||||
import cc.smtweb.framework.core.db.jdbc.IdGenerator; | |||||
/** | |||||
* 文件名生成规则 subDir/[yyyymm]/[d]/[hex(fileid)]_[hex(rand)].[fileExt] 如果文件是图片格式,会生成缩略图,文件名会直接添加.thumb.jpg后缀 规则参数 yyyymm: | |||||
* 时间的年月,固定6位字符。如200505 d: 时间的日期,值范围1~31。如5 fileid: 上传文件的ID,hex(int64) rand: 防盗链随机数,hex(int32)。 fileExt: 文件扩展名。 | |||||
*/ | |||||
@Slf4j | |||||
public class FilePathGenerator { | |||||
public static final String THUMB_FILE_EXT = ".thumb.jpg"; | |||||
// 文件时间是否作为PK | |||||
private SimpleDateFormat sdf; | |||||
@Getter | |||||
private String rootPath; | |||||
private Map<String, FileFixPath> fileFxPathMap = new HashMap<>(); | |||||
private Map<String, FileDynPath> fileDynPathMap = new HashMap<>(); | |||||
private String fileUrl; | |||||
private IdGenerator idGenerator; | |||||
public FilePathGenerator(String rootPath, String fileUrl, IdGenerator idGenerator) { | |||||
this.fileUrl = fixEnd(fileUrl); | |||||
this.idGenerator = idGenerator; | |||||
this.rootPath = fixEnd(rootPath); | |||||
sdf = new SimpleDateFormat("yyyyMM/dd/"); | |||||
} | |||||
private static String fixEnd(String path) { | |||||
if (path.endsWith("/") || path.endsWith("\\")) { | |||||
return path; | |||||
} else { | |||||
return path + "/"; | |||||
} | |||||
} | |||||
/** | |||||
* 生成文件路径,根据日期分目录存储 | |||||
* | |||||
* @param subPath 子目录,区分不同应用的文件 | |||||
* @param originalFileName 原始的文件名,用于提取扩展名用 | |||||
* @return 文件路径信息类 | |||||
*/ | |||||
public FilePathInfo make(String subPath, String originalFileName) { | |||||
return make(subPath, originalFileName, null, false); | |||||
} | |||||
public FilePathInfo make(String subPath, String originalFileName, boolean keepName) { | |||||
return make(subPath, originalFileName, null, keepName); | |||||
} | |||||
/** | |||||
* 生成文件路径,根据日期分目录存储 | |||||
* | |||||
* @param subPath 子目录,区分不同应用的文件 | |||||
* @param multipartFile 上传文件流,用于提取扩展名用 | |||||
* @return 文件路径信息类 | |||||
*/ | |||||
public FilePathInfo make(String subPath, MultipartFile multipartFile) { | |||||
return make(subPath, multipartFile.getOriginalFilename(), multipartFile.getContentType(), false); | |||||
} | |||||
private synchronized FilePathInfo make(String subPath, String originFileName, String contentType, boolean keepName) { | |||||
if (keepName) { | |||||
FileFixPath filePathSub = fileFxPathMap.get(subPath); | |||||
if (filePathSub == null) { | |||||
filePathSub = new FileFixPath(this.rootPath, subPath); | |||||
fileFxPathMap.put(subPath, filePathSub); | |||||
} | |||||
return filePathSub.makeDatePath(this.idGenerator.nextId(), originFileName); | |||||
} else { | |||||
FileDynPath filePathSub = fileDynPathMap.get(subPath); | |||||
if (filePathSub == null) { | |||||
filePathSub = new FileDynPath(this.rootPath, subPath, sdf); | |||||
fileDynPathMap.put(subPath, filePathSub); | |||||
} | |||||
return filePathSub.makeDatePath(this.idGenerator.nextId(), ext(originFileName, contentType)); | |||||
} | |||||
} | |||||
private static String ext(String filename, String contentType) { | |||||
int index = filename.lastIndexOf("."); | |||||
if (index == -1) { | |||||
if (contentType != null) { | |||||
MimeTypes allTypes = MimeTypes.getDefaultMimeTypes(); | |||||
try { | |||||
MimeType jpeg = allTypes.forName(contentType); | |||||
return jpeg.getExtension(); | |||||
} catch (MimeTypeException e) { | |||||
log.error(contentType, e); | |||||
} | |||||
} | |||||
return ""; | |||||
} | |||||
return filename.substring(index); | |||||
} | |||||
// 根据数据库存储文件路径获取URL | |||||
public String getFileUrl(FilePathInfo filePathInfo) { | |||||
return this.fileUrl + filePathInfo.getMysqlFilePath(); | |||||
} | |||||
// 根据数据库存储文件路径获取URL | |||||
public String getFileUrl(String mysqlFilePath) { | |||||
return this.fileUrl + mysqlFilePath; | |||||
} | |||||
// 根据数据库存储文件路径获取磁盘存储路径 | |||||
public String getFileDiskPath(String mysqlFilePath) { | |||||
return this.rootPath + mysqlFilePath; | |||||
} | |||||
// 获取下载路径前缀 | |||||
public String getDownloadUrl() { | |||||
return this.fileUrl; | |||||
} | |||||
} |
@@ -0,0 +1,48 @@ | |||||
package cc.smtweb.system.bpm.util; | |||||
import lombok.Getter; | |||||
/** | |||||
* 数据库需要存储 | |||||
* fileId, fileTime, subPath + fileName | |||||
*/ | |||||
@Getter | |||||
public class FilePathInfo { | |||||
// 文件ID | |||||
private long fileId; | |||||
// 文件创建时间,数据库需要存储 | |||||
private long fileTime; | |||||
// 文件子路径 | |||||
private String subPath; | |||||
// 文件名 | |||||
private String fileName; | |||||
// 本地根路径 | |||||
private String rootPath; | |||||
public FilePathInfo(String rootPath, String subPath, long fileTime, String fileName, long fileId) { | |||||
this.rootPath = rootPath; | |||||
this.subPath = subPath; | |||||
this.fileTime = fileTime; | |||||
this.fileName = fileName; | |||||
this.fileId = fileId; | |||||
} | |||||
/** | |||||
* 获取本地需要存储的文件全路径 | |||||
*/ | |||||
public String getFullFileName() { | |||||
return getDiskFilePath(); | |||||
} | |||||
public String getDiskFilePath() { | |||||
return this.rootPath + subPath + fileName; | |||||
} | |||||
/** | |||||
* 获取数据库存储需要的文件全路径 | |||||
*/ | |||||
public String getMysqlFilePath() { | |||||
return subPath + fileName; | |||||
} | |||||
} |
@@ -0,0 +1,79 @@ | |||||
package cc.smtweb.system.bpm.util; | |||||
import org.apache.commons.codec.binary.Base64; | |||||
import org.springframework.web.multipart.MultipartFile; | |||||
import java.io.*; | |||||
public class MemMultipartFile implements MultipartFile { | |||||
private static final String DATA_IMAGE = "data:image/"; | |||||
private byte[] data; | |||||
private String contentType; | |||||
private String filename; | |||||
public static MemMultipartFile build(String dataUrl) { | |||||
if (dataUrl != null && dataUrl.startsWith(DATA_IMAGE)) { | |||||
// data:image/png;base64, | |||||
int pos1 = dataUrl.indexOf(';', DATA_IMAGE.length()); | |||||
int pos2 = dataUrl.indexOf(',', DATA_IMAGE.length()); | |||||
if (pos1 > 0 && pos2 > pos1) { | |||||
byte[] data = Base64.decodeBase64(dataUrl.substring(pos2)); | |||||
if (data != null) { | |||||
String contentType = dataUrl.substring(5, pos1); | |||||
return new MemMultipartFile(contentType.replace('/', '.'), contentType, data); | |||||
} | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
private MemMultipartFile(String filename, String contentType, byte[] data) { | |||||
this.data = data; | |||||
this.contentType = contentType; | |||||
this.filename = filename; | |||||
} | |||||
@Override | |||||
public String getName() { | |||||
return "data"; | |||||
} | |||||
@Override | |||||
public String getOriginalFilename() { | |||||
return filename; | |||||
} | |||||
@Override | |||||
public String getContentType() { | |||||
return contentType; | |||||
} | |||||
@Override | |||||
public boolean isEmpty() { | |||||
return data.length == 0; | |||||
} | |||||
@Override | |||||
public long getSize() { | |||||
return data.length; | |||||
} | |||||
@Override | |||||
public byte[] getBytes() throws IOException { | |||||
return data; | |||||
} | |||||
@Override | |||||
public InputStream getInputStream() throws IOException { | |||||
return new ByteArrayInputStream(data); | |||||
} | |||||
@Override | |||||
public void transferTo(File file) throws IOException, IllegalStateException { | |||||
try(FileOutputStream os = new FileOutputStream(file)) { | |||||
os.write(data); | |||||
} | |||||
} | |||||
} |
@@ -0,0 +1,109 @@ | |||||
package cc.smtweb.system.bpm.util; | |||||
import java.awt.Color; | |||||
import java.awt.image.BufferedImage; | |||||
import java.io.File; | |||||
import java.io.IOException; | |||||
import java.util.List; | |||||
import javax.imageio.ImageIO; | |||||
import lombok.Getter; | |||||
import net.coobird.thumbnailator.Thumbnails; | |||||
import net.coobird.thumbnailator.geometry.Positions; | |||||
import net.coobird.thumbnailator.resizers.configurations.Antialiasing; | |||||
import net.sf.image4j.codec.ico.ICODecoder; | |||||
import org.apache.commons.lang3.StringUtils; | |||||
/** | |||||
* 缩略图生成工具 | |||||
* @author xkliu | |||||
*/ | |||||
@Getter | |||||
public class ThumbImage { | |||||
// 图片处理方式 | |||||
public static final int TYPE_DEFAULT = 1; | |||||
public static final int TYPE_THUMB = 2; | |||||
public static final int TYPE_AVATAR = 3; | |||||
private int imageWidth; | |||||
private int imageHeight; | |||||
public static int type(String thumb) { | |||||
// 解决历史遗留boolean类型 | |||||
if (StringUtils.isBlank(thumb) || "false".equalsIgnoreCase(thumb)) { | |||||
return TYPE_DEFAULT; | |||||
} | |||||
if ("true".equalsIgnoreCase(thumb)) { | |||||
return TYPE_THUMB; | |||||
} | |||||
return Integer.parseInt(thumb); | |||||
} | |||||
public void makeThumb(boolean isThumb, File file, int size) throws IOException { | |||||
makeThumb(file, size, size, isThumb); | |||||
} | |||||
// 后台等比压缩后大小最好控制在20k以内 | |||||
public void makeThumb(File file, int w, int h, boolean keepAspectRatio) throws IOException { | |||||
String fileName = file.getName().toLowerCase(); | |||||
BufferedImage image; | |||||
if (fileName.endsWith(".ico")) { | |||||
List<BufferedImage> images = ICODecoder.read(file); | |||||
image = images.get(images.size() - 1); | |||||
} else { | |||||
image = ImageIO.read(file); | |||||
} | |||||
imageWidth = image.getWidth(); | |||||
imageHeight = image.getHeight(); | |||||
if (fileName.endsWith(".png") || fileName.endsWith(".gif")) { | |||||
// 把透明的图填充白色背景 | |||||
BufferedImage newBufferedImage = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB); | |||||
newBufferedImage.createGraphics().drawImage(image, 0, 0, Color.WHITE, null); | |||||
image = newBufferedImage; | |||||
} | |||||
Thumbnails.Builder<BufferedImage> builder = Thumbnails.of(image); | |||||
if (keepAspectRatio) { | |||||
if (h > 0) { | |||||
// 高度为基准调整宽度到达原图缩放比例 | |||||
int imageR = imageWidth * 1000 / imageHeight; | |||||
w = h * imageR / 1000; | |||||
} else { | |||||
// 宽度为基准调整宽度到达原图缩放比例 | |||||
int imageR = imageHeight * 1000 / imageWidth; | |||||
h = w * imageR / 1000; | |||||
} | |||||
// int r = w * 1000 / h; | |||||
// int imageR = imageWidth * 1000 / imageHeight; | |||||
// if (r != imageR) { | |||||
// w = imageHeight * r / 1000; | |||||
// } | |||||
} else { | |||||
int r = w * 1000 / h; | |||||
int imageR = imageWidth * 1000 / imageHeight; | |||||
if (r != imageR) { | |||||
int width = imageWidth; | |||||
int height = imageHeight; | |||||
if (r > imageR) { | |||||
width = imageHeight * r / 1000; | |||||
} else { | |||||
height = imageWidth * 1000 / r; | |||||
} | |||||
builder.sourceRegion(Positions.CENTER, width, height); | |||||
} | |||||
} | |||||
builder.size(w, h).antialiasing(Antialiasing.ON).outputFormat("jpg").outputQuality(0.9) | |||||
.toFile(file.getAbsolutePath() + FilePathGenerator.THUMB_FILE_EXT); | |||||
} | |||||
} |
@@ -0,0 +1,109 @@ | |||||
package cc.smtweb.system.bpm.web.sys.user.area; | |||||
import cc.smtweb.framework.core.annotation.SwTable; | |||||
import cc.smtweb.framework.core.common.SwMap; | |||||
import cc.smtweb.framework.core.db.impl.DefaultEntity; | |||||
/** | |||||
* Created by 1 at 2022-06-17 07:58:14 | |||||
* 实体【[行政区划](SYS_AREA)】的Entity类 | |||||
*/ | |||||
@SwTable("SYS_AREA") | |||||
public class Area extends DefaultEntity { | |||||
public static final String ENTITY_NAME = "SYS_AREA"; | |||||
public Area() { | |||||
super(ENTITY_NAME); | |||||
} | |||||
/** 主键 */ | |||||
public long getId() { | |||||
return getLong("ar_id"); | |||||
} | |||||
/** 主键 */ | |||||
public void setId(long ar_id) { | |||||
put("ar_id", ar_id); | |||||
} | |||||
/** 编码 */ | |||||
public String getCode() { | |||||
return getStr("ar_code"); | |||||
} | |||||
/** 编码 */ | |||||
public void setCode(String ar_code) { | |||||
put("ar_code", ar_code); | |||||
} | |||||
/** 名称 */ | |||||
public String getName() { | |||||
return getStr("ar_name"); | |||||
} | |||||
/** 名称 */ | |||||
public void setName(String ar_name) { | |||||
put("ar_name", ar_name); | |||||
} | |||||
/** 父ID */ | |||||
public long getParentId() { | |||||
return getLong("ar_parent_id"); | |||||
} | |||||
/** 父ID */ | |||||
public void setParentId(long ar_parent_id) { | |||||
put("ar_parent_id", ar_parent_id); | |||||
} | |||||
/** 级次码 */ | |||||
public String getLevelCode() { | |||||
return getStr("ar_level_code"); | |||||
} | |||||
/** 级次码 */ | |||||
public void setLevelCode(String ar_level_code) { | |||||
put("ar_level_code", ar_level_code); | |||||
} | |||||
/** 全称 */ | |||||
public String getFullName() { | |||||
return getStr("ar_full_name"); | |||||
} | |||||
/** 全称 */ | |||||
public void setFullName(String ar_full_name) { | |||||
put("ar_full_name", ar_full_name); | |||||
} | |||||
/** 级次 */ | |||||
public int getType() { | |||||
return getInt("ar_type"); | |||||
} | |||||
/** 级次 */ | |||||
public void setType(int ar_type) { | |||||
put("ar_type", ar_type); | |||||
} | |||||
/** 状态 */ | |||||
public boolean isStatu() { | |||||
return getBool("ar_statu"); | |||||
} | |||||
/** 状态 */ | |||||
public void set(boolean ar_statu) { | |||||
setBool("ar_statu", ar_statu); | |||||
} | |||||
/** 备注 */ | |||||
public String getRemark() { | |||||
return getStr("ar_remark"); | |||||
} | |||||
/** 备注 */ | |||||
public void setRemark(String ar_remark) { | |||||
put("ar_remark", ar_remark); | |||||
} | |||||
/** 排序码 */ | |||||
public int getSeq() { | |||||
return getInt("ar_seq"); | |||||
} | |||||
/** 排序码 */ | |||||
public void setSeq(int ar_seq) { | |||||
put("ar_seq", ar_seq); | |||||
} | |||||
} |
@@ -0,0 +1,42 @@ | |||||
package cc.smtweb.system.bpm.web.sys.user.area; | |||||
import cc.smtweb.framework.core.annotation.SwCache; | |||||
import cc.smtweb.framework.core.cache.AbstractEntityCache; | |||||
import cc.smtweb.framework.core.cache.CacheManager; | |||||
import java.util.ArrayList; | |||||
import java.util.Comparator; | |||||
import java.util.List; | |||||
import java.util.Set; | |||||
/** | |||||
* Created by 1 at 2022-06-17 07:58:14 | |||||
* 实体【[行政区划](SYS_AREA)】的缓存类 | |||||
*/ | |||||
@SwCache(ident = "SYS_AREA", title = "页面定义") | |||||
public class AreaCache extends AbstractEntityCache<Area> { | |||||
//缓存key:按父ID | |||||
public final static String mk_pr = "pr"; | |||||
//缓存key:按编码 | |||||
public final static String mk_code = "code"; | |||||
public static AreaCache getInstance() { | |||||
return CacheManager.getIntance().getCache(AreaCache.class); | |||||
} | |||||
public AreaCache() { | |||||
//缓存key:按父ID | |||||
regList(mk_pr, "ar_parent_id"); | |||||
//缓存key:按编码 | |||||
regList(mk_code, "ar_code"); | |||||
} | |||||
//缓存key:按父ID | |||||
public final Set<Area> getByPr(String key) { | |||||
return getListByKey(mk_pr, key); | |||||
} | |||||
//缓存key:按编码 | |||||
public final Set<Area> getByCode(String key) { | |||||
return getListByKey(mk_code, key); | |||||
} | |||||
} |
@@ -0,0 +1,30 @@ | |||||
package cc.smtweb.system.bpm.web.sys.user.area; | |||||
import cc.smtweb.framework.core.annotation.SwBody; | |||||
import cc.smtweb.framework.core.annotation.SwService; | |||||
import cc.smtweb.framework.core.common.R; | |||||
import cc.smtweb.framework.core.common.SwMap; | |||||
import cc.smtweb.system.bpm.web.engine.dynPage.DynPageService; | |||||
import cc.smtweb.framework.core.mvc.service.AbstractHandler; | |||||
import cc.smtweb.framework.core.session.UserSession; | |||||
/** | |||||
* Created by 1 at 2022-06-17 07:58:14 | |||||
* 页面【[区划卡片]的服务类 | |||||
*/ | |||||
@SwService | |||||
public class AreaService extends DynPageService { | |||||
//public final static String TYPE_DEMO = "demo"; | |||||
@Override | |||||
protected AbstractHandler createHandler(String type) { | |||||
return super.createHandler(type); | |||||
} | |||||
/* demo | |||||
//自定义 | |||||
public R demo(@SwBody SwMap params, UserSession us) { | |||||
return pageHandler(params, us, TYPE_DEMO, handler -> ((DemoHandler)handler).demo()); | |||||
} | |||||
*/ | |||||
} |