refactor(modules): 拆分多模块工程并收口common基础模块

This commit is contained in:
2026-06-01 03:26:18 +08:00
parent 6fe1209801
commit 07ad8bb36b
231 changed files with 1690 additions and 172 deletions

View File

@@ -0,0 +1,17 @@
package com.bruce.common.config;
import com.bruce.common.constant.CommonConsts;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@Data
@NoArgsConstructor
@ConfigurationProperties(prefix = "common.attachment")
public class AttachmentProperties {
private String basePath = CommonConsts.DEFAULT_ATTACHMENT_BASE_PATH;
}

View File

@@ -0,0 +1,41 @@
package com.bruce.common.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class EntityAuditMetaObjectHandler implements MetaObjectHandler {
private static final String SYSTEM_USER = "system";
@Override
public void insertFill(MetaObject metaObject) {
Date now = new Date();
fillIfNull(metaObject, "createTime", now);
fillIfNull(metaObject, "updateTime", now);
fillIfNull(metaObject, "createBy", SYSTEM_USER);
fillIfNull(metaObject, "updateBy", SYSTEM_USER);
}
@Override
public void updateFill(MetaObject metaObject) {
setIfWritable(metaObject, "updateTime", new Date());
setIfWritable(metaObject, "updateBy", SYSTEM_USER);
}
private void fillIfNull(MetaObject metaObject, String fieldName, Object fieldVal) {
if (!metaObject.hasSetter(fieldName) || metaObject.getValue(fieldName) != null) {
return;
}
metaObject.setValue(fieldName, fieldVal);
}
private void setIfWritable(MetaObject metaObject, String fieldName, Object fieldVal) {
if (metaObject.hasSetter(fieldName)) {
metaObject.setValue(fieldName, fieldVal);
}
}
}

View File

@@ -0,0 +1,17 @@
package com.bruce.common.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}

View File

@@ -0,0 +1,21 @@
package com.bruce.common.constant;
public final class CommonConsts {
public static final String DATE_FORMAT_LONG_STR = "yyyy-MM-dd HH:mm:ss";
public static final String DATE_FORMAT_MILLIS_STR = "yyyy-MM-dd HH:mm:ss.SSS";
public static final String TIME_ZONE_GMT8 = "GMT+8";
public static final String DEFAULT_ATTACHMENT_BASE_PATH = "data/attachments";
public static final String STORAGE_TYPE_LOCAL = "LOCAL";
public static final String REQUEST_RESULT_SUCCESS_CODE = "0";
public static final String REQUEST_RESULT_FAIL_CODE = "-1";
private CommonConsts() {
}
}

View File

@@ -0,0 +1,41 @@
package com.bruce.common.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
import com.bruce.common.dto.response.SysAttachmentResponse;
import com.bruce.common.factory.SysAttachmentFactory;
import com.bruce.common.service.ISysAttachmentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Tag(name = "系统附件管理")
@Slf4j
@RestController
@RequestMapping("/api/attachments")
@RequiredArgsConstructor
public class SysAttachmentController {
private final ISysAttachmentService sysAttachmentService;
private final SysAttachmentFactory sysAttachmentFactory;
@Operation(summary = "上传附件")
@PostMapping("/upload")
public RequestResult<SysAttachmentResponse> upload(@ModelAttribute SysAttachmentUploadRequest request) {
log.info("上传附件开始sourceType={}, sourceId={}",
request == null ? null : request.getSourceType(),
request == null ? null : request.getSourceId());
SysAttachmentResponse response = sysAttachmentFactory.toResponse(sysAttachmentService.upload(request));
log.info("上传附件结束attachmentId={}, sourceType={}, sourceId={}",
response == null ? null : response.getId(),
request == null ? null : request.getSourceType(),
request == null ? null : request.getSourceId());
return RequestResult.success(response);
}
}

View File

