diff --git a/smtweb-system/sw-system-bpm/pom.xml b/smtweb-system/sw-system-bpm/pom.xml
index 0cc2989..bc58b5e 100644
--- a/smtweb-system/sw-system-bpm/pom.xml
+++ b/smtweb-system/sw-system-bpm/pom.xml
@@ -35,6 +35,16 @@
spring-boot-starter-freemarker
+ net.coobird
+ thumbnailator
+ [0.4, 0.5)
+
+
+ org.jclarion
+ image4j
+ 0.7
+
+
com.fasterxml.jackson.dataformat
jackson-dataformat-yaml
2.11.0
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/config/FileConfig.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/config/FileConfig.java
new file mode 100644
index 0000000..19475c5
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/config/FileConfig.java
@@ -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);
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/controller/FileDownloadController.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/controller/FileDownloadController.java
new file mode 100644
index 0000000..40e6795
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/controller/FileDownloadController.java
@@ -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 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 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 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 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;
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/controller/FileUploadController.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/controller/FileUploadController.java
new file mode 100644
index 0000000..b0f1404
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/controller/FileUploadController.java
@@ -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();
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/dao/ImageAttachDao.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/dao/ImageAttachDao.java
new file mode 100644
index 0000000..322d293
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/dao/ImageAttachDao.java
@@ -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());
+ }
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/dao/SysAttachDao.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/dao/SysAttachDao.java
new file mode 100644
index 0000000..166e76b
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/dao/SysAttachDao.java
@@ -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 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 map(Long[] ids) {
+ List list = list(ids);
+ if (list != null && !list.isEmpty()) {
+ Map 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) {
+
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/AttachPathPO.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/AttachPathPO.java
new file mode 100644
index 0000000..399bfc7
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/AttachPathPO.java
@@ -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;
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/FileDataVO.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/FileDataVO.java
new file mode 100644
index 0000000..5754e26
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/FileDataVO.java
@@ -0,0 +1,8 @@
+package cc.smtweb.system.bpm.spring.entity;
+
+import lombok.Data;
+
+@Data
+public class FileDataVO {
+ private String data;
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/UploadDataVO.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/UploadDataVO.java
new file mode 100644
index 0000000..d67aeb4
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/spring/entity/UploadDataVO.java
@@ -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;
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FileDynPath.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FileDynPath.java
new file mode 100644
index 0000000..34e841b
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FileDynPath.java
@@ -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;
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FileFixPath.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FileFixPath.java
new file mode 100644
index 0000000..5838559
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FileFixPath.java
@@ -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;
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FilePathGenerator.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FilePathGenerator.java
new file mode 100644
index 0000000..4d307ba
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FilePathGenerator.java
@@ -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 fileFxPathMap = new HashMap<>();
+ private Map 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;
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FilePathInfo.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FilePathInfo.java
new file mode 100644
index 0000000..de9557e
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/FilePathInfo.java
@@ -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;
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/MemMultipartFile.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/MemMultipartFile.java
new file mode 100644
index 0000000..33f5681
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/MemMultipartFile.java
@@ -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);
+ }
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/ThumbImage.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/ThumbImage.java
new file mode 100644
index 0000000..e3b2d22
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/util/ThumbImage.java
@@ -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 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 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);
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/Area.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/Area.java
new file mode 100644
index 0000000..484fbd3
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/Area.java
@@ -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);
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/AreaCache.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/AreaCache.java
new file mode 100644
index 0000000..f09f144
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/AreaCache.java
@@ -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 {
+ //缓存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 getByPr(String key) {
+ return getListByKey(mk_pr, key);
+ }
+ //缓存key:按编码
+ public final Set getByCode(String key) {
+ return getListByKey(mk_code, key);
+ }
+}
diff --git a/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/AreaService.java b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/AreaService.java
new file mode 100644
index 0000000..1fb31fc
--- /dev/null
+++ b/smtweb-system/sw-system-bpm/src/main/java/cc/smtweb/system/bpm/web/sys/user/area/AreaService.java
@@ -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());
+ }
+*/
+
+}