@@ -35,6 +35,16 @@ | |||
<artifactId>spring-boot-starter-freemarker</artifactId> | |||
</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> | |||
<artifactId>jackson-dataformat-yaml</artifactId> | |||
<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()); | |||
} | |||
*/ | |||
} |