@@ -0,0 +1,96 @@
package com.bruce.common.controller;
import com.bruce.common.domain.model.RequestResult;
import com.bruce.common.dto.request.SysEnumBatchSaveRequest;
import com.bruce.common.dto.request.SysEnumManageQueryRequest;
import com.bruce.common.dto.request.SysEnumQueryRequest;
import com.bruce.common.dto.request.SysEnumSaveRequest;
import com.bruce.common.dto.response.SysEnumResponse;
import com.bruce.common.service.ISysEnumService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Tag(name = "系统枚举管理")
@Slf4j
@RestController
@RequestMapping("/api/sys-enum")
public class SysEnumController {
@Autowired
private ISysEnumService sysEnumService;
@Operation(summary = "查询全部系统枚举")
@PostMapping("/list")
public RequestResult<List<SysEnumResponse>> list() {
log.info("SysEnumController.list start");
List<SysEnumResponse> responses = sysEnumService.listResponses();
log.info("SysEnumController.list success, count={}", responses.size());
return RequestResult.success(responses);
}
@Operation(summary = "根据模块和类型查询系统枚举")
@PostMapping("/query")
public RequestResult<List<SysEnumResponse>> queryByCatalogAndType(@RequestBody SysEnumQueryRequest request) {
log.info("SysEnumController.queryByCatalogAndType start, request={}", request);
List<SysEnumResponse> responses = sysEnumService.listByCatalogAndTypeResponses(request);
log.info("SysEnumController.queryByCatalogAndType success, count={}", responses.size());
return RequestResult.success(responses);
}
@Operation(summary = "管理端查询系统枚举")
@PostMapping("/queryForManagement")
public RequestResult<List<SysEnumResponse>> queryForManagement(@RequestBody(required = false) SysEnumManageQueryRequest request) {
log.info("SysEnumController.queryForManagement start, request={}", request);
List<SysEnumResponse> responses = sysEnumService.listForManagement(request);
log.info("SysEnumController.queryForManagement success, count={}", responses.size());
return RequestResult.success(responses);
}
@Operation(summary = "查询系统枚举详情")
@GetMapping("/detail")
public RequestResult<SysEnumResponse> getById(@RequestParam("id") Long id) {
log.info("SysEnumController.getById start, id={}", id);
SysEnumResponse response = sysEnumService.getResponseById(id);
log.info("SysEnumController.getById success, id={}, found={}", id, response != null);
return RequestResult.success(response);
}
@Operation(summary = "新增或修改系统枚举")
@PostMapping("/save")
public RequestResult<Boolean> saveOrUpdate(@RequestBody SysEnumSaveRequest request) {
log.info("SysEnumController.saveOrUpdate start, request={}", request);
Boolean result = sysEnumService.saveOrUpdate(request);
log.info("SysEnumController.saveOrUpdate success, id={}, catalog={}, type={}, value={}, result={}",
request.getId(), request.getCatalog(), request.getType(), request.getValue(), result);
return RequestResult.success(result);
}
@Operation(summary = "批量新增系统枚举")
@PostMapping("/batchSave")
public RequestResult<Boolean> batchSave(@RequestBody SysEnumBatchSaveRequest request) {
log.info("SysEnumController.batchSave start, request={}", request);
Boolean result = sysEnumService.batchSave(request);
log.info("SysEnumController.batchSave success, catalog={}, type={}, itemCount={}, result={}",
request.getCatalog(), request.getType(), request.getItems() == null ? 0 : request.getItems().size(), result);
return RequestResult.success(result);
}
@Operation(summary = "删除系统枚举")
@PostMapping("/delete")
public RequestResult<Boolean> deleteById(@RequestParam("id") Long id) {
log.info("SysEnumController.deleteById start, id={}", id);
Boolean result = sysEnumService.removeById(id);
log.info("SysEnumController.deleteById success, id={}, result={}", id, result);
return RequestResult.success(result);
}
}

View File

@@ -0,0 +1,21 @@
package com.bruce.common.document.parse;
import lombok.Data;
import java.nio.file.Path;
@Data
public class DocumentParseContext {
private Long documentId;
private Long attachmentId;
private String originalName;
private String suffix;
private String contentType;
private Path filePath;
}

View File

