Feat: 实现用户模块和文件模块

This commit is contained in:
wangyu 2021-01-09 16:57:07 +08:00
parent 7ef112775b
commit 470683473d
50 changed files with 1708 additions and 5 deletions

View File

@ -1,5 +1,7 @@
package com.flyfish.framework.annotations;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
@ -16,8 +18,22 @@ public @interface Property {
* 显示标题
* @return 结果
*/
@AliasFor("title")
String value() default "";
/**
* 显示标题别名
* @return 结果
*/
@AliasFor("value")
String title() default "";
/**
* 描述
* @return 结果
*/
String description() default "";
/**
* 被继承的用于父类自动拼接名称
* @return 结果

View File

@ -25,21 +25,21 @@ public abstract class AuditDomain extends Domain {
* 创建日期
*/
@CreatedDate
@Property(title = "创建日期")
@Property("创建日期")
protected Date createTime;
/**
* 修改日期
*/
@LastModifiedDate
@Property(title = "更新日期")
@Property("更新日期")
protected Date modifyTime;
/**
* 创建者
*/
@CreatedBy
@Property(title = "创建人")
@Property("创建人")
protected String creator;
/**
@ -51,7 +51,7 @@ public abstract class AuditDomain extends Domain {
* 修改者
*/
@LastModifiedBy
@Property(title = "更新人")
@Property("更新人")
protected String modifier;
/**

View File

@ -0,0 +1,23 @@
package com.flyfish.framework.domain.base;
import com.flyfish.framework.builder.CriteriaBuilder;
import lombok.Getter;
import lombok.Setter;
/**
* 属性菜单的qo
*/
@Getter
@Setter
public class TreeQo<T extends Domain> extends NameLikeQo<T> {
private Boolean leaf;
private Integer level;
@Override
public CriteriaBuilder<T> criteriaBuilder() {
return super.criteriaBuilder()
.with("level", "leaf");
}
}

View File

@ -24,6 +24,7 @@ import java.util.List;
public class User extends AuditDomain implements IUser {
private static final long serialVersionUID = -960011918745179950L;
/**
* 用户类型
*/

27
flyfish-file/pom.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>flyfish-framework</artifactId>
<groupId>com.flyfish.framework</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>flyfish-file</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.flyfish.framework</groupId>
<artifactId>flyfish-web</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,26 @@
package com.flyfish.framework.file.config;
import com.flyfish.framework.file.utils.SystemUtils;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Configuration
@ConfigurationProperties(prefix = "upload")
@Getter
@Setter
public class UploadConfiguration {
private String localPath = SystemUtils.isWindows() ? "C:/file-temp/" : "/usr/local/resources/";
public String path(List<Supplier<String>> generator) {
return generator.stream().map(Supplier::get)
.collect(Collectors.joining("/"));
}
}

View File

@ -0,0 +1,14 @@
package com.flyfish.framework.file.controller;
import com.flyfish.framework.controller.BaseController;
import com.flyfish.framework.file.domain.Attachment;
import com.flyfish.framework.file.domain.AttachmentQo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/attachments")
public class AttachmentController extends BaseController<Attachment, AttachmentQo> {
}

View File

@ -0,0 +1,64 @@
package com.flyfish.framework.file.controller;
import com.flyfish.framework.bean.Result;
import com.flyfish.framework.beans.annotations.RestMapping;
import com.flyfish.framework.file.config.UploadConfiguration;
import com.flyfish.framework.file.domain.Attachment;
import com.flyfish.framework.file.service.AttachmentService;
import com.flyfish.framework.file.utils.DownloadUtils;
import com.flyfish.framework.service.BaseService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestPart;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* 附件上传相关
* @author wangyu
*/
@RestMapping("/attachments")
public class AttachmentUploadController {
@Resource
private UploadConfiguration configuration;
@Resource
private AttachmentService attachmentService;
/**
* 上传媒体支持多个同时上传
*
* @param files 文件
* @return 结果
*/
@PostMapping("")
public Mono<Result<List<Attachment>>> uploadAttachment(@RequestPart("file") Flux<FilePart> files) {
return files.flatMap(attachmentService::upload)
.reduce(Result.accept(new ArrayList<>()), ((listResult, attachment) -> {
listResult.getData().add(attachment);
return listResult;
}));
}
@GetMapping("/**")
public Mono<Void> downloadStatic(ServerHttpRequest request, ServerHttpResponse response) {
String path = StringUtils.substringAfterLast(request.getURI().getPath(), "/attachments");
return DownloadUtils.download(configuration.getLocalPath() + path, response);
}
@GetMapping("/downloads/{id}")
public Mono<Void> downloadAttachment(@PathVariable String id, ServerHttpResponse response) {
return Mono.justOrEmpty(attachmentService.getById(id))
.flatMap(attachment -> DownloadUtils.download(configuration.getLocalPath() +
attachment.getPath(), response));
}
}

View File

@ -0,0 +1,29 @@
package com.flyfish.framework.file.domain;
import com.flyfish.framework.domain.base.AuditDomain;
import lombok.*;
import org.springframework.data.mongodb.core.mapping.Document;
@Document(collection = "attachments")
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Attachment extends AuditDomain {
/**
* 大小
*/
private String size;
/**
* 附件路径
*/
private String path;
/**
* 附件Url
*/
private String url;
}

View File

@ -0,0 +1,7 @@
package com.flyfish.framework.file.domain;
import com.flyfish.framework.domain.base.NameLikeQo;
public class AttachmentQo extends NameLikeQo<Attachment> {
}

View File

@ -0,0 +1,34 @@
package com.flyfish.framework.file.domain;
import com.flyfish.framework.file.enums.MediaType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Media {
/**
* 媒体类型
*/
private MediaType type;
/**
* 媒体路径
*/
private String url;
/**
* 媒体大小
*/
private String size;
/**
* 缩略图
*/
private String thumb;
}

View File

@ -0,0 +1,29 @@
package com.flyfish.framework.file.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Getter
@AllArgsConstructor
public enum MediaType {
UNKNOWN(Collections.emptyList(), "未知媒体"),
AUDIO(Arrays.asList("mp3", "wma", "amr"), "音频"),
VIDEO(Arrays.asList("mp4", "avi", "wmv", "mkv", "rmvb", "flv", "3gp", "m3u", "m3u8"), "视频"),
GRAPHICS(Arrays.asList("jpg", "jpeg", "bpm", "tiff", "png", "gif", "svg", "ico"), "图像"),
ARCHIVE(Arrays.asList("zip", "gz"), "压缩文档");
private final List<String> suffix;
private final String name;
public static MediaType accept(String filename) {
String extend = StringUtils.substringAfterLast(filename, ".");
return Arrays.stream(values()).filter(value -> value.suffix.contains(extend)).findFirst().orElse(UNKNOWN);
}
}

View File

@ -0,0 +1,36 @@
package com.flyfish.framework.file.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.MediaType;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
@AllArgsConstructor
@Getter
public enum MediaTypes {
OCT_STREAM(Collections.emptyList(), extend -> MediaType.APPLICATION_OCTET_STREAM),
IMAGE(Arrays.asList("jpg", "jpeg", "png", "gif"), extend -> {
return MediaType.valueOf("image/" + (extend.equalsIgnoreCase("jpg") ? "jpeg" : extend));
}),
HLS(Arrays.asList("m3u8"), extend -> {
return new MediaType("application", "vnd.apple.mpegurl");
});
private List<String> suffix;
private Function<String, MediaType> mediaType;
public static MediaType getMediaType(String filename) {
String extend = StringUtils.substringAfterLast(filename, ".");
return Arrays.stream(values()).filter(value -> value.suffix.contains(extend))
.map(type -> type.getMediaType().apply(extend))
.findFirst().orElse(MediaType.APPLICATION_OCTET_STREAM);
}
}

View File

@ -0,0 +1,8 @@
package com.flyfish.framework.file.repository;
import com.flyfish.framework.file.domain.Attachment;
import com.flyfish.framework.repository.DefaultRepository;
public interface AttachmentRepository extends DefaultRepository<Attachment> {
}

View File

@ -0,0 +1,38 @@
package com.flyfish.framework.file.service;
import com.flyfish.framework.file.domain.Attachment;
import com.flyfish.framework.file.utils.FileSizeUtils;
import com.flyfish.framework.service.impl.BaseServiceImpl;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
@Service
public class AttachmentService extends BaseServiceImpl<Attachment> {
private static String URL = "/api/attachments/";
@Resource
private FileService fileService;
/**
* 上传媒体文件
*
* @param part 文件
* @return 结果
*/
public Mono<Attachment> upload(FilePart part) {
return fileService.saveLocal(part)
.map(path -> {
Attachment attachment = Attachment.builder()
.size(FileSizeUtils.size(part.headers().getContentLength()))
.path(path)
.url(URL + path)
.build();
attachment.setName(part.filename());
return create(attachment);
});
}
}

View File

@ -0,0 +1,57 @@
package com.flyfish.framework.file.service;
import com.flyfish.framework.file.config.UploadConfiguration;
import com.flyfish.framework.file.utils.SystemUtils;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
/**
* 文件服务
*
* @author wangyu
*/
@Service
public class FileService {
@Resource
private UploadConfiguration uploadConfiguration;
private List<Supplier<String>> generator = Arrays.asList(
SystemUtils::generateDatePath
);
/**
* 保存本地文件
*
* @return 保存到本地并返回相对路径
*/
public Mono<String> saveLocal(FilePart part) {
String localPath = uploadConfiguration.getLocalPath();
String path = uploadConfiguration.path(generator) + "/" + part.filename();
String fullPath = localPath + path;
SystemUtils.testPath(fullPath);
return part.transferTo(Paths.get(fullPath))
.then(Mono.just(path));
}
/**
* 根据指定目录保存文件
*
* @param part 文件
* @param path 路径
* @return 结果
*/
public Mono<String> saveLocalPath(FilePart part, String path) {
SystemUtils.testPath(path);
return part.transferTo(Paths.get(path))
.then(Mono.just(path));
}
}

View File

@ -0,0 +1,96 @@
package com.flyfish.framework.file.service;
import com.flyfish.framework.file.config.UploadConfiguration;
import com.flyfish.framework.file.domain.Media;
import com.flyfish.framework.file.enums.MediaType;
import com.flyfish.framework.file.utils.FileSizeUtils;
import com.flyfish.framework.file.utils.ZipUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.codec.multipart.FilePart;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.io.IOException;
/**
* 媒体服务
*
* @author wangyu
*/
@Service
@Slf4j
public class MediaService {
private static String URL = "/api/media/";
@Resource
private FileService fileService;
@Resource
private UploadConfiguration uploadConfiguration;
/**
* 上传媒体文件
*
* @param part 文件
* @return 结果
*/
public Mono<Media> upload(FilePart part) {
return fileService.saveLocal(part)
.map(path -> Media.builder()
.size(FileSizeUtils.size(part.headers().getContentLength()))
.type(MediaType.accept(part.filename()))
.url(URL + path)
.build()
);
}
/**
* 分析原媒体文件尝试解压
*
* @param part 原文件
* @return 结果
*/
public Mono<Media> slice(FilePart part) {
String localPath = uploadConfiguration.getLocalPath();
return fileService.saveLocal(part)
.flatMap(path -> {
try {
return ZipUtils.unzip(localPath + path)
.stream().filter(file -> file.endsWith(".m3u8"))
.map(file -> file.replace(localPath, ""))
.findFirst()
.map(Mono::just)
.orElse(Mono.just(path));
} catch (IOException e) {
log.error("很不幸,解压发生了错误:", e);
}
return Mono.just(path);
})
.map(path -> Media.builder()
.size(FileSizeUtils.size(part.headers().getContentLength()))
.type(MediaType.accept(part.filename()))
.url(URL + path)
.build()
);
}
/**
* 上传到绝对路径
*
* @param part 文件
* @param absolutePath 绝对路径
* @return 结果
*/
public Mono<Media> upload(FilePart part, String absolutePath) {
Mono<String> saver = StringUtils.isNotBlank(absolutePath) ? fileService.saveLocalPath(part, absolutePath) :
fileService.saveLocal(part);
return saver.map(path -> Media.builder()
.size(FileSizeUtils.size(part.headers().getContentLength()))
.type(MediaType.accept(part.filename()))
.url(URL + path)
.build()
);
}
}

View File

@ -0,0 +1,26 @@
package com.flyfish.framework.file.utils;
import com.flyfish.framework.file.enums.MediaTypes;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ZeroCopyHttpOutputMessage;
import org.springframework.http.server.reactive.ServerHttpResponse;
import reactor.core.publisher.Mono;
import java.nio.file.Path;
import java.nio.file.Paths;
public abstract class DownloadUtils {
public static Mono<Void> download(String path, ServerHttpResponse response) {
ZeroCopyHttpOutputMessage zeroCopyResponse = (ZeroCopyHttpOutputMessage) response;
HttpHeaders headers = zeroCopyResponse.getHeaders();
headers.setContentType(MediaTypes.getMediaType(path));
headers.setContentDisposition(ContentDisposition.builder("attachment")
.filename(StringUtils.substringAfterLast(path, "/"))
.build());
Path file = Paths.get(path);
return zeroCopyResponse.writeWith(file, 0, file.toFile().length());
}
}

View File

@ -0,0 +1,123 @@
/**
* FileMd5Util.java
* com.bibenet.cedp.utils
* <p>
* ****************************************************************************
* Change Log
* <p>
* 1. wangyu create me at 2016年7月16日上午9:48:14 for class DESC.
* 2.
* ****************************************************************************
* Copyright (c) 2016, www.bibenet.com All Rights Reserved.O(_)O
*/
package com.flyfish.framework.file.utils;
import com.flyfish.framework.utils.Assert;
import java.io.*;
import java.math.BigInteger;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @author wangyu
* @name <p>FileMD5Util</p>
* @desc 文件MD5校验工具 <1GB
*/
public class FileMD5Utils {
/**
* <p>getMd5ByFile</p>
* 获取文件的Md5
*
* @param file 文件
* @return md5
*/
public static String getMd5ByFile(File file) {
FileInputStream in = null;
try {
in = new FileInputStream(file);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return getMd5ByInputStream(in, file.length());
}
/**
* <p>getMd5ByInputStream</p>
* 获取流的Md5
*
* @param in 输入流
* @param length 文件长度
* @return md5
*/
public static String getMd5ByInputStream(InputStream in, Long length) {
String value = null;
Assert.notNull(in, "计算md5的输入流为null");
if (in instanceof FileInputStream) {
try {
MappedByteBuffer byteBuffer = ((FileInputStream) in).getChannel().map(FileChannel.MapMode.READ_ONLY, 0, length);
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(byteBuffer);
BigInteger bi = new BigInteger(1, md5.digest());
value = bi.toString(16);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
} else {
try {
value = getMd5ByASCIIStream(in, length);
} catch (NoSuchAlgorithmException | IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return value;
}
private static String getMd5ByASCIIStream(InputStream in, Long length) throws NoSuchAlgorithmException, IOException {
StringBuilder md5 = new StringBuilder();
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] dataBytes = new byte[1024];
int nread;
while ((nread = in.read(dataBytes)) != -1) {
md.update(dataBytes, 0, nread);
}
byte[] mdbytes = md.digest();
// 转换二进制到HEX
for (byte mdByte : mdbytes) {
md5.append(Integer.toString((mdByte & 0xff) + 0x100, 16).substring(1));
}
return md5.toString();
}
public static String getMd5ByPath(String path, Long length) {
Assert.notNull(path, "md5验证时文件路径为空");
try {
return getMd5ByInputStream(new FileInputStream(path), length);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -0,0 +1,51 @@
/**
* create by InteliJ Idea
* <p>
* ****************************************************************************
* Change Log
* <p>
* 1. wangyu create me at 2018年03月22日17:59 for class DESC. 2.
* ****************************************************************************
* Copyright (c) 2018, www.bibenet.com All Rights Reserved.O(_)O
*/
package com.flyfish.framework.file.utils;
/**
* 文件大小工具类
*
* @author wangyu
*/
public class FileSizeUtils {
public static String size(Long size) {
if (null != size) {
//如果字节数少于1024则直接以B为单位否则先除于1024后3位因太少无意义
if (size < 1024) {
return size + "B";
} else {
size = size / 1024;
}
//如果原字节数除于1024之后少于1024则可以直接以KB作为单位
//因为还没有到达要使用另一个单位的时候
//接下去以此类推
if (size < 1024) {
return size + "KB";
} else {
size = size / 1024;
}
if (size < 1024) {
//因为如果以MB为单位的话要保留最后1位小数
//因此把此数乘以100之后再取余
size = size * 100;
return size / 100 + "."
+ size % 100 + "MB";
} else {
//否则如果要以GB为单位的先除于1024再作同样的处理
size = size * 100 / 1024;
return size / 100 + "."
+ size % 100 + "GB";
}
}
return "0KB";
}
}

View File

@ -0,0 +1,95 @@
package com.flyfish.framework.file.utils;
import com.flyfish.framework.context.DateContext;
import com.flyfish.framework.utils.Assert;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileReader;
import java.io.LineNumberReader;
import java.util.Date;
public abstract class SystemUtils {
/**
* 判断是否是windows系统
*
* @return 结果
*/
public static boolean isWindows() {
String os = System.getProperty("os.name");
return os.toLowerCase().startsWith("win");
}
public static String generateDatePath() {
return DateContext.formatDate(new Date(), "yyyy/MM/dd");
}
public static String generateDatePath(Date date) {
return DateContext.formatDate(date, "yyyy/MM/dd");
}
/**
* 根据当前位置生成按照指定步长的区间
*
* @param pos 位置
* @param step 步长
* @return 结果
*/
public static String range(long pos, int step) {
// 求余
long left = pos % step;
// 求整数商
long times = pos / step;
// 得出区间
long min = times * step;
long max = (times + 1) * step;
// 返回
return min + "-" + max;
}
/**
* 根据基本路径生成时间戳//的目录接口并创建文件
*
* @param fullPath 基本路径
* @param fileName 文件名
* @return 结果
*/
public static File createFile(String fullPath, String fileName) {
testPath(fullPath);
return new File(fullPath + fileName);
}
/**
* 测试路径并创建
*
* @param fullPath 全路径
*/
public static void testPath(String fullPath) {
if (fullPath.contains(".")) {
fullPath = getPath(fullPath);
}
File directory = new File(fullPath);
if (!directory.exists()) {
if (!directory.mkdirs()) {
Assert.isTrue(directory.exists(), "目录创建失败!");
}
}
}
public static String getPath(String fullPath) {
return StringUtils.substringBeforeLast(fullPath, "/");
}
public static long getLineNumber(String fullPath) throws Exception {
try (FileReader in = new FileReader(fullPath);
LineNumberReader reader = new LineNumberReader(in)) {
long skipped = reader.skip(Long.MAX_VALUE);
if (skipped == 0) {
return 0;
}
return reader.getLineNumber();
}
}
}

View File

@ -0,0 +1,103 @@
package com.flyfish.framework.file.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.StreamUtils;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.zip.ZipFile;
/**
* zip解压缩工具
*
* @author wangyu
*/
@Slf4j
public class ZipUtils {
/**
* 解压到当前目录
*
* @param path 路径
*/
public static List<String> unzip(String path) throws IOException {
// 目录
String directory = StringUtils.substringBeforeLast(path, "/");
// 解压
return unzip(path, directory);
}
/**
* 解压文件到指定目录
* 解压后的文件名和之前一致
*
* @param path zip文件目录
* @param target 目标解压目录
*/
public static List<String> unzip(String path, String target) throws IOException {
// 解决中文文件夹乱码
ZipFile zip = new ZipFile(path, Charset.defaultCharset());
// 取得真实名字
String name = StringUtils.substringBefore(StringUtils.substringAfterLast(path, "/"), ".");
// 父目录
String parentPath = target + "/" + name;
Path pathFile = Paths.get(parentPath);
if (!Files.exists(pathFile)) {
Files.createDirectories(pathFile);
}
// 循环zip内的文件
List<String> files = zip.stream().map(entry -> {
String entryName = entry.getName();
String output;
try (InputStream in = zip.getInputStream(entry)) {
OutputStream out = prepare(output = parentPath + "/" + entryName);
// 拷贝所有流
StreamUtils.copy(in, out);
return output;
} catch (IOException e) {
log.error("[zip]解压某个文件发生异常:{}", e.getMessage());
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList());
log.debug("解压了:{}", files);
return files;
}
// public static void main(String[] args) throws IOException {
// unzip("/Users/wangyu/Documents/ceshi.m3u8");
// }
/**
* output
* 准备安全的输出环境
*
* @param output 输出路径
* @return 结果
*/
private static FileOutputStream prepare(String output) throws IOException {
// 输出文件路径信息
log.info("抽取{}", output);
// 判断路径是否存在,不存在则创建文件路径
Path targetPath = Paths.get(StringUtils.substringBeforeLast(output, "/"));
if (!Files.exists(targetPath)) {
Files.createDirectories(targetPath);
}
// 判断文件全路径是否为文件夹,如果是上面已经上传,不需要解压
if (Files.isDirectory(Paths.get(output))) {
throw new IOException("[unzip]文件路径为目录,跳过写入");
}
return new FileOutputStream(output);
}
}

3
flyfish-user/README.md Normal file
View File

@ -0,0 +1,3 @@
# 用户模块
提供快速的用户集成能力,提供多种组合配置

View File

@ -17,4 +17,17 @@
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.flyfish.framework</groupId>
<artifactId>flyfish-data</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.flyfish.framework</groupId>
<artifactId>flyfish-web</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,15 @@
package com.flyfish.framework.controller;
import com.flyfish.framework.domain.DepartmentQo;
import com.flyfish.framework.domain.po.Department;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 部分或者校区controller
*/
@RestController
@RequestMapping("/departments")
public class DepartmentController extends BaseController<Department, DepartmentQo> {
}

View File

@ -0,0 +1,15 @@
package com.flyfish.framework.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录controller
* @author wangyu
*/
@RestController
@RequestMapping("/login")
public class LoginController {
}

View File

@ -0,0 +1,16 @@
package com.flyfish.framework.controller;
import com.flyfish.framework.domain.PermissionQo;
import com.flyfish.framework.domain.po.Permission;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 权限controller
*
* @author wybab
*/
@RestController
@RequestMapping("/permissions")
public class PermissionController extends BaseController<Permission, PermissionQo> {
}

View File

@ -0,0 +1,17 @@
package com.flyfish.framework.controller;
import com.flyfish.framework.domain.RoleQo;
import com.flyfish.framework.domain.po.Role;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 角色controller
*
* @author wybab
*/
@RestController
@RequestMapping("/roles")
public class RoleController extends BaseController<Role, RoleQo> {
}

View File

@ -0,0 +1,52 @@
package com.flyfish.framework.controller;
import com.flyfish.framework.bean.Result;
import com.flyfish.framework.configuration.annotations.CurrentUser;
import com.flyfish.framework.domain.UserPasswordDto;
import com.flyfish.framework.domain.UserQo;
import com.flyfish.framework.domain.base.IUser;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.service.UserService;
import com.flyfish.framework.utils.Assert;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/users")
public class UserController extends BaseController<User, UserQo> {
/**
* 修改密码逻辑
*
* @param passwordDto 密码dto
* @param user 用户
* @return 结果
*/
@PutMapping("/passwords")
public Result<Void> changePassword(@RequestBody UserPasswordDto passwordDto, @CurrentUser User user) {
// 检查原密码
Assert.isTrue(user.getPassword().equals(passwordDto.getOldPassword()), "原密码不正确!");
Assert.isTrue(!user.getPassword().equals(passwordDto.getPassword()), "新密码和旧密码一致,输入个新的吧!");
userContext.setUser(user);
// 更新密码
User updating = new User();
updating.setId(user.getId());
updating.setPassword(passwordDto.getPassword());
service.updateSelectiveById(updating);
return Result.ok();
}
/**
* 获取当前用户
*
* @return 结果
*/
@GetMapping("/current")
public Mono<Result<IUser>> getCurrentUser() {
UserService userService = getService();
return ReactiveSecurityContextHolder.getContext()
.map(context -> (IUser) context.getAuthentication().getPrincipal())
.map(Result::ok);
}
}

View File

@ -0,0 +1,153 @@
package com.flyfish.framework.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.flyfish.framework.domain.base.IUser;
import com.flyfish.framework.domain.po.Department;
import com.flyfish.framework.domain.po.Role;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.enums.UserStatus;
import com.flyfish.framework.enums.UserType;
import com.flyfish.framework.utils.CopyUtils;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 客户端用户详情
*
* @author wangyu
*/
@Getter
@Setter
public class AdminUserDetails implements UserDetails, IUser {
private static final long serialVersionUID = -2441854985340378429L;
private static final List<UserType> adminTypes = Arrays.asList(UserType.ADMIN, UserType.SUPER_ADMIN);
/**
* 判断是否是管理员
* @param user 用户
* @return 结果
*/
public static boolean isAdmin(User user) {
return adminTypes.contains(user.getUserType());
}
/**
* 主键
*/
protected String id;
/**
* 编号
*/
protected String code;
/**
* 名称
*/
protected String name;
/**
* 用户类型
*/
private UserType userType;
/**
* 用户状态
*/
private UserStatus userStatus;
/**
* 冗余的电话号码
*/
private String phone;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 是否启用
*/
private Boolean enable;
/**
* 能否登录移动端
*/
private Boolean app;
/**
* 有效期
*/
@JsonFormat(pattern = "yyyy-MM-dd")
private Date validDate;
/**
* 可操作校区
*/
private List<Department> departments;
/**
* 所属角色
*/
private List<Role> roles;
/**
* 微信openId
*/
private String openId;
/**
* 查询冗余标记用户信息
*/
private Object detail;
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
@JsonIgnore
public boolean isAccountNonExpired() {
return adminTypes.contains(getUserType());
}
@Override
@JsonIgnore
public boolean isAccountNonLocked() {
return isAccountNonExpired();
}
@Override
@JsonIgnore
public boolean isCredentialsNonExpired() {
return isAccountNonExpired();
}
@JsonIgnore
@Override
public boolean isEnabled() {
return BooleanUtils.isTrue(getEnable());
}
public User toUser() {
return CopyUtils.copyProps(this, new User());
}
}

View File

@ -0,0 +1,27 @@
package com.flyfish.framework.domain;
import com.flyfish.framework.builder.CriteriaBuilder;
import com.flyfish.framework.domain.base.NameLikeQo;
import com.flyfish.framework.domain.po.Department;
import lombok.Getter;
import lombok.Setter;
/**
* 部门校区查询实体
*
* @author wybab
*/
@Getter
@Setter
public class DepartmentQo extends NameLikeQo<Department> {
private Boolean leaf;
private Integer level;
@Override
public CriteriaBuilder<Department> criteriaBuilder() {
return super.criteriaBuilder()
.with("level", "leaf");
}
}

View File

@ -0,0 +1,16 @@
package com.flyfish.framework.domain;
import lombok.Data;
/**
* 登录DTO
*
* @author wangyu
*/
@Data
public class LoginDto {
private String username;
private String password;
}

View File

@ -0,0 +1,25 @@
package com.flyfish.framework.domain;
import com.flyfish.framework.builder.CriteriaBuilder;
import com.flyfish.framework.domain.base.NameLikeQo;
import com.flyfish.framework.domain.po.Permission;
import lombok.Getter;
import lombok.Setter;
/**
* 权限查询实体
*
* @author wybab
*/
@Getter
@Setter
public class PermissionQo extends NameLikeQo<Permission> {
private Boolean admin;
@Override
public CriteriaBuilder<Permission> criteriaBuilder() {
return super.criteriaBuilder().with("admin");
}
}

View File

@ -0,0 +1,26 @@
package com.flyfish.framework.domain;
import com.flyfish.framework.builder.CriteriaBuilder;
import com.flyfish.framework.domain.base.NameLikeQo;
import com.flyfish.framework.domain.po.Role;
import lombok.Getter;
import lombok.Setter;
/**
* 角色查询实体
*
* @author wybab
*/
@Getter
@Setter
public class RoleQo extends NameLikeQo<Role> {
private Boolean admin;
private Boolean system;
@Override
public CriteriaBuilder<Role> criteriaBuilder() {
return super.criteriaBuilder().with("admin", "system");
}
}

View File

@ -0,0 +1,17 @@
package com.flyfish.framework.domain;
import lombok.Getter;
import lombok.Setter;
/**
* 修改密码dto
* @author wangyu
*/
@Getter
@Setter
public class UserPasswordDto {
private String oldPassword;
private String password;
}

View File

@ -0,0 +1,25 @@
package com.flyfish.framework.domain;
import com.flyfish.framework.builder.CriteriaBuilder;
import com.flyfish.framework.domain.base.NameLikeQo;
import com.flyfish.framework.domain.po.User;
import lombok.*;
@Getter
@Setter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UserQo extends NameLikeQo<User> {
private String userType;
private String username;
private String password;
@Override
public CriteriaBuilder<User> criteriaBuilder() {
return super.criteriaBuilder().with("userType", "username", "password");
}
}

View File

@ -0,0 +1,11 @@
package com.flyfish.framework.repository;
import com.flyfish.framework.domain.po.Department;
/**
* 部门仓库
*
* @author wybab
*/
public interface DepartmentRepository extends DefaultRepository<Department> {
}

View File

@ -0,0 +1,12 @@
package com.flyfish.framework.repository;
import com.flyfish.framework.domain.po.Permission;
/**
* 权限仓库
*
* @author wybab
*/
public interface PermissionRepository extends DefaultRepository<Permission> {
}

View File

@ -0,0 +1,14 @@
package com.flyfish.framework.repository;
import com.flyfish.framework.domain.po.User;
import reactor.core.publisher.Mono;
/**
* 异步用户仓库
*
* @author wybab
*/
public interface ReactiveUserRepository extends DefaultReactiveRepository<User> {
Mono<User> findByUsername(String username);
}

View File

@ -0,0 +1,11 @@
package com.flyfish.framework.repository;
import com.flyfish.framework.domain.po.Role;
/**
* 角色仓库
*
* @author wybab
*/
public interface RoleRepository extends DefaultRepository<Role> {
}

View File

@ -0,0 +1,30 @@
package com.flyfish.framework.repository;
import com.flyfish.framework.domain.po.User;
import java.util.Optional;
/**
* 用户repo
*
* @author wangyu
*/
public interface UserRepository extends DefaultRepository<User> {
/**
* 通过用户名密码查询用户
*
* @param username 用户名
* @param password 密码
* @return 结果
*/
Optional<User> findByUsernameAndPassword(String username, String password);
/**
* 通过用户名查询
*
* @param username 用户名
* @return 结果
*/
Optional<User> findByUsername(String username);
}

View File

@ -0,0 +1,10 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.po.Department;
import com.flyfish.framework.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class DepartmentService extends BaseServiceImpl<Department> {
}

View File

@ -0,0 +1,151 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.AdminUserDetails;
import com.flyfish.framework.domain.base.IUser;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.enums.UserStatus;
import com.flyfish.framework.utils.Assert;
import com.flyfish.framework.utils.CopyUtils;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.web.server.context.ServerSecurityContextRepository;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* 基于mongo实现的用户详情
*
* @author wangyu
*/
@RequiredArgsConstructor
@Service
public class MongoUserDetailsServiceImpl implements MongoUserDetailsService {
// 存储用户校验规则的map
private static final Map<Function<User, Boolean>, Supplier<AuthenticationException>> checkMap;
static {
checkMap = new HashMap<>();
// 初始化用户校验规则
checkMap.put(user -> null != user.getEnable() && !user.getEnable() ||
user.getUserStatus() == UserStatus.DISABLED, () -> new DisabledException("用户被禁用"));
checkMap.put(user -> user.getUserStatus() == UserStatus.LOCKED,
() -> new LockedException("账户已经锁定!请联系管理员修改密码!"));
}
private final UserService service;
private final ReactiveUserService userService;
@Resource
private ServerSecurityContextRepository contextRepository;
private ReactiveAuthenticationManager authenticationManager;
@PostConstruct
private void init() {
UserDetailsRepositoryReactiveAuthenticationManager manager =
new UserDetailsRepositoryReactiveAuthenticationManager(this);
manager.setPasswordEncoder(NoOpPasswordEncoder.getInstance());
authenticationManager = manager;
}
@Override
public Mono<UserDetails> updatePassword(UserDetails userDetails, String s) {
return userService.updateById(((IUser) userDetails).toUser()).map(this::mapToUserDetails);
}
@Override
public Mono<UserDetails> findByUsername(String s) {
return userService.findByUsername(s)
.flatMap(this::validate)
.map(this::mapToUserDetails)
.switchIfEmpty(Mono.defer(() -> Mono.error(new UsernameNotFoundException("用户不存在!"))));
}
private UserDetails mapToUserDetails(User user) {
AdminUserDetails userDetail = new AdminUserDetails();
CopyUtils.copyProps(user, userDetail);
return userDetail;
}
/**
* 校验
*
* @param user 用户
* @return 结果
*/
private Mono<User> validate(User user) {
return checkMap.entrySet().stream()
.filter(entry -> entry.getKey().apply(user))
.findFirst()
.map(entry -> Mono.<User>error(entry.getValue().get()))
.orElse(Mono.just(user));
}
/**
* 通过用户名密码直接认证
*
* @param user 用户
* @return 结果
*/
@Override
public Mono<Authentication> authenticate(UserDetails user, ServerWebExchange exchange) {
return loadContext(user)
.flatMap(securityContext -> contextRepository.save(exchange, securityContext)
.subscriberContext(ReactiveSecurityContextHolder.withSecurityContext(
Mono.just(securityContext)))
.then(Mono.just(securityContext)))
.map(SecurityContext::getAuthentication)
.doOnNext(authentication -> {
if (authentication.getPrincipal() instanceof IUser) {
IUser userDetail = (IUser) authentication.getPrincipal();
Assert.isTrue(BooleanUtils.isTrue(userDetail.getApp()), "用户没有移动端登录权限!");
}
});
}
@Override
public Mono<SecurityContext> loadContext(UserDetails user) {
// 构建账号信息
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
user.getUsername(), user.getPassword());
// 登录先
return authenticationManager.authenticate(token)
.map(authentication -> {
SecurityContextImpl securityContext = new SecurityContextImpl();
securityContext.setAuthentication(authentication);
return securityContext;
});
}
/**
* 退出登录
*
* @param exchange 数据请求
* @return 结果
*/
@Override
public Mono<Void> logout(ServerWebExchange exchange) {
return ReactiveSecurityContextHolder.getContext()
.switchIfEmpty(contextRepository.load(exchange))
.flatMap(context -> contextRepository.save(exchange, context));
}
}

View File

@ -0,0 +1,11 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.po.Permission;
import com.flyfish.framework.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class PermissionService extends BaseServiceImpl<Permission> {
}

View File

@ -0,0 +1,27 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.repository.ReactiveUserRepository;
import com.flyfish.framework.service.impl.BaseReactiveServiceImpl;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
/**
* 异步用户service
*
* @author wangyu
*/
@Service
public class ReactiveUserService extends BaseReactiveServiceImpl<User> {
/**
* 获取用户数据
*
* @param username 用户
* @return 结果
*/
public Mono<User> findByUsername(String username) {
return ((ReactiveUserRepository) repository).findByUsername(username);
}
}

View File

@ -0,0 +1,43 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.PermissionQo;
import com.flyfish.framework.domain.po.Permission;
import com.flyfish.framework.domain.po.Role;
import com.flyfish.framework.enums.RoleType;
import com.flyfish.framework.service.impl.BaseServiceImpl;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class RoleService extends BaseServiceImpl<Role> {
private final PermissionService permissionService;
/**
* 如果是管理员设置拥有所有权限
*
* @param entity 实体
* @return 结果
*/
@Override
public Role create(Role entity) {
if (entity.isSystem()) {
entity.setPermissions(permissionService.getAll());
} else if (BooleanUtils.isTrue(entity.getAdmin())) {
// 如果是管理员拥有所有权限
PermissionQo qo = new PermissionQo();
qo.setAdmin(false);
List<Permission> permissions = permissionService.getList(qo);
entity.setPermissions(permissions);
}
if (null == entity.getType()) {
entity.setType(RoleType.PC);
}
return super.create(entity);
}
}

View File

@ -0,0 +1,59 @@
package com.flyfish.framework.service;
import com.flyfish.framework.domain.UserQo;
import com.flyfish.framework.domain.base.Qo;
import com.flyfish.framework.domain.po.User;
import com.flyfish.framework.enums.UserStatus;
import com.flyfish.framework.enums.UserType;
import com.flyfish.framework.repository.UserRepository;
import com.flyfish.framework.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.List;
import java.util.Optional;
@Service
public class UserService extends BaseServiceImpl<User> {
@Resource
private DepartmentService departmentService;
/**
* 生成超级管理员
*/
@PostConstruct
private void init() {
UserQo qo = new UserQo();
qo.setUserType(UserType.SUPER_ADMIN.name());
if (count(qo) == 0) {
User user = new User();
user.setUsername("admin");
user.setPassword("admin");
user.setUserType(UserType.SUPER_ADMIN);
user.setEnable(true);
user.setApp(false);
user.setUserStatus(UserStatus.NORMAL);
user.setPhone("10000000000");
user.setName("超级管理员");
user.setCode("Administrator");
createSelective(user);
}
}
/**
* 获取用户数据
*
* @param username 用户
* @return 结果
*/
public Optional<User> findByUsername(String username) {
return ((UserRepository) repository).findByUsername(username);
}
@Override
public List<User> getList(Qo<User> query) {
return super.getList(query);
}
}

View File

@ -35,7 +35,7 @@ public class EnumController {
Set<Class<? extends NamedEnum>> classSet = reflections.getSubTypesOf(NamedEnum.class);
// 注入
classSet.stream().filter(clazz -> ClassUtils.isAssignable(clazz, Enum.class)).forEach(clazz -> {
String name = StringFormats.camel2Line(clazz.getSimpleName());
String name = StringFormats.camel2Line(ClassUtils.getShortClassName(clazz));
List<EnumValue> values = Arrays.stream(clazz.getEnumConstants()).reduce(new ArrayList<>(), (result, item) -> {
result.add(new EnumValue(((Enum<?>) item).name(), item.getName()));
return result;

View File

@ -40,6 +40,7 @@
<module>flyfish-common</module>
<module>flyfish-web</module>
<module>flyfish-user</module>
<module>flyfish-file</module>
</modules>
<repositories>