提交 075e438f 作者: 方治民

feat: 新增数据字典/分类相关实现、i18n 配置优化、文件上传现在支持对大视频文件进行切片处理,以及诸多细节优化

上级 4d47ee14
...@@ -42,6 +42,9 @@ dependencies { ...@@ -42,6 +42,9 @@ dependencies {
implementation project(":basic-auth") implementation project(":basic-auth")
implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}" implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
// Optional: Dict - 数据字典
implementation project(":basic-dict")
// Optional: WebSocket && STOMP 依赖 Auth + Redis 模块 // Optional: WebSocket && STOMP 依赖 Auth + Redis 模块
implementation project(":basic-websocket") implementation project(":basic-websocket")
...@@ -57,6 +60,8 @@ dependencies { ...@@ -57,6 +60,8 @@ dependencies {
implementation 'org.bytedeco:ffmpeg-platform:5.0-1.5.7' implementation 'org.bytedeco:ffmpeg-platform:5.0-1.5.7'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox // https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}" implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
implementation "net.bramp.ffmpeg:ffmpeg:${ffmpegWrapperVersion}"
// fastjson // fastjson
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}" implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
......
...@@ -3,10 +3,7 @@ package com.yiring.app.domain.user; ...@@ -3,10 +3,7 @@ package com.yiring.app.domain.user;
import com.yiring.auth.domain.user.User; import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity; import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.Entity; import jakarta.persistence.*;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.io.Serial; import java.io.Serial;
import java.io.Serializable; import java.io.Serializable;
import lombok.*; import lombok.*;
...@@ -31,7 +28,7 @@ import org.hibernate.annotations.Comment; ...@@ -31,7 +28,7 @@ import org.hibernate.annotations.Comment;
@FieldNameConstants @FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
@Entity @Entity
@Table(name = "SYS_USER_EXTENSION") @Table(name = "SYS_USER_EXTENSION", uniqueConstraints = @UniqueConstraint(columnNames = "user_id"))
@Comment("用户扩展表") @Comment("用户扩展表")
public class UserExtension extends BasicEntity implements Serializable { public class UserExtension extends BasicEntity implements Serializable {
...@@ -40,7 +37,7 @@ public class UserExtension extends BasicEntity implements Serializable { ...@@ -40,7 +37,7 @@ public class UserExtension extends BasicEntity implements Serializable {
@Comment("用户") @Comment("用户")
@OneToOne @OneToOne
@JoinColumn(nullable = false, unique = true) @JoinColumn(nullable = false, name = "user_id")
User user; User user;
@Comment("性别") @Comment("性别")
......
...@@ -4,22 +4,28 @@ package com.yiring.app.service.upload; ...@@ -4,22 +4,28 @@ package com.yiring.app.service.upload;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import com.yiring.common.core.Minio; import com.yiring.common.core.Minio;
import com.yiring.common.service.UploadProcessService; import com.yiring.common.service.UploadProcessService;
import com.yiring.common.util.Commons;
import io.minio.ObjectWriteResponse;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage; import java.awt.image.RenderedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import lombok.Cleanup; import lombok.Cleanup;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer; import org.apache.pdfbox.rendering.PDFRenderer;
import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.*;
import org.bytedeco.javacv.Frame; import org.springframework.context.annotation.Primary;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -29,13 +35,16 @@ import org.springframework.stereotype.Component; ...@@ -29,13 +35,16 @@ import org.springframework.stereotype.Component;
* 2022/9/23 16:44 * 2022/9/23 16:44
*/ */
@ConditionalOnClass({ PDDocument.class, FFmpegFrameGrabber.class }) @Slf4j
@Primary
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class UploadProcessServiceImpl implements UploadProcessService { public class UploadProcessServiceImpl implements UploadProcessService {
final Minio minio; final Minio minio;
Pattern pattern = Pattern.compile("^.*\\.ts$");
@Override @Override
public String handle(String object, InputStream is) { public String handle(String object, InputStream is) {
String suffix = FileUtil.getSuffix(object); String suffix = FileUtil.getSuffix(object);
...@@ -110,13 +119,82 @@ public class UploadProcessServiceImpl implements UploadProcessService { ...@@ -110,13 +119,82 @@ public class UploadProcessServiceImpl implements UploadProcessService {
ImageIO.write(frameToBufferedImage(frame), format, os); ImageIO.write(frameToBufferedImage(frame), format, os);
@Cleanup @Cleanup
InputStream io = new ByteArrayInputStream(os.toByteArray()); InputStream io = new ByteArrayInputStream(os.toByteArray());
int size = io.available();
minio.putObject(io, MediaType.IMAGE_JPEG_VALUE, filepath + "." + format); minio.putObject(io, MediaType.IMAGE_JPEG_VALUE, filepath + "." + format);
// 大视频文件切片上传(> 10M)
if (size > (10 * 10 * 1024)) {
filepath = fillSuffix(handleToM3u8(object, suffix, ff), "T" + (ff.getLengthInTime() / (1000 * 1000)));
}
} }
ff.stop(); ff.stop();
return filepath; return filepath;
} }
@SneakyThrows
public String handleToM3u8(String object, String suffix, FFmpegFrameGrabber ff) {
File tmpDir = FileUtil.getTmpDir();
String sourceName = FileUtil.getName(object);
String objectFolder = object.replace("/" + sourceName, "");
Path m3u8 = Paths.get(
tmpDir.getPath(),
Commons.uuid(),
sourceName.replaceAll("^(.*)\\." + suffix + "$", "$1.m3u8")
);
FileUtil.mkParentDirs(m3u8);
String out = m3u8.toFile().getPath();
long start = System.currentTimeMillis();
@Cleanup
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(
out,
ff.getImageWidth(),
ff.getImageHeight(),
ff.getAudioChannels()
);
recorder.setFormat("hls");
recorder.setOption("hls_wrap", "0");
recorder.setOption("hls_time", "5");
recorder.setOption("hls_list_size", "0");
recorder.setOption("hls_flags", "delete_segments");
recorder.setOption("hls_segment_type", "mpegts");
recorder.setOption("hls_segment_filename", out.replace(".m3u8", "-%d.ts"));
recorder.setOption("hls_delete_threshold", "1");
recorder.setOption("vsync", "2");
recorder.setOption("c:v", "copy");
recorder.setOption("c:a", "copy");
recorder.setOption("tune", "fastdecode");
recorder.setOption("threads", "8");
recorder.start();
Frame frame;
while ((frame = ff.grabImage()) != null) {
try {
recorder.record(frame);
} catch (FrameRecorder.Exception e) {
log.error(e.getMessage(), e);
}
}
recorder.setTimestamp(ff.getTimestamp());
recorder.flush();
long end = System.currentTimeMillis();
long times = end - start;
log.info("[Times] {}: {} ms", "video convert to m3u8", times);
// 解析 m3u8 文件
List<String> lines = FileUtil.readLines(m3u8.toString(), StandardCharsets.UTF_8);
// 获取 ts 切片文件
List<String> tss = lines.stream().filter(line -> pattern.matcher(line).matches()).toList();
// 上传 ts 切片文件
for (String ts : tss) {
Path path = Paths.get(m3u8.getParent().toString(), ts);
minio.putObject(path.toFile(), objectFolder);
}
// 上传 m3u8 索引文件
ObjectWriteResponse objectWriteResponse = minio.putObject(m3u8.toFile(), objectFolder);
return objectWriteResponse.object();
}
public static Frame getPictureFrame(FFmpegFrameGrabber grabber) throws FFmpegFrameGrabber.Exception { public static Frame getPictureFrame(FFmpegFrameGrabber grabber) throws FFmpegFrameGrabber.Exception {
int ftp = grabber.getLengthInFrames(); int ftp = grabber.getLengthInFrames();
int flag = 0; int flag = 0;
......
...@@ -15,12 +15,15 @@ import com.yiring.common.core.I18n; ...@@ -15,12 +15,15 @@ import com.yiring.common.core.I18n;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException; import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.PageParam; import com.yiring.common.param.PageParam;
import com.yiring.common.service.FileManageService;
import com.yiring.common.util.Commons; import com.yiring.common.util.Commons;
import com.yiring.common.util.FileUtils; import com.yiring.common.util.FileUtils;
import com.yiring.common.validation.group.Group; import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo; import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException; import java.io.IOException;
...@@ -36,6 +39,7 @@ import org.springframework.data.domain.Example; ...@@ -36,6 +39,7 @@ import org.springframework.data.domain.Example;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/** /**
* 示例接口 * 示例接口
...@@ -55,6 +59,7 @@ public class ExampleController { ...@@ -55,6 +59,7 @@ public class ExampleController {
final I18n i18n; final I18n i18n;
final Auths auths; final Auths auths;
final UserExtensionRepository userExtensionRepository; final UserExtensionRepository userExtensionRepository;
final FileManageService fileManageService;
@Operation(summary = "Hello World") @Operation(summary = "Hello World")
@GetMapping @GetMapping
...@@ -101,6 +106,23 @@ public class ExampleController { ...@@ -101,6 +106,23 @@ public class ExampleController {
FileUtils.download(response, resource.getFile()); FileUtils.download(response, resource.getFile());
} }
@Operation(summary = "文件上传")
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(
@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file,
@ParameterObject @Validated IdParam param
) {
log.info("upload params: {}", param);
try {
String link = fileManageService.upload(file);
return Result.ok(link);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception();
}
}
@SaCheckSafe @SaCheckSafe
@SaCheckLogin @SaCheckLogin
@Operation(summary = "查询用户属性") @Operation(summary = "查询用户属性")
......
# 环境变量 # 环境变量
env: env:
host: 192.168.0.156 # host: 192.168.0.156
host: 127.0.0.1
prod: false prod: false
extra: extra:
username: admin # username: admin
password: 123456 password: 123456
username: postgres
ffmpeg:
path: D:\Environments\FFmpeg\bin
# ---------------------------------------------- # ----------------------------------------------
spring: spring:
...@@ -17,10 +21,13 @@ spring: ...@@ -17,10 +21,13 @@ spring:
open-in-view: true open-in-view: true
hibernate: hibernate:
ddl-auto: update ddl-auto: update
show-sql: false show-sql: true
properties: properties:
hibernate: hibernate:
format_sql: true format_sql: true
# https://stackoverflow.com/questions/49283069/columnunique-true-produces-a-warn-o-h-engine-jdbc-spi-sqlexceptionhelper
schema_update:
unique_constraint_strategy: RECREATE_QUIETLY
# https://github.com/spring-projects/spring-data-jpa/issues/2717 # https://github.com/spring-projects/spring-data-jpa/issues/2717
# https://hibernate.atlassian.net/browse/HHH-15827 # https://hibernate.atlassian.net/browse/HHH-15827
jakarta: jakarta:
...@@ -40,7 +47,7 @@ spring: ...@@ -40,7 +47,7 @@ spring:
springdoc: springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded" default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json" default-produces-media-type: "application/json"
default-flat-param-object: true default-flat-param-object: false
override-with-generic-response: false override-with-generic-response: false
api-docs: api-docs:
resolve-schema-properties: true resolve-schema-properties: true
......
...@@ -13,11 +13,8 @@ spring: ...@@ -13,11 +13,8 @@ spring:
name: "basic-api-app" name: "basic-api-app"
servlet: servlet:
multipart: multipart:
max-file-size: 50MB max-file-size: 1024MB
max-request-size: 100MB max-request-size: 1048MB
messages:
basename: i18n/status,i18n/validation,i18n/messages
always-use-message-format: true
profiles: profiles:
include: auth, conf-patch include: auth, conf-patch
active: dev-postgresql active: dev-postgresql
......
...@@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; ...@@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
* 2023/1/12 14:06 * 2023/1/12 14:06
*/ */
@Slf4j @Slf4j
@Order(0) @Order(1)
@RestControllerAdvice @RestControllerAdvice
@RequiredArgsConstructor @RequiredArgsConstructor
public class AuthExceptionHandler { public class AuthExceptionHandler {
......
...@@ -14,6 +14,7 @@ import java.util.Set; ...@@ -14,6 +14,7 @@ import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/** /**
* 获取登录用户权限信息实现 * 获取登录用户权限信息实现
...@@ -23,6 +24,7 @@ import org.springframework.stereotype.Component; ...@@ -23,6 +24,7 @@ import org.springframework.stereotype.Component;
* 2022/3/25 9:37 * 2022/3/25 9:37
*/ */
@Transactional(readOnly = true)
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface { public class StpInterfaceImpl implements StpInterface {
...@@ -54,7 +56,7 @@ public class StpInterfaceImpl implements StpInterface { ...@@ -54,7 +56,7 @@ public class StpInterfaceImpl implements StpInterface {
* @param loginId 登录 ID * @param loginId 登录 ID
* @return 用户信息 * @return 用户信息
*/ */
private User getUser(Object loginId) { public User getUser(Object loginId) {
String id = Objects.toString(loginId); String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id); Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) { if (optional.isEmpty()) {
......
...@@ -41,10 +41,10 @@ import org.hibernate.annotations.Where; ...@@ -41,10 +41,10 @@ import org.hibernate.annotations.Where;
@Table( @Table(
name = TABLE_NAME, name = TABLE_NAME,
indexes = { indexes = {
@Index(columnList = "deleteTime"), @Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = "type"), @Index(columnList = Permission.Fields.type),
@Index(columnList = "pid"), @Index(columnList = Permission.Fields.pid),
@Index(columnList = "tree"), @Index(columnList = Permission.Fields.tree),
} }
) )
@Comment("系统权限") @Comment("系统权限")
......
...@@ -43,7 +43,8 @@ import org.hibernate.annotations.Where; ...@@ -43,7 +43,8 @@ import org.hibernate.annotations.Where;
@Entity @Entity
@Table( @Table(
name = TABLE_NAME, name = TABLE_NAME,
indexes = { @Index(columnList = "deleteTime"), @Index(columnList = "uid,deleteTime", unique = true) } indexes = @Index(columnList = BasicEntity.Fields.deleteTime),
uniqueConstraints = { @UniqueConstraint(columnNames = { Role.Fields.uid, BasicEntity.Fields.deleteTime }) }
) )
@Comment("系统角色") @Comment("系统角色")
public class Role extends BasicEntity implements Serializable { public class Role extends BasicEntity implements Serializable {
......
...@@ -40,12 +40,11 @@ import org.hibernate.annotations.Where; ...@@ -40,12 +40,11 @@ import org.hibernate.annotations.Where;
@Entity @Entity
@Table( @Table(
name = User.TABLE_NAME, name = User.TABLE_NAME,
indexes = { indexes = { @Index(columnList = User.Fields.enabled), @Index(columnList = BasicEntity.Fields.deleteTime) },
@Index(columnList = "enabled"), uniqueConstraints = {
@Index(columnList = "deleteTime"), @UniqueConstraint(columnNames = { User.Fields.username, BasicEntity.Fields.deleteTime }),
@Index(columnList = "username,deleteTime", unique = true), @UniqueConstraint(columnNames = { User.Fields.mobile, BasicEntity.Fields.deleteTime }),
@Index(columnList = "mobile,deleteTime", unique = true), @UniqueConstraint(columnNames = { User.Fields.email, BasicEntity.Fields.deleteTime }),
@Index(columnList = "email,deleteTime", unique = true),
} }
) )
@Comment("系统用户") @Comment("系统用户")
......
...@@ -51,9 +51,6 @@ public class RegisterParam implements Serializable { ...@@ -51,9 +51,6 @@ public class RegisterParam implements Serializable {
@Parameter(description = "邮箱", example = "developer@yiring.com") @Parameter(description = "邮箱", example = "developer@yiring.com")
String email; String email;
@Parameter(description = "简介", example = "平台管理员")
String introduction;
@Parameter(description = "是否启用", example = "true") @Parameter(description = "是否启用", example = "true")
Boolean enable; Boolean enable;
} }
/* (C) 2022 YiRing, Inc. */ /* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.permission; package com.yiring.auth.param.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.common.validation.group.Group; import com.yiring.common.validation.group.Group;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
...@@ -34,47 +34,47 @@ public class PermissionParam implements Serializable { ...@@ -34,47 +34,47 @@ public class PermissionParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = -6781934969837655538L; private static final long serialVersionUID = -6781934969837655538L;
@Parameter(description = "id", example = "1") @Schema(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class }) @NotBlank(groups = { Group.Edit.class })
String id; String id;
@Parameter(description = "权限类型", example = "MENU") @Schema(description = "权限类型", example = "MENU")
@NotNull(message = "权限类型不能为空") @NotNull(message = "权限类型不能为空")
Permission.Type type; Permission.Type type;
@Parameter(description = "序号", example = "1") @Schema(description = "序号", example = "1")
Integer serial; Integer serial;
@Parameter(description = "标识", example = "Dashboard") @Schema(description = "标识", example = "Dashboard")
@NotEmpty(message = "权限标识不能为空") @NotEmpty(message = "权限标识不能为空")
String uid; String uid;
@Parameter(description = "名称", example = "Dashboard") @Schema(description = "名称", example = "Dashboard")
@NotEmpty(message = "权限名称不能为空") @NotEmpty(message = "权限名称不能为空")
String name; String name;
@Parameter(description = "路径", example = "/dashboard") @Schema(description = "路径", example = "/dashboard")
String path; String path;
@Parameter(description = "重定向", example = "/dashboard/workbench") @Schema(description = "重定向", example = "/dashboard/workbench")
String redirect; String redirect;
@Parameter(description = "组件", example = "LAYOUT") @Schema(description = "组件", example = "LAYOUT")
String component; String component;
@Parameter(description = "图标", example = "ion:grid-outline") @Schema(description = "图标", example = "ion:grid-outline")
String icon; String icon;
@Parameter(description = "是否隐藏", example = "false") @Schema(description = "是否隐藏", example = "false")
Boolean hidden; Boolean hidden;
@Parameter(description = "是否启用", example = "true") @Schema(description = "是否启用", example = "true")
Boolean enable; Boolean enable;
@Parameter(description = "父级ID", example = "0") @Schema(description = "父级ID", example = "0")
@Builder.Default @Builder.Default
String pid = "0"; String pid = "0";
@Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}") @Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
String meta; JSONObject meta;
} }
...@@ -86,7 +86,6 @@ public class AuthController { ...@@ -86,7 +86,6 @@ public class AuthController {
.username(param.getUsername()) .username(param.getUsername())
.password(SaSecureUtil.sha256(param.getPassword())) .password(SaSecureUtil.sha256(param.getPassword()))
.enabled(param.getEnable()) .enabled(param.getEnable())
.createTime(LocalDateTime.now())
.build(); .build();
userRepository.saveAndFlush(user); userRepository.saveAndFlush(user);
return Result.ok(); return Result.ok();
...@@ -156,7 +155,7 @@ public class AuthController { ...@@ -156,7 +155,7 @@ public class AuthController {
public Result<String> safe(@ParameterObject @Validated SafeParam param) { public Result<String> safe(@ParameterObject @Validated SafeParam param) {
User user = auths.getLoginUser(); User user = auths.getLoginUser();
if (SaSecureUtil.sha256(param.getPassword()).equals(user.getPassword())) { if (SaSecureUtil.sha256(param.getPassword()).equals(user.getPassword())) {
StpUtil.openSafe(120); StpUtil.openSafe(360);
return Result.ok(); return Result.ok();
} }
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
package com.yiring.auth.web.sys.permission; package com.yiring.auth.web.sys.permission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONValidator;
import com.yiring.auth.domain.permission.Permission; import com.yiring.auth.domain.permission.Permission;
import com.yiring.auth.domain.permission.PermissionRepository; import com.yiring.auth.domain.permission.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam; import com.yiring.auth.param.permission.PermissionParam;
...@@ -31,10 +29,7 @@ import org.springframework.beans.BeanUtils; ...@@ -31,10 +29,7 @@ import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example; import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** /**
* 系统权限管理控制器 * 系统权限管理控制器
...@@ -60,7 +55,7 @@ public class PermissionController { ...@@ -60,7 +55,7 @@ public class PermissionController {
@Operation(summary = "新增") @Operation(summary = "新增")
@PostMapping("add") @PostMapping("add")
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) PermissionParam param) { public Result<String> add(@RequestBody @Validated({ Group.Add.class }) PermissionParam param) {
if (has(param.getUid())) { if (has(param.getUid())) {
throw BusinessException.i18n("Code.1001"); throw BusinessException.i18n("Code.1001");
} }
...@@ -72,7 +67,7 @@ public class PermissionController { ...@@ -72,7 +67,7 @@ public class PermissionController {
@Operation(summary = "修改") @Operation(summary = "修改")
@PostMapping("modify") @PostMapping("modify")
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) PermissionParam param) { public Result<String> modify(@RequestBody @Validated({ Group.Edit.class }) PermissionParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception(); throw Status.NOT_FOUND.exception();
...@@ -91,8 +86,8 @@ public class PermissionController { ...@@ -91,8 +86,8 @@ public class PermissionController {
} }
@Operation(summary = "删除") @Operation(summary = "删除")
@PostMapping("deleted") @PostMapping("remove")
public Result<String> deleted(@ParameterObject @Validated IdParam param) { public Result<String> remove(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId()); Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) { if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception(); throw Status.NOT_FOUND.exception();
...@@ -173,9 +168,6 @@ public class PermissionController { ...@@ -173,9 +168,6 @@ public class PermissionController {
private void save(Permission entity, PermissionParam param) { private void save(Permission entity, PermissionParam param) {
BeanUtils.copyProperties(param, entity, Permission.Fields.meta); BeanUtils.copyProperties(param, entity, Permission.Fields.meta);
entity.setTree(getTreeNode(param.getPid())); entity.setTree(getTreeNode(param.getPid()));
if (JSONValidator.from(param.getMeta()).validate()) {
entity.setMeta(JSON.parseObject(param.getMeta()));
}
permissionRepository.saveAndFlush(entity); permissionRepository.saveAndFlush(entity);
} }
} }
...@@ -113,8 +113,8 @@ public class RoleController { ...@@ -113,8 +113,8 @@ public class RoleController {
} }
@Operation(summary = "删除") @Operation(summary = "删除")
@PostMapping("deleted") @PostMapping("remove")
public Result<String> deleted(@ParameterObject @Validated IdsParam param) { public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<Role> roles = roleRepository.findAllById(param.toIds()); List<Role> roles = roleRepository.findAllById(param.toIds());
roleRepository.deleteAll(roles); roleRepository.deleteAll(roles);
return Result.ok(); return Result.ok();
......
...@@ -30,7 +30,7 @@ public @interface DownloadResponse { ...@@ -30,7 +30,7 @@ public @interface DownloadResponse {
@AliasFor(annotation = ApiResponse.class) @AliasFor(annotation = ApiResponse.class)
Content content() default @Content( Content content() default @Content(
schema = @Schema(type = "string", format = "binary"), schema = @Schema(type = "file", format = "binary"),
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE
); );
} }
...@@ -37,11 +37,34 @@ public class EnvConfig implements Serializable { ...@@ -37,11 +37,34 @@ public class EnvConfig implements Serializable {
boolean prod; boolean prod;
/** /**
* FFmpeg 配置
*/
FFmpeg ffmpeg;
/**
* 扩展配置 * 扩展配置
*/ */
Extra extra; Extra extra;
/** /**
* FFmpeg 路径相关配置
*/
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
@Configuration("env.config.ffmpeg")
@ConfigurationProperties(prefix = "env.ffmpeg")
public static class FFmpeg implements Serializable {
@Serial
private static final long serialVersionUID = 972365906869473179L;
/**
* ffmpeg 路径
*/
String path;
}
/**
* 扩展环境配置变量 * 扩展环境配置变量
*/ */
@Data @Data
......
...@@ -192,7 +192,7 @@ public class Result<T> implements Serializable { ...@@ -192,7 +192,7 @@ public class Result<T> implements Serializable {
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details, @PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details,
Throwable error Throwable error
) { ) {
if (Objects.isNull(code)) { if (Objects.isNull(code) && Objects.nonNull(details)) {
String prefix = "Code."; String prefix = "Code.";
if (details.startsWith(prefix)) { if (details.startsWith(prefix)) {
String codeText = details.replace(prefix, ""); String codeText = details.replace(prefix, "");
......
/* (C) 2021 YiRing, Inc. */ /* (C) 2021 YiRing, Inc. */
package com.yiring.common.param; package com.yiring.common.param;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
...@@ -37,13 +36,11 @@ public class PageParam implements Serializable { ...@@ -37,13 +36,11 @@ public class PageParam implements Serializable {
@Serial @Serial
private static final long serialVersionUID = 6103761701912769946L; private static final long serialVersionUID = 6103761701912769946L;
@Parameter
@Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer") @Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer")
@NotNull @NotNull
@Range(min = 1, max = 100) @Range(min = 1, max = 100)
Integer pageSize; Integer pageSize;
@Parameter
@Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer") @Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer")
@NotNull @NotNull
@Min(1) @Min(1)
...@@ -84,6 +81,6 @@ public class PageParam implements Serializable { ...@@ -84,6 +81,6 @@ public class PageParam implements Serializable {
if (Objects.nonNull(sortField)) { if (Objects.nonNull(sortField)) {
sort = Sort.by(new Sort.Order(sortOrder, sortField)); sort = Sort.by(new Sort.Order(sortOrder, sortField));
} }
return PageRequest.of(pageSize - 1, pageNo, sort); return PageRequest.of(pageNo - 1, pageSize, sort);
} }
} }
...@@ -24,6 +24,11 @@ public interface Group { ...@@ -24,6 +24,11 @@ public interface Group {
interface Edit extends Default {} interface Edit extends Default {}
/** /**
* 数据查询分组
*/
interface Query extends Default {}
/**
* 通用的必填分组 * 通用的必填分组
*/ */
interface Required extends Default {} interface Required extends Default {}
......
...@@ -37,4 +37,10 @@ public class KeyValueVo implements Serializable { ...@@ -37,4 +37,10 @@ public class KeyValueVo implements Serializable {
*/ */
@Schema(description = "label", example = "label") @Schema(description = "label", example = "label")
String label; String label;
/**
* 扩展字段,可用于显示禁用状态
*/
@Schema(description = "是否启用", example = "true")
String enable;
} }
...@@ -5,23 +5,27 @@ import cn.hutool.core.convert.Convert; ...@@ -5,23 +5,27 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Console; import cn.hutool.core.lang.Console;
import cn.hutool.core.net.NetUtil; import cn.hutool.core.net.NetUtil;
import com.yiring.common.core.I18n; import com.yiring.common.core.I18n;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.models.ExternalDocumentation; import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.tags.Tag; import io.swagger.v3.oas.models.tags.Tag;
import java.lang.annotation.Annotation;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.models.GroupedOpenApi; import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
/** /**
* Swagger Config * Swagger Config
...@@ -81,7 +85,12 @@ public class SwaggerConfig implements CommandLineRunner { ...@@ -81,7 +85,12 @@ public class SwaggerConfig implements CommandLineRunner {
@Bean(name = "api.manage") @Bean(name = "api.manage")
public GroupedOpenApi manage() { public GroupedOpenApi manage() {
return api("③ 系统管理", List.of("com.yiring.auth.web.sys"), List.of("/**"), Collections.emptyList()); return api(
"③ 系统管理",
List.of("com.yiring.auth.web.sys", "com.yiring.dict.web"),
List.of("/**"),
Collections.emptyList()
);
} }
@Bean(name = "api.example") @Bean(name = "api.example")
...@@ -115,24 +124,33 @@ public class SwaggerConfig implements CommandLineRunner { ...@@ -115,24 +124,33 @@ public class SwaggerConfig implements CommandLineRunner {
}; };
} }
// @Bean /**
// public OperationCustomizer addGlobalStatusResponse() { * 自定义 OperationId 用于优化 Pont 接口文件生成
// return (operation, handlerMethod) -> { */
// ApiResponses responses = operation.getResponses(); @Bean
// for (Status status : Status.values()) { public OperationCustomizer operationIdCustomizer() {
// if (Status.OK.equals(status)) { return (operation, handlerMethod) -> {
// continue; Class<?> superClazz = handlerMethod.getBeanType().getSuperclass();
// } if (Objects.nonNull(superClazz)) {
// Annotation[] annotations = handlerMethod.getMethod().getAnnotations();
// ApiResponse response = new ApiResponse() String methodType = "GET";
// .description(i18n.get(status.getReasonPhrase())) for (Annotation annotation : annotations) {
// .$ref("Response"); if (annotation instanceof PostMapping) {
// responses.addApiResponse(String.valueOf(status.value()), response); methodType = "POST";
// } } else if (annotation instanceof PutMapping) {
// methodType = "PUT";
// return operation; } else if (annotation instanceof DeleteMapping) {
// }; methodType = "DELETE";
// } }
}
String beanName = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
operation.setOperationId(String.format("%sUsing%s_%s", methodName, methodType, beanName));
}
return operation;
};
}
private GroupedOpenApi api( private GroupedOpenApi api(
String group, String group,
...@@ -144,9 +162,10 @@ public class SwaggerConfig implements CommandLineRunner { ...@@ -144,9 +162,10 @@ public class SwaggerConfig implements CommandLineRunner {
.builder() .builder()
.group(group) .group(group)
.packagesToScan(basePackages.toArray(new String[0])) .packagesToScan(basePackages.toArray(new String[0]))
.addOpenApiMethodFilter(method -> method.isAnnotationPresent(Operation.class)) .addOpenApiMethodFilter(method -> method.isAnnotationPresent(io.swagger.v3.oas.annotations.Operation.class))
.pathsToMatch(pathsToMatch.toArray(new String[0])) .pathsToMatch(pathsToMatch.toArray(new String[0]))
.pathsToExclude(pathsToExclude.toArray(new String[0])) .pathsToExclude(pathsToExclude.toArray(new String[0]))
.addOperationCustomizer(operationIdCustomizer())
.build(); .build();
} }
......
...@@ -3,9 +3,11 @@ package com.yiring.common.config; ...@@ -3,9 +3,11 @@ package com.yiring.common.config;
import java.util.Locale; import java.util.Locale;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource; import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
...@@ -18,15 +20,34 @@ import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; ...@@ -18,15 +20,34 @@ import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
* 2022/8/17 10:33 * 2022/8/17 10:33
*/ */
@Order
@Configuration @Configuration
@RequiredArgsConstructor @RequiredArgsConstructor
public class I18nConfig { public class I18nConfig {
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
/**
* 解决多模块 i18n/messages 重复文件的读取问题
* <a href="https://stackoverflow.com/questions/17661252/spring-multi-module-i18n-with-modules-extending-the-messagesource-contents?noredirect=1&lq=1">...</a>
*
* @return MessageSource
*/
@Bean
public MessageSource messageSource() {
SmReloadableResourceBundleMessageSource messageSource = new SmReloadableResourceBundleMessageSource();
messageSource.setBasenames("classpath*:i18n/messages", "classpath:i18n/status", "classpath:i18n/validation");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setAlwaysUseMessageFormat(true);
messageSource.setDefaultLocale(DEFAULT_LOCALE);
return messageSource;
}
@Bean @Bean
public LocaleResolver localeResolver() { public LocaleResolver localeResolver() {
// header accept-language // header accept-language
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver(); AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); resolver.setDefaultLocale(DEFAULT_LOCALE);
return resolver; return resolver;
} }
...@@ -34,9 +55,8 @@ public class I18nConfig { ...@@ -34,9 +55,8 @@ public class I18nConfig {
public LocalValidatorFactoryBean getValidator() { public LocalValidatorFactoryBean getValidator() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8"); messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE); messageSource.setDefaultLocale(DEFAULT_LOCALE);
messageSource.setBasenames("classpath:/ValidationMessages", "classpath:i18n/validation"); messageSource.setBasenames("classpath:/ValidationMessages", "classpath:i18n/validation");
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean(); LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource); bean.setValidationMessageSource(messageSource);
return bean; return bean;
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.config;
import java.io.IOException;
import java.util.Properties;
import lombok.NonNull;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/**
* @author Jim
* @version 0.1
* 2023/1/20 18:00
*/
public class SmReloadableResourceBundleMessageSource extends ReloadableResourceBundleMessageSource {
private static final String PROPERTIES_SUFFIX = ".properties";
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
@Override
protected @NonNull PropertiesHolder refreshProperties(String filename, PropertiesHolder propHolder) {
if (filename.startsWith(PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX)) {
return refreshClassPathProperties(filename, propHolder);
} else {
return super.refreshProperties(filename, propHolder);
}
}
private PropertiesHolder refreshClassPathProperties(String filename, PropertiesHolder propHolder) {
Properties properties = new Properties();
long lastModified = -1;
try {
Resource[] resources = resolver.getResources(filename + PROPERTIES_SUFFIX);
for (Resource resource : resources) {
String sourcePath = resource.getURI().toString().replace(PROPERTIES_SUFFIX, "");
PropertiesHolder holder = super.refreshProperties(sourcePath, propHolder);
properties.putAll(holder.getProperties());
if (lastModified < resource.lastModified()) lastModified = resource.lastModified();
}
} catch (IOException ignored) {}
return new PropertiesHolder(properties, lastModified);
}
}
...@@ -7,7 +7,6 @@ import org.jetbrains.annotations.PropertyKey; ...@@ -7,7 +7,6 @@ import org.jetbrains.annotations.PropertyKey;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable; import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
...@@ -20,7 +19,7 @@ import org.springframework.stereotype.Component; ...@@ -20,7 +19,7 @@ import org.springframework.stereotype.Component;
*/ */
@Slf4j @Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE) @Order
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class I18n { public class I18n {
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理服务
*
* @author Jim
* @version 0.1
* 2023/1/29 16:18
*/
public interface FileManageService {
/**
* 文件上传
*
* @param file 文件
* @return 文件地址
*/
String upload(MultipartFile file) throws Exception;
}
...@@ -17,11 +17,11 @@ public interface UploadProcessService { ...@@ -17,11 +17,11 @@ public interface UploadProcessService {
* PDF:haha.pdf -> haha.P12.pdf 记录 PDF 文件总页数,P12 即代表 PDF 总共 12 页,同时可通过 haha.P12.pdf.1.jpg... 按序号读取到每一页 PDF 对应的图片 * PDF:haha.pdf -> haha.P12.pdf 记录 PDF 文件总页数,P12 即代表 PDF 总共 12 页,同时可通过 haha.P12.pdf.1.jpg... 按序号读取到每一页 PDF 对应的图片
* 音视频:haha.mp3/4 -> haha.T12.mp3/4 记录音视频的时长(秒),T12 即代表音视频时长为 12 秒 * 音视频:haha.mp3/4 -> haha.T12.mp3/4 记录音视频的时长(秒),T12 即代表音视频时长为 12 秒
* 视频封面:haha.mp4 -> haha.mp4.jpg 截取视频封面 * 视频封面:haha.mp4 -> haha.mp4.jpg 截取视频封面
*
* @param object 上传文件存储地址 * @param object 上传文件存储地址
* @param is 文件流 * @param is 文件流
* @return 预处理后的文件地址(可能对文件名追加了时长、页数、分辨率等标识) * @return 预处理后的文件地址(可能对文件名追加了时长、页数、分辨率等标识)
*/ */
@SuppressWarnings("unused")
default String handle(String object, InputStream is) { default String handle(String object, InputStream is) {
return object; return object;
} }
......
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service.impl;
import com.yiring.common.service.UploadProcessService;
import org.springframework.stereotype.Service;
/**
* @author Jim
* @version 0.1
* 2023/2/13 11:14
*/
@Service
public class DefaultUploadProcessServiceImpl implements UploadProcessService {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.common.service.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.core.Minio;
import com.yiring.common.service.FileManageService;
import com.yiring.common.service.UploadProcessService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
/**
* 文件管理服务实现
*
* @author Jim
* @version 0.1
* 2023/1/29 16:22
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class FileManageServiceImpl implements FileManageService {
final Minio minio;
final UploadProcessService service;
@Override
public String upload(MultipartFile file) throws Exception {
// 获取文件名信息
String filename = file.getOriginalFilename();
if (StrUtil.isBlank(filename)) {
throw new RuntimeException("上传文件名不能为空");
}
// 获取文件信息以及默认存储地址
String uuid = IdUtil.fastSimpleUUID();
String object = minio.buildUploadPath(filename, "", uuid);
// 预处理(默认不做任何处理,具体逻辑需自行在外部实现)
object = service.handle(object, file.getInputStream());
// 上传原文件
minio.putObject(file.getInputStream(), file.getContentType(), object);
return minio.getDefaultURI(object);
}
}
...@@ -8,7 +8,7 @@ import com.yiring.common.core.Minio; ...@@ -8,7 +8,7 @@ import com.yiring.common.core.Minio;
import com.yiring.common.core.Result; import com.yiring.common.core.Result;
import com.yiring.common.core.Status; import com.yiring.common.core.Status;
import com.yiring.common.param.DownloadParam; import com.yiring.common.param.DownloadParam;
import com.yiring.common.service.UploadProcessService; import com.yiring.common.service.FileManageService;
import com.yiring.common.util.FileUtils; import com.yiring.common.util.FileUtils;
import com.yiring.common.vo.ImageInfo; import com.yiring.common.vo.ImageInfo;
import io.minio.GetObjectResponse; import io.minio.GetObjectResponse;
...@@ -49,7 +49,7 @@ import org.springframework.web.multipart.MultipartFile; ...@@ -49,7 +49,7 @@ import org.springframework.web.multipart.MultipartFile;
public class MinioController { public class MinioController {
final Minio minio; final Minio minio;
final UploadProcessService service; final FileManageService fileManageService;
/** /**
* minio 上传文件,成功返回文件 url * minio 上传文件,成功返回文件 url
...@@ -58,22 +58,8 @@ public class MinioController { ...@@ -58,22 +58,8 @@ public class MinioController {
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) @PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) { public Result<String> upload(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) {
try { try {
// 获取文件名信息 String link = fileManageService.upload(file);
String filename = file.getOriginalFilename(); return Result.ok(link);
if (filename == null) {
throw Status.BAD_REQUEST.exception();
}
// 获取文件信息以及默认存储地址
String uuid = IdUtil.fastSimpleUUID();
String object = minio.buildUploadPath(filename, "", uuid);
// 预处理(默认不做任何处理,具体逻辑需自行在外部实现)
object = service.handle(object, file.getInputStream());
// 上传原文件
minio.putObject(file.getInputStream(), file.getContentType(), object);
return Result.ok(minio.getDefaultURI(object));
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception(); throw Status.BAD_REQUEST.exception();
......
...@@ -20,4 +20,9 @@ public @interface Times { ...@@ -20,4 +20,9 @@ public @interface Times {
* 描述 * 描述
*/ */
String value() default ""; String value() default "";
/**
* 阈值(毫秒),当计算耗时超过阈值时,打印警告日志
*/
long threshold() default 0;
} }
...@@ -10,6 +10,7 @@ import org.aspectj.lang.ProceedingJoinPoint; ...@@ -10,6 +10,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
...@@ -30,12 +31,22 @@ public class TimesAspect { ...@@ -30,12 +31,22 @@ public class TimesAspect {
@Around("times()") @Around("times()")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
try { Object result = point.proceed();
return point.proceed();
} finally {
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
log.info("[Times] {}: {} ms", getTimesValue(point), end - start); long times = end - start;
// 计算是否超过阈值,打印日志
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Times annotation = method.getAnnotation(Times.class);
String name = StrUtil.isEmpty(annotation.value()) ? method.getName() : annotation.value();
if (annotation.threshold() > 0 && times > annotation.threshold()) {
log.warn("[Times] {}: {} ms", name, times);
} else {
log.info("[Times] {}: {} ms", name, times);
} }
return result;
} }
public String getTimesValue(JoinPoint joinPoint) { public String getTimesValue(JoinPoint joinPoint) {
......
...@@ -3,14 +3,10 @@ package com.yiring.common.util; ...@@ -3,14 +3,10 @@ package com.yiring.common.util;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import com.yiring.common.vo.ImageInfo; import com.yiring.common.vo.ImageInfo;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
...@@ -18,8 +14,8 @@ import java.nio.file.attribute.BasicFileAttributes; ...@@ -18,8 +14,8 @@ import java.nio.file.attribute.BasicFileAttributes;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.util.FileCopyUtils;
/** /**
* 文件工具类 * 文件工具类
...@@ -37,9 +33,9 @@ public class FileUtils { ...@@ -37,9 +33,9 @@ public class FileUtils {
* *
* @param response HttpServletResponse * @param response HttpServletResponse
* @param file File * @param file File
* @throws IOException IOException
*/ */
public void download(HttpServletResponse response, File file) throws IOException { @SneakyThrows
public void download(HttpServletResponse response, File file) {
String filename = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8); String filename = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8);
BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class); BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
long lastModified = basicFileAttributes.lastModifiedTime().toMillis(); long lastModified = basicFileAttributes.lastModifiedTime().toMillis();
...@@ -47,7 +43,13 @@ public class FileUtils { ...@@ -47,7 +43,13 @@ public class FileUtils {
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(basicFileAttributes.size())); response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(basicFileAttributes.size()));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setContentType(FileUtil.getMimeType(file.toPath())); response.setContentType(FileUtil.getMimeType(file.toPath()));
FileReader.create(file).writeToStream(response.getOutputStream(), true); try (
FileInputStream object = new FileInputStream(file);
BufferedInputStream bis = new BufferedInputStream(object);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())
) {
FileCopyUtils.copy(bis, bos);
}
} }
/** /**
...@@ -59,8 +61,8 @@ public class FileUtils { ...@@ -59,8 +61,8 @@ public class FileUtils {
* @param filename 文件名称(带后缀) * @param filename 文件名称(带后缀)
* @param contentType 文档类型 * @param contentType 文档类型
* @param lastModified 最后修改时间 * @param lastModified 最后修改时间
* @throws IOException IOException
*/ */
@SneakyThrows
public void download( public void download(
HttpServletResponse response, HttpServletResponse response,
InputStream object, InputStream object,
...@@ -68,15 +70,19 @@ public class FileUtils { ...@@ -68,15 +70,19 @@ public class FileUtils {
String filename, String filename,
String contentType, String contentType,
long lastModified long lastModified
) throws IOException { ) {
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8); filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.LAST_MODIFIED, String.valueOf(lastModified)); response.setHeader(HttpHeaders.LAST_MODIFIED, String.valueOf(lastModified));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(length)); response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(length));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename); response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setContentType(contentType); response.setContentType(contentType);
IOUtils.copy(object, response.getOutputStream()); try (
object.close(); object;
response.flushBuffer(); BufferedInputStream bis = new BufferedInputStream(object);
BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())
) {
FileCopyUtils.copy(bis, bos);
}
} }
/** /**
......
dependencies {
implementation project(':basic-common:core')
implementation project(':basic-common:util')
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
// 本地依赖
implementation fileTree(dir: project.rootDir.getPath() + '\\libs', includes: ['*jar'])
// swagger(knife4j)
implementation "com.github.xiaoymin:knife4j-openapi3-jakarta-spring-boot-starter:${knife4jOpen3Version}"
// hutool-core
implementation "cn.hutool:hutool-core:${hutoolVersion}"
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import static com.yiring.dict.domain.Category.DELETE_SQL;
import static com.yiring.dict.domain.Category.TABLE_NAME;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
* 分类字典
*
* @author Jim
* @version 0.1
* 2023/1/20 13:58
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@Table(
name = TABLE_NAME,
indexes = {
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = Category.Fields.name),
@Index(columnList = Category.Fields.code),
}
)
@Comment("分类字典")
public class Category extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_CATEGORY";
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
public static final String DEFAULT_TOP_PID = "0";
@Serial
private static final long serialVersionUID = -3537807812313662319L;
@Comment("分类名称")
@Column(nullable = false)
String name;
@Comment("分类编号")
@Column(nullable = false, unique = true)
String code;
@Comment("分类描述")
String description;
@Comment("分类父级 ID")
String pid;
@Comment("分类是否启用")
@Column(nullable = false)
Boolean enable;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2023/1/20 14:10
*/
@Repository
public interface CategoryRepository extends JpaRepository<Category, Serializable>, JpaSpecificationExecutor<Category> {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import static com.yiring.dict.domain.Dict.DELETE_SQL;
import static com.yiring.dict.domain.Dict.TABLE_NAME;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
* 字典
*
* @author Jim
* @version 0.1
* 2023/1/20 13:58
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@Table(
name = TABLE_NAME,
indexes = {
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = Dict.Fields.name),
@Index(columnList = Dict.Fields.code),
}
)
@Comment("数据字典")
public class Dict extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_DICT";
public static final String DELETE_SQL = "update " + TABLE_NAME + BasicEntity.Where.DELETE_SET;
@Serial
private static final long serialVersionUID = -6780729527484051504L;
@Comment("字典名称")
@Column(nullable = false)
String name;
@Comment("字典编号")
@Column(nullable = false, unique = true)
String code;
@Comment("字典描述")
String description;
@JsonIgnore
@Builder.Default
@Comment("字典选项集合")
@OneToMany(cascade = CascadeType.ALL, mappedBy = "dict")
List<DictItem> items = new ArrayList<>(0);
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import static com.yiring.dict.domain.DictItem.DELETE_SQL;
import static com.yiring.dict.domain.DictItem.TABLE_NAME;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
import lombok.experimental.FieldNameConstants;
import lombok.experimental.SuperBuilder;
import org.hibernate.annotations.Comment;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.SQLDeleteAll;
import org.hibernate.annotations.Where;
/**
* 字典选项
*
* @author Jim
* @version 0.1
* 2023/1/20 13:58
*/
@Getter
@Setter
@EqualsAndHashCode(callSuper = false)
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@SQLDelete(sql = DELETE_SQL)
@SQLDeleteAll(sql = DELETE_SQL)
@Where(clause = BasicEntity.Where.EXIST)
@Entity
@Table(
name = TABLE_NAME,
indexes = {
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = DictItem.Fields.name),
@Index(columnList = DictItem.Fields.enable),
}
)
@Comment("数据字典选项")
public class DictItem extends BasicEntity implements Serializable {
public static final String TABLE_NAME = "SYS_DICT_ITEM";
public static final String DELETE_SQL = "update " + TABLE_NAME + Where.DELETE_SET;
@Serial
private static final long serialVersionUID = -7321430621819435483L;
@Comment("字典")
@ManyToOne
@JoinColumn(nullable = false)
Dict dict;
@Comment("字典选项名称")
@Column(nullable = false)
String name;
@Comment("字典选项值")
@Column(nullable = false)
String value;
@Comment("字典选项描述")
String description;
@Comment("字典选项排序序号")
Integer serial;
@Comment("字典选项是否启用")
@Column(nullable = false)
Boolean enable;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2023/1/20 14:10
*/
@Repository
public interface DictItemRepository extends JpaRepository<DictItem, Serializable>, JpaSpecificationExecutor<DictItem> {}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.domain;
import java.io.Serializable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
/**
* @author Jim
* @version 0.1
* 2023/1/20 14:10
*/
@Repository
public interface DictRepository extends JpaRepository<Dict, Serializable>, JpaSpecificationExecutor<Dict> {}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.param;
import com.yiring.common.validation.group.Group;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典选项参数
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictItemParam")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictItemParam implements Serializable {
@Serial
private static final long serialVersionUID = -4045504997838271449L;
@Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "字典 ID", example = "1")
@NotBlank
String dictId;
@Parameter(description = "字典选项名称", example = "男")
@NotBlank
String name;
@Parameter(description = "字典选项值", example = "1")
@NotBlank
String value;
@Parameter(description = "字典选项描述")
String description;
@Parameter(description = "字典选项排序序号")
Integer serial;
@Parameter(description = "字典选项是否启用", example = "true")
@NotNull
Boolean enable;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.param;
import com.yiring.common.validation.group.Group;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典参数
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictParam")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictParam implements Serializable {
@Serial
private static final long serialVersionUID = -5755754262686183535L;
@Parameter(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "字典名称", example = "性别")
@NotBlank
String name;
@Parameter(description = "字典编号", example = "GENDER")
@NotBlank
String code;
@Parameter(description = "字典描述")
String description;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.param;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典选项下拉查询参数
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "SelectorDictItemParam")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class SelectorDictItemParam implements Serializable {
@Serial
private static final long serialVersionUID = -8165884102089982475L;
@Parameter(description = "字典 ID", example = "1")
String dictId;
@Parameter(description = "字典 ID", example = "1")
String dictCode;
@Parameter(description = "字典选项是否启用", example = "true")
Boolean enable;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典选项输出
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictItemVo")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictItemVo implements Serializable {
@Serial
private static final long serialVersionUID = -5041312962732993815L;
@Schema(description = "id", example = "1")
String id;
@Schema(description = "字典选项名称", example = "男")
String name;
@Schema(description = "字典选项值", example = "1")
String value;
@Schema(description = "字典选项描述")
String description;
@Schema(description = "字典选项排序序号", example = "1")
Integer serial;
@Schema(description = "字典选项是否启用", example = "true")
Boolean enable;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.dict.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
import lombok.experimental.FieldDefaults;
/**
* 数据字典输出
*
* @author Jim
* @version 0.1
* 2022/3/25 17:09
*/
@Schema(name = "DictVo")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class DictVo implements Serializable {
@Serial
private static final long serialVersionUID = -5041312962732993815L;
@Schema(description = "id", example = "1")
String id;
@Schema(description = "字典名称", example = "性别")
String name;
@Schema(description = "字典编号", example = "GENDER")
String code;
@Schema(description = "字典描述")
String description;
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.web;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.util.Commons;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.KeyValueVo;
import com.yiring.common.vo.PageVo;
import com.yiring.dict.domain.Dict;
import com.yiring.dict.domain.DictRepository;
import com.yiring.dict.param.DictParam;
import com.yiring.dict.vo.DictVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Page;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据字典管理
*
* @author Jim
* @version 0.1
* 2023/1/20 15:29
*/
@Slf4j
@Validated
@Tag(
name = "字典管理",
description = "Dict",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-96") }) }
)
@RestController
@RequestMapping("/sys/dict/")
@RequiredArgsConstructor
public class DictController {
final DictRepository dictRepository;
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) DictParam param) {
if (has(param.getCode())) {
throw BusinessException.i18n("Code.101000");
}
Dict entity = new Dict();
BeanUtils.copyProperties(param, entity);
dictRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) DictParam param) {
Optional<Dict> optional = dictRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
Dict entity = optional.get();
if (!entity.getCode().equals(param.getCode())) {
throw BusinessException.i18n("Code.101002");
}
BeanUtils.copyProperties(param, entity);
dictRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "删除")
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<Dict> dictList = dictRepository.findAllById(param.toIds());
dictRepository.deleteAll(dictList);
return Result.ok();
}
@Operation(summary = "查询")
@GetMapping("find")
public Result<DictVo> find(@ParameterObject @Validated IdParam param) {
Optional<Dict> optional = dictRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
Dict entity = optional.get();
DictVo vo = Commons.transform(entity, DictVo.class);
return Result.ok(vo);
}
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<DictVo>> page(@ParameterObject @Validated PageParam param) {
Page<Dict> page = dictRepository.findAll(PageParam.toPageable(param));
List<DictVo> data = Commons.transform(page.getContent(), DictVo.class);
PageVo<DictVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@Operation(summary = "选项查询")
@GetMapping("selector")
public Result<ArrayList<KeyValueVo>> selector() {
List<Dict> dictList = dictRepository.findAll();
ArrayList<KeyValueVo> vos = dictList
.stream()
.map(dict -> KeyValueVo.builder().key(dict.getId()).value(dict.getCode()).label(dict.getName()).build())
.collect(Collectors.toCollection(ArrayList::new));
return Result.ok(vos);
}
/**
* 检查是否存在已有相同编码的字典
*
* @param code 字典编码
* @return 是否存在
*/
private boolean has(String code) {
Dict entity = Dict.builder().code(code).build();
return dictRepository.count(Example.of(entity)) > 0;
}
}
/* (C) 2023 YiRing, Inc. */
package com.yiring.dict.web;
import cn.hutool.core.util.StrUtil;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
import com.yiring.common.domain.BasicEntity;
import com.yiring.common.exception.BusinessException;
import com.yiring.common.param.IdParam;
import com.yiring.common.param.IdsParam;
import com.yiring.common.param.PageParam;
import com.yiring.common.util.Commons;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.KeyValueVo;
import com.yiring.common.vo.PageVo;
import com.yiring.dict.domain.Dict;
import com.yiring.dict.domain.DictItem;
import com.yiring.dict.domain.DictItemRepository;
import com.yiring.dict.param.DictItemParam;
import com.yiring.dict.param.SelectorDictItemParam;
import com.yiring.dict.vo.DictItemVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.extensions.Extension;
import io.swagger.v3.oas.annotations.extensions.ExtensionProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.persistence.criteria.Predicate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.beans.BeanUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 数据字典选项管理
*
* @author Jim
* @version 0.1
* 2023/1/20 15:29
*/
@Slf4j
@Validated
@Tag(
name = "字典选项管理",
description = "DictItem",
extensions = { @Extension(properties = { @ExtensionProperty(name = "x-order", value = "-95") }) }
)
@RestController
@RequestMapping("/sys/dict/item/")
@RequiredArgsConstructor
public class DictItemController {
final DictItemRepository dictItemRepository;
@Operation(summary = "新增")
@PostMapping("add")
public Result<String> add(@ParameterObject @Validated({ Group.Add.class }) DictItemParam param) {
DictItem entity = new DictItem();
BeanUtils.copyProperties(param, entity);
entity.setDict(Dict.builder().id(param.getDictId()).build());
dictItemRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "修改")
@PostMapping("modify")
public Result<String> modify(@ParameterObject @Validated({ Group.Edit.class }) DictItemParam param) {
Optional<DictItem> optional = dictItemRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
DictItem entity = optional.get();
BeanUtils.copyProperties(param, entity);
dictItemRepository.saveAndFlush(entity);
return Result.ok();
}
@Operation(summary = "删除")
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<DictItem> items = dictItemRepository.findAllById(param.toIds());
dictItemRepository.deleteAll(items);
return Result.ok();
}
@Operation(summary = "查询")
@GetMapping("find")
public Result<DictItemVo> find(@ParameterObject @Validated IdParam param) {
Optional<DictItem> optional = dictItemRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
}
DictItem entity = optional.get();
DictItemVo vo = Commons.transform(entity, DictItemVo.class);
return Result.ok(vo);
}
@Operation(summary = "分页查询")
@GetMapping("page")
public Result<PageVo<DictItemVo>> page(@ParameterObject @Validated PageParam param) {
Page<DictItem> page = dictItemRepository.findAll(PageParam.toPageable(param));
List<DictItemVo> data = Commons.transform(page.toList(), DictItemVo.class);
PageVo<DictItemVo> vo = PageVo.build(data, page.getTotalElements());
return Result.ok(vo);
}
@Operation(summary = "选项查询")
@GetMapping("selector")
public Result<ArrayList<KeyValueVo>> selector(@ParameterObject @Validated SelectorDictItemParam param) {
if (StrUtil.isBlank(param.getDictId()) && StrUtil.isBlank(param.getDictCode())) {
throw BusinessException.i18n("Code.101001");
}
Specification<DictItem> specification = (root, query, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if (Objects.nonNull(param.getEnable())) {
predicates.add(cb.equal(root.get(DictItem.Fields.enable), param.getEnable()));
}
if (Objects.nonNull(param.getDictId())) {
predicates.add(cb.equal(root.get(DictItem.Fields.dict).get(BasicEntity.Fields.id), param.getDictId()));
}
if (Objects.nonNull(param.getDictCode())) {
predicates.add(cb.equal(root.get(DictItem.Fields.dict).get(Dict.Fields.code), param.getDictCode()));
}
return query.where(predicates.toArray(new Predicate[0])).getRestriction();
};
List<DictItem> items = dictItemRepository.findAll(specification);
ArrayList<KeyValueVo> vos = items
.stream()
.map(item -> KeyValueVo.builder().key(item.getId()).value(item.getValue()).label(item.getName()).build())
.collect(Collectors.toCollection(ArrayList::new));
return Result.ok(vos);
}
}
Code.101000=\u5B57\u5178\u7F16\u53F7\u91CD\u590D
Code.101002=\u5B57\u5178\u7F16\u53F7\u4E0D\u5141\u8BB8\u4FEE\u6539
Code.101001=id\u548Ccode\u53C2\u6570\u81F3\u5C11\u4F20\u9012\u4E00\u4E2A
Code.101000=\u5B57\u5178\u7F16\u53F7\u91CD\u590D
Code.101002=\u5B57\u5178\u7F16\u53F7\u4E0D\u5141\u8BB8\u4FEE\u6539
Code.101001=id\u548Ccode\u53C2\u6570\u81F3\u5C11\u4F20\u9012\u4E00\u4E2A
plugins { plugins {
id 'java' id 'java'
// https://start.spring.io // https://start.spring.io
id 'org.springframework.boot' version '3.0.1' id 'org.springframework.boot' version '3.0.2'
id 'org.graalvm.buildtools.native' version '0.9.18' id 'org.graalvm.buildtools.native' version '0.9.18'
// https://plugins.gradle.org/plugin/io.spring.dependency-management // https://plugins.gradle.org/plugin/io.spring.dependency-management
id 'io.spring.dependency-management' version '1.1.0' id 'io.spring.dependency-management' version '1.1.0'
// https://plugins.gradle.org/plugin/com.diffplug.spotless // https://plugins.gradle.org/plugin/com.diffplug.spotless
id "com.diffplug.spotless" version "6.12.1" id "com.diffplug.spotless" version "6.13.0"
} }
ext { ext {
// Spotless // Spotless
// https://www.npmjs.com/package/prettier // https://www.npmjs.com/package/prettier
prettierVersion = '2.8.2' prettierVersion = '2.8.3'
// https://www.npmjs.com/package/prettier-plugin-java // https://www.npmjs.com/package/prettier-plugin-java
prettierJavaVersion = '2.0.0' prettierJavaVersion = '2.0.0'
// SpringCloud // SpringCloud
// https://start.spring.io/ // https://start.spring.io/
springCloudVersion = '2022.0.0' springCloudVersion = '2022.0.1'
// SpringBootAdmin
springBootAdminVersion = '3.0.0'
// Dependencies // Dependencies
// // https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter // // https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter
...@@ -28,9 +30,9 @@ ext { ...@@ -28,9 +30,9 @@ ext {
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot3-starter // https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot3-starter
saTokenVersion = '1.34.0' saTokenVersion = '1.34.0'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all // https://mvnrepository.com/artifact/cn.hutool/hutool-all
hutoolVersion = '5.8.11' hutoolVersion = '5.8.12'
// https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 // https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2
fastJsonVersion = '2.0.22' fastJsonVersion = '2.0.23'
// https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core // https://mvnrepository.com/artifact/com.xuxueli/xxl-job-core
xxlJobVersion = '2.3.1' xxlJobVersion = '2.3.1'
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
...@@ -38,9 +40,9 @@ ext { ...@@ -38,9 +40,9 @@ ext {
// https://mvnrepository.com/artifact/io.minio/minio // https://mvnrepository.com/artifact/io.minio/minio
minioVersion = '8.5.1' minioVersion = '8.5.1'
// https://mvnrepository.com/artifact/io.hypersistence/hypersistence-utils-hibernate-60 // https://mvnrepository.com/artifact/io.hypersistence/hypersistence-utils-hibernate-60
hibernateTypesVersion = '3.1.0' hibernateTypesVersion = '3.1.2'
// https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial // https://mvnrepository.com/artifact/org.hibernate/hibernate-spatial
hibernateSpatialVersion = '6.1.6.Final' hibernateSpatialVersion = '6.1.7.Final'
// https://mvnrepository.com/artifact/org.locationtech.jts/jts-core // https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
jtsVersion = '1.19.0' jtsVersion = '1.19.0'
// https://mvnrepository.com/artifact/com.github.liaochong/myexcel // https://mvnrepository.com/artifact/com.github.liaochong/myexcel
...@@ -49,6 +51,8 @@ ext { ...@@ -49,6 +51,8 @@ ext {
jetbrainsAnnotationsVersion = '24.0.0' jetbrainsAnnotationsVersion = '24.0.0'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox // https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
pdfboxVersion = '2.0.27' pdfboxVersion = '2.0.27'
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
ffmpegWrapperVersion = '0.7.0'
} }
allprojects { allprojects {
...@@ -87,6 +91,7 @@ subprojects { ...@@ -87,6 +91,7 @@ subprojects {
dependencyManagement { dependencyManagement {
imports { imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
} }
} }
......
...@@ -8,6 +8,7 @@ pluginManagement { ...@@ -8,6 +8,7 @@ pluginManagement {
rootProject.name = 'basic-api-boot' rootProject.name = 'basic-api-boot'
include 'app' include 'app'
include 'basic-auth' include 'basic-auth'
include 'basic-dict'
include 'basic-websocket' include 'basic-websocket'
include 'basic-common:core' include 'basic-common:core'
include 'basic-common:util' include 'basic-common:util'
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论