@@ -0,0 +1,12 @@
package com.bruce.common.document.parse;
public class DocumentParseException extends RuntimeException {
public DocumentParseException(String message) {
super(message);
}
public DocumentParseException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,20 @@
package com.bruce.common.document.parse;
import lombok.Data;
import java.util.LinkedHashMap;
import java.util.Map;
@Data
public class DocumentParseResult {
private String text;
private Integer textLength;
private Integer pageCount;
private Integer sheetCount;
private Map<String, Object> metadata = new LinkedHashMap<>();
}

View File

@@ -0,0 +1,8 @@
package com.bruce.common.document.parse;
public interface DocumentParser {
boolean supports(DocumentParseContext context);
DocumentParseResult parse(DocumentParseContext context);
}

View File

@@ -0,0 +1,37 @@
package com.bruce.common.document.parse;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Locale;
@Component
public class DocumentParserFactory {
private final List<DocumentParser> parsers;
public DocumentParserFactory(List<DocumentParser> parsers) {
this.parsers = parsers;
}
public DocumentParser resolve(DocumentParseContext context) {
return parsers.stream()
.filter(parser -> parser.supports(context))
.findFirst()
.orElseThrow(() -> new DocumentParseException("不支持的文档类型: " + resolveType(context)));
}
private String resolveType(DocumentParseContext context) {
if (context == null) {
return "unknown";
}
if (StringUtils.hasText(context.getSuffix())) {
return context.getSuffix().trim().toLowerCase(Locale.ROOT);
}
if (StringUtils.hasText(context.getContentType())) {
return context.getContentType().trim();
}
return "unknown";
}
}

View File

@@ -0,0 +1,64 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParseException;
import com.bruce.common.document.parse.DocumentParseResult;
import org.apache.tika.Tika;
import org.apache.tika.exception.TikaException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.Locale;
import java.util.Set;
abstract class AbstractTikaDocumentParser {
private static final int MAX_TEXT_LENGTH = -1;
private final Tika tika = new Tika();
boolean supportsSuffix(DocumentParseContext context, Set<String> suffixes) {
return context != null
&& StringUtils.hasText(context.getSuffix())
&& suffixes.contains(context.getSuffix().trim().toLowerCase(Locale.ROOT));
}
boolean supportsContentType(DocumentParseContext context, String prefix) {
return context != null
&& StringUtils.hasText(context.getContentType())
&& context.getContentType().trim().toLowerCase(Locale.ROOT).startsWith(prefix);
}
DocumentParseResult parseWithTika(DocumentParseContext context) {
if (context == null || context.getFilePath() == null) {
throw new DocumentParseException("解析文件不能为空");
}
try {
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, context.getOriginalName());
if (StringUtils.hasText(context.getContentType())) {
metadata.set(Metadata.CONTENT_TYPE, context.getContentType());
}
String text;
try (InputStream inputStream = Files.newInputStream(context.getFilePath())) {
text = tika.parseToString(inputStream, metadata, MAX_TEXT_LENGTH);
}
DocumentParseResult result = new DocumentParseResult();
result.setText(text == null ? "" : text.trim());
result.setTextLength(result.getText().length());
result.getMetadata().put("contentType", firstNonBlank(metadata.get(Metadata.CONTENT_TYPE), context.getContentType()));
result.getMetadata().put("resourceName", firstNonBlank(metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY), context.getOriginalName()));
return result;
} catch (IOException | TikaException e) {
throw new DocumentParseException("文档解析失败: " + e.getMessage(), e);
}
}
private String firstNonBlank(String first, String fallback) {
return StringUtils.hasText(first) ? first : fallback;
}
}

View File

@@ -0,0 +1,26 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class ExcelDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("xls", "xlsx");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES)
|| supportsContentType(context, "application/vnd.ms-excel")
|| supportsContentType(context, "application/vnd.openxmlformats-officedocument.spreadsheetml");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,24 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class PdfDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("pdf");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES) || supportsContentType(context, "application/pdf");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,24 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class TxtDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("txt", "md", "log");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES) || supportsContentType(context, "text/");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,26 @@
package com.bruce.common.document.parse.impl;
import com.bruce.common.document.parse.DocumentParseContext;
import com.bruce.common.document.parse.DocumentParser;
import com.bruce.common.document.parse.DocumentParseResult;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class WordDocumentParser extends AbstractTikaDocumentParser implements DocumentParser {
private static final Set<String> SUFFIXES = Set.of("doc", "docx");
@Override
public boolean supports(DocumentParseContext context) {
return supportsSuffix(context, SUFFIXES)
|| supportsContentType(context, "application/msword")
|| supportsContentType(context, "application/vnd.openxmlformats-officedocument.wordprocessingml");
}
@Override
public DocumentParseResult parse(DocumentParseContext context) {
return parseWithTika(context);
}
}

View File

