提交 075e438f 作者: 方治民

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

上级 4d47ee14
......@@ -42,6 +42,9 @@ dependencies {
implementation project(":basic-auth")
implementation "cn.dev33:sa-token-spring-boot3-starter:${saTokenVersion}"
// Optional: Dict - 数据字典
implementation project(":basic-dict")
// Optional: WebSocket && STOMP 依赖 Auth + Redis 模块
implementation project(":basic-websocket")
......@@ -57,6 +60,8 @@ dependencies {
implementation 'org.bytedeco:ffmpeg-platform:5.0-1.5.7'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
implementation "org.apache.pdfbox:pdfbox:${pdfboxVersion}"
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
implementation "net.bramp.ffmpeg:ffmpeg:${ffmpegWrapperVersion}"
// fastjson
implementation "com.alibaba.fastjson2:fastjson2:${fastJsonVersion}"
......
......@@ -3,10 +3,7 @@ package com.yiring.app.domain.user;
import com.yiring.auth.domain.user.User;
import com.yiring.common.domain.BasicEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import jakarta.persistence.*;
import java.io.Serial;
import java.io.Serializable;
import lombok.*;
......@@ -31,7 +28,7 @@ import org.hibernate.annotations.Comment;
@FieldNameConstants
@FieldDefaults(level = AccessLevel.PRIVATE)
@Entity
@Table(name = "SYS_USER_EXTENSION")
@Table(name = "SYS_USER_EXTENSION", uniqueConstraints = @UniqueConstraint(columnNames = "user_id"))
@Comment("用户扩展表")
public class UserExtension extends BasicEntity implements Serializable {
......@@ -40,7 +37,7 @@ public class UserExtension extends BasicEntity implements Serializable {
@Comment("用户")
@OneToOne
@JoinColumn(nullable = false, unique = true)
@JoinColumn(nullable = false, name = "user_id")
User user;
@Comment("性别")
......
......@@ -4,22 +4,28 @@ package com.yiring.app.service.upload;
import cn.hutool.core.io.FileUtil;
import com.yiring.common.core.Minio;
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.RenderedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
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.regex.Pattern;
import javax.imageio.ImageIO;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.Frame;
import org.bytedeco.javacv.Java2DFrameConverter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.bytedeco.javacv.*;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
......@@ -29,13 +35,16 @@ import org.springframework.stereotype.Component;
* 2022/9/23 16:44
*/
@ConditionalOnClass({ PDDocument.class, FFmpegFrameGrabber.class })
@Slf4j
@Primary
@Component
@RequiredArgsConstructor
public class UploadProcessServiceImpl implements UploadProcessService {
final Minio minio;
Pattern pattern = Pattern.compile("^.*\\.ts$");
@Override
public String handle(String object, InputStream is) {
String suffix = FileUtil.getSuffix(object);
......@@ -110,13 +119,82 @@ public class UploadProcessServiceImpl implements UploadProcessService {
ImageIO.write(frameToBufferedImage(frame), format, os);
@Cleanup
InputStream io = new ByteArrayInputStream(os.toByteArray());
int size = io.available();
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();
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 {
int ftp = grabber.getLengthInFrames();
int flag = 0;
......
......@@ -15,12 +15,15 @@ import com.yiring.common.core.I18n;
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.PageParam;
import com.yiring.common.service.FileManageService;
import com.yiring.common.util.Commons;
import com.yiring.common.util.FileUtils;
import com.yiring.common.validation.group.Group;
import com.yiring.common.vo.PageVo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
......@@ -36,6 +39,7 @@ import org.springframework.data.domain.Example;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* 示例接口
......@@ -55,6 +59,7 @@ public class ExampleController {
final I18n i18n;
final Auths auths;
final UserExtensionRepository userExtensionRepository;
final FileManageService fileManageService;
@Operation(summary = "Hello World")
@GetMapping
......@@ -101,6 +106,23 @@ public class ExampleController {
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
@SaCheckLogin
@Operation(summary = "查询用户属性")
......
# 环境变量
env:
host: 192.168.0.156
# host: 192.168.0.156
host: 127.0.0.1
prod: false
extra:
username: admin
# username: admin
password: 123456
username: postgres
ffmpeg:
path: D:\Environments\FFmpeg\bin
# ----------------------------------------------
spring:
......@@ -17,10 +21,13 @@ spring:
open-in-view: true
hibernate:
ddl-auto: update
show-sql: false
show-sql: true
properties:
hibernate:
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://hibernate.atlassian.net/browse/HHH-15827
jakarta:
......@@ -40,7 +47,7 @@ spring:
springdoc:
default-consumes-media-type: "application/x-www-form-urlencoded"
default-produces-media-type: "application/json"
default-flat-param-object: true
default-flat-param-object: false
override-with-generic-response: false
api-docs:
resolve-schema-properties: true
......
......@@ -13,11 +13,8 @@ spring:
name: "basic-api-app"
servlet:
multipart:
max-file-size: 50MB
max-request-size: 100MB
messages:
basename: i18n/status,i18n/validation,i18n/messages
always-use-message-format: true
max-file-size: 1024MB
max-request-size: 1048MB
profiles:
include: auth, conf-patch
active: dev-postgresql
......
......@@ -18,7 +18,7 @@ import org.springframework.web.bind.annotation.RestControllerAdvice;
* 2023/1/12 14:06
*/
@Slf4j
@Order(0)
@Order(1)
@RestControllerAdvice
@RequiredArgsConstructor
public class AuthExceptionHandler {
......
......@@ -14,6 +14,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
/**
* 获取登录用户权限信息实现
......@@ -23,6 +24,7 @@ import org.springframework.stereotype.Component;
* 2022/3/25 9:37
*/
@Transactional(readOnly = true)
@Component
@RequiredArgsConstructor
public class StpInterfaceImpl implements StpInterface {
......@@ -54,7 +56,7 @@ public class StpInterfaceImpl implements StpInterface {
* @param loginId 登录 ID
* @return 用户信息
*/
private User getUser(Object loginId) {
public User getUser(Object loginId) {
String id = Objects.toString(loginId);
Optional<User> optional = userRepository.findById(id);
if (optional.isEmpty()) {
......
......@@ -41,10 +41,10 @@ import org.hibernate.annotations.Where;
@Table(
name = TABLE_NAME,
indexes = {
@Index(columnList = "deleteTime"),
@Index(columnList = "type"),
@Index(columnList = "pid"),
@Index(columnList = "tree"),
@Index(columnList = BasicEntity.Fields.deleteTime),
@Index(columnList = Permission.Fields.type),
@Index(columnList = Permission.Fields.pid),
@Index(columnList = Permission.Fields.tree),
}
)
@Comment("系统权限")
......
......@@ -43,7 +43,8 @@ import org.hibernate.annotations.Where;
@Entity
@Table(
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("系统角色")
public class Role extends BasicEntity implements Serializable {
......
......@@ -40,12 +40,11 @@ import org.hibernate.annotations.Where;
@Entity
@Table(
name = User.TABLE_NAME,
indexes = {
@Index(columnList = "enabled"),
@Index(columnList = "deleteTime"),
@Index(columnList = "username,deleteTime", unique = true),
@Index(columnList = "mobile,deleteTime", unique = true),
@Index(columnList = "email,deleteTime", unique = true),
indexes = { @Index(columnList = User.Fields.enabled), @Index(columnList = BasicEntity.Fields.deleteTime) },
uniqueConstraints = {
@UniqueConstraint(columnNames = { User.Fields.username, BasicEntity.Fields.deleteTime }),
@UniqueConstraint(columnNames = { User.Fields.mobile, BasicEntity.Fields.deleteTime }),
@UniqueConstraint(columnNames = { User.Fields.email, BasicEntity.Fields.deleteTime }),
}
)
@Comment("系统用户")
......
......@@ -51,9 +51,6 @@ public class RegisterParam implements Serializable {
@Parameter(description = "邮箱", example = "developer@yiring.com")
String email;
@Parameter(description = "简介", example = "平台管理员")
String introduction;
@Parameter(description = "是否启用", example = "true")
Boolean enable;
}
/* (C) 2022 YiRing, Inc. */
package com.yiring.auth.param.permission;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.yiring.auth.domain.permission.Permission;
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.NotEmpty;
......@@ -34,47 +34,47 @@ public class PermissionParam implements Serializable {
@Serial
private static final long serialVersionUID = -6781934969837655538L;
@Parameter(description = "id", example = "1")
@Schema(description = "id", example = "1")
@NotBlank(groups = { Group.Edit.class })
String id;
@Parameter(description = "权限类型", example = "MENU")
@Schema(description = "权限类型", example = "MENU")
@NotNull(message = "权限类型不能为空")
Permission.Type type;
@Parameter(description = "序号", example = "1")
@Schema(description = "序号", example = "1")
Integer serial;
@Parameter(description = "标识", example = "Dashboard")
@Schema(description = "标识", example = "Dashboard")
@NotEmpty(message = "权限标识不能为空")
String uid;
@Parameter(description = "名称", example = "Dashboard")
@Schema(description = "名称", example = "Dashboard")
@NotEmpty(message = "权限名称不能为空")
String name;
@Parameter(description = "路径", example = "/dashboard")
@Schema(description = "路径", example = "/dashboard")
String path;
@Parameter(description = "重定向", example = "/dashboard/workbench")
@Schema(description = "重定向", example = "/dashboard/workbench")
String redirect;
@Parameter(description = "组件", example = "LAYOUT")
@Schema(description = "组件", example = "LAYOUT")
String component;
@Parameter(description = "图标", example = "ion:grid-outline")
@Schema(description = "图标", example = "ion:grid-outline")
String icon;
@Parameter(description = "是否隐藏", example = "false")
@Schema(description = "是否隐藏", example = "false")
Boolean hidden;
@Parameter(description = "是否启用", example = "true")
@Schema(description = "是否启用", example = "true")
Boolean enable;
@Parameter(description = "父级ID", example = "0")
@Schema(description = "父级ID", example = "0")
@Builder.Default
String pid = "0";
@Parameter(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
String meta;
@Schema(description = "元数据", example = "{\"title\": \"routes.dashboard.dashboard\"}")
JSONObject meta;
}
......@@ -86,7 +86,6 @@ public class AuthController {
.username(param.getUsername())
.password(SaSecureUtil.sha256(param.getPassword()))
.enabled(param.getEnable())
.createTime(LocalDateTime.now())
.build();
userRepository.saveAndFlush(user);
return Result.ok();
......@@ -156,7 +155,7 @@ public class AuthController {
public Result<String> safe(@ParameterObject @Validated SafeParam param) {
User user = auths.getLoginUser();
if (SaSecureUtil.sha256(param.getPassword()).equals(user.getPassword())) {
StpUtil.openSafe(120);
StpUtil.openSafe(360);
return Result.ok();
}
......
......@@ -2,8 +2,6 @@
package com.yiring.auth.web.sys.permission;
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.PermissionRepository;
import com.yiring.auth.param.permission.PermissionParam;
......@@ -31,10 +29,7 @@ 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;
import org.springframework.web.bind.annotation.*;
/**
* 系统权限管理控制器
......@@ -60,7 +55,7 @@ public class PermissionController {
@Operation(summary = "新增")
@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())) {
throw BusinessException.i18n("Code.1001");
}
......@@ -72,7 +67,7 @@ public class PermissionController {
@Operation(summary = "修改")
@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());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -91,8 +86,8 @@ public class PermissionController {
}
@Operation(summary = "删除")
@PostMapping("deleted")
public Result<String> deleted(@ParameterObject @Validated IdParam param) {
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdParam param) {
Optional<Permission> optional = permissionRepository.findById(param.getId());
if (optional.isEmpty()) {
throw Status.NOT_FOUND.exception();
......@@ -173,9 +168,6 @@ public class PermissionController {
private void save(Permission entity, PermissionParam param) {
BeanUtils.copyProperties(param, entity, Permission.Fields.meta);
entity.setTree(getTreeNode(param.getPid()));
if (JSONValidator.from(param.getMeta()).validate()) {
entity.setMeta(JSON.parseObject(param.getMeta()));
}
permissionRepository.saveAndFlush(entity);
}
}
......@@ -113,8 +113,8 @@ public class RoleController {
}
@Operation(summary = "删除")
@PostMapping("deleted")
public Result<String> deleted(@ParameterObject @Validated IdsParam param) {
@PostMapping("remove")
public Result<String> remove(@ParameterObject @Validated IdsParam param) {
List<Role> roles = roleRepository.findAllById(param.toIds());
roleRepository.deleteAll(roles);
return Result.ok();
......
......@@ -30,7 +30,7 @@ public @interface DownloadResponse {
@AliasFor(annotation = ApiResponse.class)
Content content() default @Content(
schema = @Schema(type = "string", format = "binary"),
schema = @Schema(type = "file", format = "binary"),
mediaType = MediaType.APPLICATION_OCTET_STREAM_VALUE
);
}
......@@ -37,11 +37,34 @@ public class EnvConfig implements Serializable {
boolean prod;
/**
* FFmpeg 配置
*/
FFmpeg ffmpeg;
/**
* 扩展配置
*/
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
......
......@@ -192,7 +192,7 @@ public class Result<T> implements Serializable {
@PropertyKey(resourceBundle = I18n.RESOURCE_BUNDLE) String details,
Throwable error
) {
if (Objects.isNull(code)) {
if (Objects.isNull(code) && Objects.nonNull(details)) {
String prefix = "Code.";
if (details.startsWith(prefix)) {
String codeText = details.replace(prefix, "");
......
/* (C) 2021 YiRing, Inc. */
package com.yiring.common.param;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
......@@ -37,13 +36,11 @@ public class PageParam implements Serializable {
@Serial
private static final long serialVersionUID = 6103761701912769946L;
@Parameter
@Schema(description = "分页条数", defaultValue = "10", example = "10", type = "integer")
@NotNull
@Range(min = 1, max = 100)
Integer pageSize;
@Parameter
@Schema(description = "当前页数", defaultValue = "1", example = "1", type = "integer")
@NotNull
@Min(1)
......@@ -84,6 +81,6 @@ public class PageParam implements Serializable {
if (Objects.nonNull(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 {
interface Edit extends Default {}
/**
* 数据查询分组
*/
interface Query extends Default {}
/**
* 通用的必填分组
*/
interface Required extends Default {}
......
......@@ -37,4 +37,10 @@ public class KeyValueVo implements Serializable {
*/
@Schema(description = "label", example = "label")
String label;
/**
* 扩展字段,可用于显示禁用状态
*/
@Schema(description = "是否启用", example = "true")
String enable;
}
......@@ -5,23 +5,27 @@ import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Console;
import cn.hutool.core.net.NetUtil;
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.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.tags.Tag;
import java.lang.annotation.Annotation;
import java.util.*;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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
......@@ -81,7 +85,12 @@ public class SwaggerConfig implements CommandLineRunner {
@Bean(name = "api.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")
......@@ -115,24 +124,33 @@ public class SwaggerConfig implements CommandLineRunner {
};
}
// @Bean
// public OperationCustomizer addGlobalStatusResponse() {
// return (operation, handlerMethod) -> {
// ApiResponses responses = operation.getResponses();
// for (Status status : Status.values()) {
// if (Status.OK.equals(status)) {
// continue;
// }
//
// ApiResponse response = new ApiResponse()
// .description(i18n.get(status.getReasonPhrase()))
// .$ref("Response");
// responses.addApiResponse(String.valueOf(status.value()), response);
// }
//
// return operation;
// };
// }
/**
* 自定义 OperationId 用于优化 Pont 接口文件生成
*/
@Bean
public OperationCustomizer operationIdCustomizer() {
return (operation, handlerMethod) -> {
Class<?> superClazz = handlerMethod.getBeanType().getSuperclass();
if (Objects.nonNull(superClazz)) {
Annotation[] annotations = handlerMethod.getMethod().getAnnotations();
String methodType = "GET";
for (Annotation annotation : annotations) {
if (annotation instanceof PostMapping) {
methodType = "POST";
} else if (annotation instanceof PutMapping) {
methodType = "PUT";
} 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(
String group,
......@@ -144,9 +162,10 @@ public class SwaggerConfig implements CommandLineRunner {
.builder()
.group(group)
.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]))
.pathsToExclude(pathsToExclude.toArray(new String[0]))
.addOperationCustomizer(operationIdCustomizer())
.build();
}
......
......@@ -3,9 +3,11 @@ package com.yiring.common.config;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.annotation.Order;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
......@@ -18,15 +20,34 @@ import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
* 2022/8/17 10:33
*/
@Order
@Configuration
@RequiredArgsConstructor
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
public LocaleResolver localeResolver() {
// header accept-language
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
resolver.setDefaultLocale(DEFAULT_LOCALE);
return resolver;
}
......@@ -34,9 +55,8 @@ public class I18nConfig {
public LocalValidatorFactoryBean getValidator() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setDefaultEncoding("UTF-8");
messageSource.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
messageSource.setDefaultLocale(DEFAULT_LOCALE);
messageSource.setBasenames("classpath:/ValidationMessages", "classpath:i18n/validation");
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource);
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;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
......@@ -20,7 +19,7 @@ import org.springframework.stereotype.Component;
*/
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Order
@Component
@RequiredArgsConstructor
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 {
* 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.mp4 -> haha.mp4.jpg 截取视频封面
*
* @param object 上传文件存储地址
* @param is 文件流
* @param is 文件流
* @return 预处理后的文件地址(可能对文件名追加了时长、页数、分辨率等标识)
*/
@SuppressWarnings("unused")
default String handle(String object, InputStream is) {
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;
import com.yiring.common.core.Result;
import com.yiring.common.core.Status;
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.vo.ImageInfo;
import io.minio.GetObjectResponse;
......@@ -49,7 +49,7 @@ import org.springframework.web.multipart.MultipartFile;
public class MinioController {
final Minio minio;
final UploadProcessService service;
final FileManageService fileManageService;
/**
* minio 上传文件,成功返回文件 url
......@@ -58,22 +58,8 @@ public class MinioController {
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Result<String> upload(@Parameter(name = "文件", required = true) @RequestPart("file") MultipartFile file) {
try {
// 获取文件名信息
String filename = file.getOriginalFilename();
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));
String link = fileManageService.upload(file);
return Result.ok(link);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw Status.BAD_REQUEST.exception();
......
......@@ -20,4 +20,9 @@ public @interface Times {
* 描述
*/
String value() default "";
/**
* 阈值(毫秒),当计算耗时超过阈值时,打印警告日志
*/
long threshold() default 0;
}
......@@ -10,6 +10,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
......@@ -30,12 +31,22 @@ public class TimesAspect {
@Around("times()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long start = System.currentTimeMillis();
try {
return point.proceed();
} finally {
long end = System.currentTimeMillis();
log.info("[Times] {}: {} ms", getTimesValue(point), end - start);
Object result = point.proceed();
long end = System.currentTimeMillis();
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) {
......
......@@ -3,14 +3,10 @@ package com.yiring.common.util;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileReader;
import com.yiring.common.vo.ImageInfo;
import jakarta.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
......@@ -18,8 +14,8 @@ import java.nio.file.attribute.BasicFileAttributes;
import javax.imageio.ImageIO;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.util.FileCopyUtils;
/**
* 文件工具类
......@@ -37,9 +33,9 @@ public class FileUtils {
*
* @param response HttpServletResponse
* @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);
BasicFileAttributes basicFileAttributes = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
long lastModified = basicFileAttributes.lastModifiedTime().toMillis();
......@@ -47,7 +43,13 @@ public class FileUtils {
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(basicFileAttributes.size()));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
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 {
* @param filename 文件名称(带后缀)
* @param contentType 文档类型
* @param lastModified 最后修改时间
* @throws IOException IOException
*/
@SneakyThrows
public void download(
HttpServletResponse response,
InputStream object,
......@@ -68,15 +70,19 @@ public class FileUtils {
String filename,
String contentType,
long lastModified
) throws IOException {
) {
filename = URLEncoder.encode(filename, StandardCharsets.UTF_8);
response.setHeader(HttpHeaders.LAST_MODIFIED, String.valueOf(lastModified));
response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(length));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + filename);
response.setContentType(contentType);
IOUtils.copy(object, response.getOutputStream());
object.close();
response.flushBuffer();
try (
object;
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 {
id 'java'
// 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'
// https://plugins.gradle.org/plugin/io.spring.dependency-management
id 'io.spring.dependency-management' version '1.1.0'
// 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 {
// Spotless
// https://www.npmjs.com/package/prettier
prettierVersion = '2.8.2'
prettierVersion = '2.8.3'
// https://www.npmjs.com/package/prettier-plugin-java
prettierJavaVersion = '2.0.0'
// SpringCloud
// https://start.spring.io/
springCloudVersion = '2022.0.0'
springCloudVersion = '2022.0.1'
// SpringBootAdmin
springBootAdminVersion = '3.0.0'
// Dependencies
// // https://mvnrepository.com/artifact/com.github.xiaoymin/knife4j-openapi3-jakarta-spring-boot-starter
......@@ -28,9 +30,9 @@ ext {
// https://mvnrepository.com/artifact/cn.dev33/sa-token-spring-boot3-starter
saTokenVersion = '1.34.0'
// https://mvnrepository.com/artifact/cn.hutool/hutool-all
hutoolVersion = '5.8.11'
hutoolVersion = '5.8.12'
// 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
xxlJobVersion = '2.3.1'
// https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
......@@ -38,9 +40,9 @@ ext {
// https://mvnrepository.com/artifact/io.minio/minio
minioVersion = '8.5.1'
// 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
hibernateSpatialVersion = '6.1.6.Final'
hibernateSpatialVersion = '6.1.7.Final'
// https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
jtsVersion = '1.19.0'
// https://mvnrepository.com/artifact/com.github.liaochong/myexcel
......@@ -49,6 +51,8 @@ ext {
jetbrainsAnnotationsVersion = '24.0.0'
// https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox
pdfboxVersion = '2.0.27'
// https://mvnrepository.com/artifact/net.bramp.ffmpeg/ffmpeg
ffmpegWrapperVersion = '0.7.0'
}
allprojects {
......@@ -87,6 +91,7 @@ subprojects {
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
mavenBom "de.codecentric:spring-boot-admin-dependencies:${springBootAdminVersion}"
}
}
......
......@@ -8,6 +8,7 @@ pluginManagement {
rootProject.name = 'basic-api-boot'
include 'app'
include 'basic-auth'
include 'basic-dict'
include 'basic-websocket'
include 'basic-common:core'
include 'basic-common:util'
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论