@@ -0,0 +1,60 @@
package com.bruce.common.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("sys_attachment")
@Schema(description = "系统附件")
public class SysAttachment extends BaseEntity {
@Schema(description = "来源业务类型")
@TableField("source_type")
private String sourceType;
@Schema(description = "来源业务ID")
@TableField("source_id")
private Long sourceId;
@Schema(description = "原始文件名")
@TableField("original_name")
private String originalName;
@Schema(description = "存储文件名")
@TableField("file_name")
private String fileName;
@Schema(description = "文件后缀")
@TableField("file_suffix")
private String fileSuffix;
@Schema(description = "文件MIME类型")
@TableField("content_type")
private String contentType;
@Schema(description = "文件大小(字节)")
@TableField("file_size")
private Long fileSize;
@Schema(description = "存储类型")
@TableField("storage_type")
private String storageType;
@Schema(description = "文件存储路径")
@TableField("file_path")
private String filePath;
@Schema(description = "文件访问地址")
@TableField("file_url")
private String fileUrl;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,37 @@
package com.bruce.common.domain.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bruce.common.domain.model.BaseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
@TableName("sys_enum")
@Schema(description = "系统枚举配置")
public class SysEnum extends BaseEntity {
@Schema(description = "模块")
private String catalog;
@Schema(description = "类型")
private String type;
@Schema(description = "名称")
private String name;
@Schema(description = "")
private Integer value;
@Schema(description = "字符串值")
private String strvalue;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,45 @@
package com.bruce.common.domain.model;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.Version;
import com.bruce.common.constant.CommonConsts;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@Schema(description = "实体公共基类")
public class BaseEntity {
@Schema(description = "主键ID")
@TableId(type = IdType.ASSIGN_ID)
private Long id;
@Schema(description = "创建者")
@TableField(value = "create_by", fill = FieldFill.INSERT)
private String createBy;
@Schema(description = "创建时间", example = "2026-05-18 20:00:00")
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@Schema(description = "更新者")
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
private String updateBy;
@Schema(description = "更新时间", example = "2026-05-18 20:00:00")
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
@Schema(description = "版本")
@Version
private Integer version;
}

View File

@@ -0,0 +1,60 @@
package com.bruce.common.domain.model;
import com.bruce.common.constant.CommonConsts;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RequestResult<T> {
public static final String FAIL_CODE = CommonConsts.REQUEST_RESULT_FAIL_CODE;
public static final String SUCCESS_CODE = CommonConsts.REQUEST_RESULT_SUCCESS_CODE;
@Schema(description = "错误消息")
private String message;
@Schema(description = "消息代码")
private String resultcode;
@Schema(description = "数据")
private T data;
public RequestResult(T data) {
this.resultcode = SUCCESS_CODE;
this.data = data;
}
public static <T> RequestResult<T> success() {
return build(SUCCESS_CODE, null, null);
}
public static <T> RequestResult<T> success(T data) {
return build(SUCCESS_CODE, null, data);
}
public static <T> RequestResult<T> success(String message, T data) {
return build(SUCCESS_CODE, message, data);
}
public static <T> RequestResult<T> fail(String message) {
return build(FAIL_CODE, message, null);
}
public static <T> RequestResult<T> fail(String resultcode, String message) {
return build(resultcode, message, null);
}
private static <T> RequestResult<T> build(String resultcode, String message, T data) {
RequestResult<T> result = new RequestResult<>();
result.setResultcode(resultcode);
result.setMessage(message);
result.setData(data);
return result;
}
}

View File

@@ -0,0 +1,19 @@
package com.bruce.common.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
@Data
@Schema(description = "附件上传请求")
public class SysAttachmentUploadRequest {
@Schema(description = "上传文件")
private MultipartFile file;
@Schema(description = "来源类型")
private String sourceType;
@Schema(description = "来源业务ID")
private Long sourceId;
}

View File

@@ -0,0 +1,40 @@
package com.bruce.common.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
@Schema(description = "系统枚举批量保存请求")
public class SysEnumBatchSaveRequest {
@Schema(description = "模块目录")
private String catalog;
@Schema(description = "枚举类型")
private String type;
@Schema(description = "枚举项")
private List<Item> items;
@Data
@Schema(description = "系统枚举批量保存项")
public static class Item {
@Schema(description = "枚举名称")
private String name;
@Schema(description = "整型值")
private Integer value;
@Schema(description = "字符串值")
private String strvalue;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "备注")
private String remark;
}
}

View File

@@ -0,0 +1,18 @@
package com.bruce.common.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "系统枚举管理查询请求")
public class SysEnumManageQueryRequest {
@Schema(description = "模块目录")
private String catalog;
@Schema(description = "枚举类型")
private String type;
@Schema(description = "关键字,匹配目录、类型、名称、字符串值或备注")
private String keyword;
}

View File

@@ -0,0 +1,15 @@
package com.bruce.common.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "系统枚举查询请求")
public class SysEnumQueryRequest {
@Schema(description = "模块目录")
private String catalog;
@Schema(description = "枚举类型")
private String type;
}

View File

@@ -0,0 +1,33 @@
package com.bruce.common.dto.request;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "系统枚举保存请求")
public class SysEnumSaveRequest {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "模块目录")
private String catalog;
@Schema(description = "枚举类型")
private String type;
@Schema(description = "枚举名称")
private String name;
@Schema(description = "整型值")
private Integer value;
@Schema(description = "字符串值")
private String strvalue;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,42 @@
package com.bruce.common.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "系统附件响应")
public class SysAttachmentResponse {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "来源业务类型")
private String sourceType;
@Schema(description = "来源业务ID")
private Long sourceId;
@Schema(description = "原始文件名")
private String originalName;
@Schema(description = "存储文件名")
private String fileName;
@Schema(description = "文件后缀")
private String fileSuffix;
@Schema(description = "文件MIME类型")
private String contentType;
@Schema(description = "文件大小(字节)")
private Long fileSize;
@Schema(description = "存储类型")
private String storageType;
@Schema(description = "文件访问地址")
private String fileUrl;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,33 @@
package com.bruce.common.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "系统枚举响应")
public class SysEnumResponse {
@Schema(description = "主键ID")
private Long id;
@Schema(description = "模块目录")
private String catalog;
@Schema(description = "枚举类型")
private String type;
@Schema(description = "枚举名称")
private String name;
@Schema(description = "整型值")
private Integer value;
@Schema(description = "字符串值")
private String strvalue;
@Schema(description = "排序")
private Integer sort;
@Schema(description = "备注")
private String remark;
}

View File

@@ -0,0 +1,41 @@
package com.bruce.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum CommonStatusEnum implements PersistableSysEnumDefinition {
DISABLED(0, "禁用"),
ENABLED(1, "启用"),
DRAFT(2, "草稿"),
PROCESSING(3, "处理中"),
COMPLETED(4, "已完成"),
FAILED(5, "失败");
private static final String CATALOG = "common";
private static final String TYPE = "common_status";
private static final String REMARK = "通用状态";
private final Integer value;
private final String label;
@Override
public String getCatalog() {
return CATALOG;
}
@Override
public String getType() {
return TYPE;
}
@Override
public String getRemark() {
return REMARK;
}
}

View File

@@ -0,0 +1,37 @@
package com.bruce.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum EnableStatusEnum implements PersistableSysEnumDefinition {
DISABLED(0, "禁用"),
ENABLED(1, "启用");
private static final String CATALOG = "common";
private static final String TYPE = "enable_status";
private static final String REMARK = "通用启用状态";
private final Integer value;
private final String label;
@Override
public String getCatalog() {
return CATALOG;
}
@Override
public String getType() {
return TYPE;
}
@Override
public String getRemark() {
return REMARK;
}
}

View File

@@ -0,0 +1,56 @@
package com.bruce.common.enums;
/**
* 可同步到 sys_enum 的枚举定义契约。
* <p>
* 长期固定的结构化文本字段统一通过该契约描述,
* 便于前后端传值、数据库初始化和后续协作保持同一事实来源。
*/
public interface PersistableSysEnumDefinition {
/**
* 枚举所属模块目录,例如 common、rag。
*/
String getCatalog();
/**
* 枚举所属类型,同一 catalog/type 视为同一个枚举组。
*/
String getType();
/**
* 落库到 sys_enum.name 的展示名称。
*/
default String getName() {
return getLabel();
}
/**
* 枚举整型值,前后端协议统一传该值。
*/
Integer getValue();
/**
* 可选的字符串值,当前大多数业务枚举不使用。
*/
default String getStrvalue() {
return null;
}
/**
* 排序值,默认与枚举值保持一致。
*/
default Integer getSort() {
return getValue();
}
/**
* sys_enum.remark 中的说明文本。
*/
String getRemark();
/**
* 枚举显示文案,通常与 name 保持一致。
*/
String getLabel();
}

View File

@@ -0,0 +1,22 @@
package com.bruce.common.factory;
import com.bruce.common.domain.entity.SysAttachment;
import com.bruce.common.dto.response.SysAttachmentResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
/**
* 系统附件工厂,集中处理附件元数据出参转换。
*/
@Component
public class SysAttachmentFactory {
public SysAttachmentResponse toResponse(SysAttachment entity) {
if (entity == null) {
return null;
}
SysAttachmentResponse response = new SysAttachmentResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
}

View File

@@ -0,0 +1,63 @@
package com.bruce.common.factory;
import com.bruce.common.domain.entity.SysEnum;
import com.bruce.common.dto.request.SysEnumBatchSaveRequest;
import com.bruce.common.dto.request.SysEnumSaveRequest;
import com.bruce.common.dto.response.SysEnumResponse;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 系统枚举工厂,统一负责请求对象、实体对象和响应对象之间的转换。
*/
@Component
public class SysEnumFactory {
/**
* 将保存请求转换为实体,避免在服务层散落字段拷贝逻辑。
*/
public SysEnum toEntity(SysEnumSaveRequest request) {
if (request == null) {
return null;
}
SysEnum entity = new SysEnum();
BeanUtils.copyProperties(request, entity);
return entity;
}
/**
* 将批量请求中的单个枚举项转换为实体catalog/type 由外层分组统一提供。
*/
public SysEnum toEntity(String catalog, String type, SysEnumBatchSaveRequest.Item item) {
if (item == null) {
return null;
}
SysEnum entity = new SysEnum();
entity.setCatalog(catalog);
entity.setType(type);
entity.setName(item.getName());
entity.setValue(item.getValue());
entity.setStrvalue(item.getStrvalue());
entity.setSort(item.getSort());
entity.setRemark(item.getRemark());
return entity;
}
/**
* 将实体转换为返回对象,保持接口层不直接暴露实体。
*/
public SysEnumResponse toResponse(SysEnum entity) {
if (entity == null) {
return null;
}
SysEnumResponse response = new SysEnumResponse();
BeanUtils.copyProperties(entity, response);
return response;
}
public List<SysEnumResponse> toResponses(List<SysEnum> entities) {
return entities == null ? List.of() : entities.stream().map(this::toResponse).toList();
}
}

View File

@@ -0,0 +1,30 @@
package com.bruce.common.handler;
import com.bruce.common.domain.model.RequestResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<RequestResult<Void>> handleIllegalArgumentException(IllegalArgumentException exception) {
log.warn("GlobalExceptionHandler.handleIllegalArgumentException, message={}", exception.getMessage(), exception);
return buildResponse(HttpStatus.BAD_REQUEST, exception.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<RequestResult<Void>> handleException(Exception exception) {
log.error("GlobalExceptionHandler.handleException", exception);
return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, "系统内部错误,请稍后重试");
}
private ResponseEntity<RequestResult<Void>> buildResponse(HttpStatus status, String message) {
return ResponseEntity.status(status)
.body(RequestResult.fail(String.valueOf(status.value()), message));
}
}

View File

@@ -0,0 +1,9 @@
package com.bruce.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.common.domain.entity.SysAttachment;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysAttachmentMapper extends BaseMapper<SysAttachment> {
}

View File

@@ -0,0 +1,9 @@
package com.bruce.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bruce.common.domain.entity.SysEnum;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SysEnumMapper extends BaseMapper<SysEnum> {
}

View File

@@ -0,0 +1,10 @@
package com.bruce.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.common.domain.entity.SysAttachment;
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
public interface ISysAttachmentService extends IService<SysAttachment> {
SysAttachment upload(SysAttachmentUploadRequest request);
}

View File

@@ -0,0 +1,30 @@
package com.bruce.common.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bruce.common.domain.entity.SysEnum;
import com.bruce.common.dto.request.SysEnumBatchSaveRequest;
import com.bruce.common.dto.request.SysEnumManageQueryRequest;
import com.bruce.common.dto.request.SysEnumQueryRequest;
import com.bruce.common.dto.request.SysEnumSaveRequest;
import com.bruce.common.dto.response.SysEnumResponse;
import java.util.List;
public interface ISysEnumService extends IService<SysEnum> {
List<SysEnum> listByCatalogAndType(SysEnumQueryRequest request);
List<SysEnumResponse> listResponses();
List<SysEnumResponse> listByCatalogAndTypeResponses(SysEnumQueryRequest request);
List<SysEnumResponse> listForManagement(SysEnumManageQueryRequest request);
SysEnumResponse getResponseById(Long id);
boolean saveOrUpdate(SysEnumSaveRequest request);
boolean batchSave(SysEnumBatchSaveRequest request);
boolean replaceByCatalogAndType(String catalog, String type, List<SysEnum> items);
}

View File

@@ -0,0 +1,74 @@
package com.bruce.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.common.config.AttachmentProperties;
import com.bruce.common.constant.CommonConsts;
import com.bruce.common.domain.entity.SysAttachment;
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
import com.bruce.common.mapper.SysAttachmentMapper;
import com.bruce.common.service.ISysAttachmentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@Service
public class SysAttachmentServiceImpl extends ServiceImpl<SysAttachmentMapper, SysAttachment> implements ISysAttachmentService {
@Autowired
private AttachmentProperties attachmentProperties;
@Override
public SysAttachment upload(SysAttachmentUploadRequest request) {
// 这里先完成上传校验和本地落盘,后续如接入对象存储也只需替换该流程内部实现。
if (request == null) {
throw new IllegalArgumentException("上传请求不能为空");
}
MultipartFile file = request.getFile();
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
String sourceType = request.getSourceType();
if (!StringUtils.hasText(sourceType)) {
throw new IllegalArgumentException("sourceType不能为空");
}
String originalName = file.getOriginalFilename();
String suffix = StringUtils.getFilenameExtension(originalName);
String storedFileName = UUID.randomUUID() + (StringUtils.hasText(suffix) ? "." + suffix : "");
String dateDirectory = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
Path rootPath = Paths.get(attachmentProperties.getBasePath()).toAbsolutePath().normalize();
Path targetDirectory = rootPath.resolve(dateDirectory);
Path targetPath = targetDirectory.resolve(storedFileName);
try {
Files.createDirectories(targetDirectory);
file.transferTo(targetPath);
} catch (IOException e) {
throw new IllegalStateException("文件上传失败", e);
}
SysAttachment attachment = new SysAttachment();
attachment.setSourceType(sourceType);
attachment.setSourceId(request.getSourceId());
attachment.setOriginalName(originalName);
attachment.setFileName(storedFileName);
attachment.setFileSuffix(suffix);
attachment.setContentType(file.getContentType());
attachment.setFileSize(file.getSize());
attachment.setStorageType(CommonConsts.STORAGE_TYPE_LOCAL);
attachment.setFilePath(dateDirectory + "/" + storedFileName);
attachment.setFileUrl(null);
attachment.setVersion(1);
save(attachment);
return attachment;
}
}

View File

@@ -0,0 +1,238 @@
package com.bruce.common.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bruce.common.domain.entity.SysEnum;
import com.bruce.common.dto.request.SysEnumBatchSaveRequest;
import com.bruce.common.dto.request.SysEnumManageQueryRequest;
import com.bruce.common.dto.request.SysEnumQueryRequest;
import com.bruce.common.dto.request.SysEnumSaveRequest;
import com.bruce.common.dto.response.SysEnumResponse;
import com.bruce.common.factory.SysEnumFactory;
import com.bruce.common.mapper.SysEnumMapper;
import com.bruce.common.service.ISysEnumService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Slf4j
@Service
@RequiredArgsConstructor
public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> implements ISysEnumService {
private final SysEnumFactory sysEnumFactory;
@Override
public List<SysEnum> listByCatalogAndType(SysEnumQueryRequest request) {
log.info("SysEnumServiceImpl.listByCatalogAndType start, request={}", request);
if (request == null) {
throw new IllegalArgumentException("查询请求不能为空");
}
List<SysEnum> result = lambdaQuery()
.eq(StringUtils.hasText(request.getCatalog()), SysEnum::getCatalog, request.getCatalog())
.eq(StringUtils.hasText(request.getType()), SysEnum::getType, request.getType())
.orderByAsc(SysEnum::getSort)
.list();
log.info("SysEnumServiceImpl.listByCatalogAndType success, count={}", result.size());
return result;
}
@Override
public List<SysEnumResponse> listResponses() {
log.info("查询系统枚举列表开始");
List<SysEnumResponse> responses = sysEnumFactory.toResponses(list());
log.info("查询系统枚举列表结束count={}", responses.size());
return responses;
}
@Override
public List<SysEnumResponse> listByCatalogAndTypeResponses(SysEnumQueryRequest request) {
log.info("按模块和类型查询系统枚举开始catalog={}, type={}",
request == null ? null : request.getCatalog(),
request == null ? null : request.getType());
List<SysEnumResponse> responses = sysEnumFactory.toResponses(listByCatalogAndType(request));
log.info("按模块和类型查询系统枚举结束count={}", responses.size());
return responses;
}
@Override
public List<SysEnumResponse> listForManagement(SysEnumManageQueryRequest request) {
log.info("SysEnumServiceImpl.listForManagement start, request={}", request);
SysEnumManageQueryRequest queryRequest = request == null ? new SysEnumManageQueryRequest() : request;
String keyword = queryRequest.getKeyword();
List<SysEnumResponse> responses = sysEnumFactory.toResponses(lambdaQuery()
.eq(StringUtils.hasText(queryRequest.getCatalog()), SysEnum::getCatalog, queryRequest.getCatalog())
.eq(StringUtils.hasText(queryRequest.getType()), SysEnum::getType, queryRequest.getType())
.and(StringUtils.hasText(keyword), wrapper -> wrapper
.like(SysEnum::getCatalog, keyword)
.or()
.like(SysEnum::getType, keyword)
.or()
.like(SysEnum::getName, keyword)
.or()
.like(SysEnum::getStrvalue, keyword)
.or()
.like(SysEnum::getRemark, keyword))
.orderByAsc(SysEnum::getCatalog)
.orderByAsc(SysEnum::getType)
.orderByAsc(SysEnum::getSort)
.orderByAsc(SysEnum::getId)
.list());
log.info("SysEnumServiceImpl.listForManagement success, count={}", responses.size());
return responses;
}
@Override
public SysEnumResponse getResponseById(Long id) {
log.info("查询系统枚举详情开始id={}", id);
SysEnumResponse response = sysEnumFactory.toResponse(getById(id));
log.info("查询系统枚举详情结束id={}, found={}", id, response != null);
return response;
}
@Override
public boolean saveOrUpdate(SysEnumSaveRequest request) {
log.info("保存系统枚举开始id={}, catalog={}, type={}, value={}",
request == null ? null : request.getId(),
request == null ? null : request.getCatalog(),
request == null ? null : request.getType(),
request == null ? null : request.getValue());
if (request == null) {
throw new IllegalArgumentException("保存请求不能为空");
}
SysEnum sysEnum = sysEnumFactory.toEntity(request);
boolean result = super.saveOrUpdate(sysEnum);
log.info("保存系统枚举结束id={}, catalog={}, type={}, value={}, result={}",
request.getId(), request.getCatalog(), request.getType(), request.getValue(), result);
return result;
}
@Override
public boolean batchSave(SysEnumBatchSaveRequest request) {
log.info("批量保存系统枚举开始catalog={}, type={}",
request == null ? null : request.getCatalog(),
request == null ? null : request.getType());
List<SysEnum> existingEnums = lambdaQuery()
.eq(request != null && StringUtils.hasText(request.getCatalog()), SysEnum::getCatalog, request == null ? null : request.getCatalog())
.eq(request != null && StringUtils.hasText(request.getType()), SysEnum::getType, request == null ? null : request.getType())
.list();
validateBatchSaveRequest(request, existingEnums);
List<SysEnum> enums = request.getItems().stream()
.map(item -> sysEnumFactory.toEntity(request.getCatalog(), request.getType(), item))
.toList();
boolean result = saveBatch(enums);
log.info("批量保存系统枚举结束catalog={}, type={}, itemCount={}, result={}",
request.getCatalog(), request.getType(), enums.size(), result);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean replaceByCatalogAndType(String catalog, String type, List<SysEnum> items) {
log.info("SysEnumServiceImpl.replaceByCatalogAndType start, catalog={}, type={}, itemCount={}",
catalog, type, items == null ? 0 : items.size());
validateReplaceByCatalogAndTypeRequest(catalog, type, items);
boolean removed = lambdaUpdate()
.eq(SysEnum::getCatalog, catalog)
.eq(SysEnum::getType, type)
.remove();
boolean saved = saveBatch(items);
boolean result = removed || saved;
log.info("SysEnumServiceImpl.replaceByCatalogAndType success, catalog={}, type={}, removed={}, saved={}, result={}",
catalog, type, removed, saved, result);
return result;
}
public void validateBatchSaveRequest(SysEnumBatchSaveRequest request, List<SysEnum> existingEnums) {
log.info("SysEnumServiceImpl.validateBatchSaveRequest start");
if (request == null) {
throw new IllegalArgumentException("批量保存请求不能为空");
}
if (!StringUtils.hasText(request.getCatalog())) {
throw new IllegalArgumentException("模块目录不能为空");
}
if (!StringUtils.hasText(request.getType())) {
throw new IllegalArgumentException("枚举类型不能为空");
}
if (request.getItems() == null || request.getItems().isEmpty()) {
throw new IllegalArgumentException("枚举项不能为空");
}
Set<Integer> requestValues = new HashSet<>();
for (SysEnumBatchSaveRequest.Item item : request.getItems()) {
if (item == null) {
throw new IllegalArgumentException("枚举项不能为空");
}
if (!StringUtils.hasText(item.getName())) {
throw new IllegalArgumentException("枚举名称不能为空");
}
if (item.getValue() == null) {
throw new IllegalArgumentException("枚举整型值不能为空");
}
if (!requestValues.add(item.getValue())) {
throw new IllegalArgumentException("批量保存请求中存在重复枚举值: " + item.getValue());
}
}
Set<Integer> existingValues = new HashSet<>();
if (existingEnums != null) {
existingEnums.stream()
.map(SysEnum::getValue)
.forEach(existingValues::add);
}
for (Integer value : requestValues) {
if (existingValues.contains(value)) {
throw new IllegalArgumentException("枚举值已存在: " + value);
}
}
log.info("SysEnumServiceImpl.validateBatchSaveRequest success, catalog={}, type={}, requestValueCount={}, existingValueCount={}",
request.getCatalog(), request.getType(), requestValues.size(), existingValues.size());
}
private void validateReplaceByCatalogAndTypeRequest(String catalog, String type, List<SysEnum> items) {
if (!StringUtils.hasText(catalog)) {
throw new IllegalArgumentException("模块目录不能为空");
}
if (!StringUtils.hasText(type)) {
throw new IllegalArgumentException("枚举类型不能为空");
}
if (items == null || items.isEmpty()) {
throw new IllegalArgumentException("枚举项不能为空");
}
Set<Integer> values = new HashSet<>();
Set<Integer> sorts = new HashSet<>();
for (SysEnum item : items) {
if (item == null) {
throw new IllegalArgumentException("枚举项不能为空");
}
if (!catalog.equals(item.getCatalog()) || !type.equals(item.getType())) {
throw new IllegalArgumentException("替换的枚举项 catalog/type 必须与目标分组一致");
}
if (!StringUtils.hasText(item.getName())) {
throw new IllegalArgumentException("枚举名称不能为空");
}
if (item.getValue() == null) {
throw new IllegalArgumentException("枚举整型值不能为空");
}
if (!values.add(item.getValue())) {
throw new IllegalArgumentException("枚举值重复: " + item.getValue());
}
if (item.getSort() == null) {
throw new IllegalArgumentException("枚举排序不能为空");
}
if (!sorts.add(item.getSort())) {
throw new IllegalArgumentException("枚举排序重复: " + item.getSort());
}
}
}
}

View File

@@ -0,0 +1,33 @@
package com.bruce.common.typehandler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
public class PgJsonbStringTypeHandler extends BaseTypeHandler<String> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
ps.setObject(i, parameter, Types.OTHER);
}
@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return rs.getString(columnName);
}
@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.getString(columnIndex);
}
@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return cs.getString(columnIndex);
}
}