diff --git a/backend/src/main/java/com/yfd/platform/common/utils/Base64MultipartFile.java b/backend/src/main/java/com/yfd/platform/common/utils/Base64MultipartFile.java new file mode 100644 index 00000000..eca8101d --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/common/utils/Base64MultipartFile.java @@ -0,0 +1,69 @@ +package com.yfd.platform.common.utils; + +import org.springframework.web.multipart.MultipartFile; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Base64 转 MultipartFile 的适配器实现 + * 用于将前端传来的 Base64 字符串伪装成标准文件对象 + */ +public class Base64MultipartFile implements MultipartFile { + + private final byte[] content; + private final String name; + private final String originalFilename; + private final String contentType; + + public Base64MultipartFile(byte[] content, String name, String originalFilename, String contentType) { + this.content = content; + this.name = name; + this.originalFilename = originalFilename; + this.contentType = contentType; + } + + @Override + public String getName() { + return name; + } + + @Override + public String getOriginalFilename() { + return originalFilename; + } + + @Override + public String getContentType() { + return contentType; + } + + @Override + public boolean isEmpty() { + return content == null || content.length == 0; + } + + @Override + public long getSize() { + return content != null ? content.length : 0; + } + + @Override + public byte[] getBytes() throws IOException { + return content; + } + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(content); + } + + @Override + public void transferTo(File dest) throws IOException, IllegalStateException { + try (FileOutputStream fos = new FileOutputStream(dest)) { + fos.write(content); + } + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java index 3fbb007d..185ecce8 100644 --- a/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SecurityConfig.java @@ -84,6 +84,7 @@ public class SecurityConfig { // .requestMatchers("/mapLegend/**").permitAll() // .requestMatchers("/base/msalongb/**").permitAll() // .requestMatchers("/base/msalongdetb/**").permitAll() +// .requestMatchers("/smartImage/**").permitAll() .requestMatchers("/sms/**").permitAll() .requestMatchers(HttpMethod.GET, "/").permitAll() .requestMatchers(HttpMethod.GET, diff --git a/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java b/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java index 13328298..8e63f008 100644 --- a/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java +++ b/backend/src/main/java/com/yfd/platform/config/SwaggerConfig.java @@ -202,6 +202,14 @@ public class SwaggerConfig { .build(); } + @Bean + public GroupedOpenApi groupEnvSmartImageApi() { + return GroupedOpenApi.builder() + .group("3.11 智能配图-首页(si)") + .packagesToScan("com.yfd.platform.qgc_env.si.controller") + .build(); + } + diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/si/controller/SmartImageController.java b/backend/src/main/java/com/yfd/platform/qgc_env/si/controller/SmartImageController.java new file mode 100644 index 00000000..6cce6357 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/si/controller/SmartImageController.java @@ -0,0 +1,98 @@ +package com.yfd.platform.qgc_env.si.controller; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.yfd.platform.common.DataSourceRequest; +import com.yfd.platform.common.exception.BizException; +import com.yfd.platform.common.utils.UserNameFillHelper; +import com.yfd.platform.config.ResponseResult; +import com.yfd.platform.qgc_env.si.entity.vo.SmartImageVo; + +import com.yfd.platform.qgc_env.si.service.ISmartImageService; +import com.yfd.platform.utils.DataSourceRequestUtil; +import com.yfd.platform.utils.DictCodeToNameConverter; +import com.yfd.platform.utils.SecurityUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.annotation.Resource; +import org.springframework.web.bind.annotation.*; + +import java.util.Date; +import java.util.List; + +/** + * 智能配图控制器 + * + * @author migration + */ +@RestController +@RequestMapping("/smartImage") +@Tag(name = "智能配图控制器") +public class SmartImageController { + + @Resource + private ISmartImageService smartImageService; + + @Resource + private UserNameFillHelper userNameFillHelper; + + @Resource + private DictCodeToNameConverter dictCodeToNameConverter; + /** + * 条件过滤数据列表 + */ + @Operation(summary = "条件过滤数据列表") + @PostMapping("/GetKendoList") + public ResponseResult getKendoList( @RequestBody DataSourceRequest request) { + Page pushConfigPage = DataSourceRequestUtil.executeQuery(request, SmartImageVo.class, smartImageService); + userNameFillHelper.fillUserNames(pushConfigPage.getRecords()); + return ResponseResult.successData(pushConfigPage); + + } + + + /** + * 新增或修改 + */ + @Operation(summary = "新增或修改") + @PostMapping("/save") + public ResponseResult save(@RequestBody SmartImageVo smartImage) { + if (smartImage == null) { + return ResponseResult.error("智能配图信息不能为空."); + } + if (StrUtil.isBlank(smartImage.getPeituName())) { + throw new BizException("配图名称(peituName)不能为空."); + } + if (StrUtil.isBlank(smartImage.getConfig())) { + throw new BizException("配图配置(config)不能为空."); + } + + // 新增时设置创建信息 + if (StrUtil.isBlank(smartImage.getId())) { + smartImage.setCreatedAt(new Date()); + smartImage.setRecordUser(SecurityUtils.getUserId()); + smartImage.setIsDeleted(0); + } + + // 设置操作人、操作时间、更新信息 + smartImage.setOperator(SecurityUtils.getUserId()); + smartImage.setOperateTime(new Date()); + smartImage.setModifyUser(SecurityUtils.getUserId()); + smartImage.setModifyTime(new Date()); + + + smartImageService.saveWithAttachment(smartImage); + return ResponseResult.successData(smartImage.getId()); + } + + + + /** + * 删除 + */ + @Operation(summary = "删除") + @PostMapping("/delete") + public ResponseResult delete(@RequestBody List ids) { + smartImageService.deleteByIds(ids); + return ResponseResult.success("删除成功."); + } +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/si/entity/vo/SmartImageVo.java b/backend/src/main/java/com/yfd/platform/qgc_env/si/entity/vo/SmartImageVo.java new file mode 100644 index 00000000..eb744344 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/si/entity/vo/SmartImageVo.java @@ -0,0 +1,99 @@ +package com.yfd.platform.qgc_env.si.entity.vo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.yfd.platform.annotation.UserIdField; +import com.yfd.platform.annotation.UserNameField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; + +/** + * 智能配图表 + * + * @author migration + */ + +@Data +@TableName("SD_SMART_IMAGE") +@Schema(description = "智能配图表") +public class SmartImageVo implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(type = IdType.ASSIGN_UUID) + private String id; + + /** 配图名称 */ + @TableField("PEITU_NAME") + private String peituName; + + /** 操作人,关联SYS_USER.ID */ + @TableField("OPERATOR") + @UserIdField + private String operator; + + /** 操作人名称(非数据库字段) */ + @TableField(exist = false) + @UserNameField + private String operatorName; + + /** 配图操作时间 */ + @TableField("OPERATE_TIME") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date operateTime; + + /** 配图配置,存储JSON格式长文本 */ + @TableField("CONFIG") + private String config; + + /** 图片在服务器存储的KEY */ + @TableField("IMAGE_KEY") + private String imageKey; + + /** 创建时间 */ + @TableField("CREATED_AT") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createdAt; + + /** 创建人,关联SYS_USER.ID */ + @TableField("RECORD_USER") + private String recordUser; + + /** 创建人名称(非数据库字段) */ + @TableField(exist = false) + private String recordUserName; + + /** 更新人,关联SYS_USER.ID */ + @TableField("MODIFY_USER") + private String modifyUser; + + /** 更新时间 */ + @TableField("MODIFY_TIME") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date modifyTime; + + /** 是否已删除:0=未删除,1=已删除 */ + @TableField("IS_DELETED") + private Integer isDeleted; + + /** 删除人,关联SYS_USER.ID */ + @TableField("DELETE_USER") + private String deleteUser; + + /** 删除时间 */ + @TableField("DELETE_TIME") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date deleteTime; + + @TableField(exist = false) + private String bsfile; + + @TableField(exist = false) + private String fileName; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/si/mapper/SmartImageMapper.java b/backend/src/main/java/com/yfd/platform/qgc_env/si/mapper/SmartImageMapper.java new file mode 100644 index 00000000..b5ff1f47 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/si/mapper/SmartImageMapper.java @@ -0,0 +1,10 @@ +package com.yfd.platform.qgc_env.si.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.yfd.platform.qgc_env.si.entity.vo.SmartImageVo; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmartImageMapper extends BaseMapper { + +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/si/service/ISmartImageService.java b/backend/src/main/java/com/yfd/platform/qgc_env/si/service/ISmartImageService.java new file mode 100644 index 00000000..d74dbe02 --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/si/service/ISmartImageService.java @@ -0,0 +1,39 @@ +package com.yfd.platform.qgc_env.si.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.yfd.platform.common.DataSourceRequest; +import com.yfd.platform.common.DataSourceResult; +import com.yfd.platform.qgc_env.si.entity.vo.SmartImageVo; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +/** + * 智能配图表 Service 接口 + * + * @author migration + */ +public interface ISmartImageService extends IService { + + /** + * 条件过滤数据列表(Kendo 数据源) + * + * @param dataSourceRequest 数据源请求 + * @return 数据源结果 + */ + DataSourceResult getKendoList(DataSourceRequest dataSourceRequest); + + /** + * 批量删除 + * + * @param ids 主键ID列表 + */ + void deleteByIds(List ids); + + /** + * 保存或更新(含附件上传) + * + * @param smartImage 智能配图实体 + */ + void saveWithAttachment(SmartImageVo smartImage); +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/si/service/impl/SmartImageServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/si/service/impl/SmartImageServiceImpl.java new file mode 100644 index 00000000..9e55aabc --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/si/service/impl/SmartImageServiceImpl.java @@ -0,0 +1,207 @@ +package com.yfd.platform.qgc_env.si.service.impl; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.yfd.platform.common.DataSourceLoadOptionsBase; +import com.yfd.platform.common.DataSourceRequest; +import com.yfd.platform.common.DataSourceResult; +import com.yfd.platform.common.exception.BizException; +import com.yfd.platform.common.utils.Base64MultipartFile; +import com.yfd.platform.qgc_data.service.AttachmentUploadService; +import com.yfd.platform.qgc_env.si.entity.vo.SmartImageVo; +import com.yfd.platform.qgc_env.si.mapper.SmartImageMapper; +import com.yfd.platform.qgc_env.si.service.ISmartImageService; +import com.yfd.platform.utils.DataSourceRequestUtil; +import com.yfd.platform.utils.QgcQueryWrapperUtil; +import jakarta.annotation.Resource; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; + +/** + * 智能配图表 Service 实现类 + * + * @author migration + */ +@Service +@Slf4j +public class SmartImageServiceImpl extends ServiceImpl implements ISmartImageService { + + @Resource + private AttachmentUploadService attachmentUploadService; + @Override + public DataSourceResult getKendoList(DataSourceRequest dataSourceRequest) { + // 1. 构建查询条件 + QueryWrapper wrapper = DataSourceRequestUtil.buildQueryWrapper( + dataSourceRequest, SmartImageVo.class); + + // 默认按创建时间倒序 + wrapper.orderByDesc("CREATED_AT"); + + // 2. 分页参数处理 + DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest(); + Page page = loadOptions == null ? null + : (Page) QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake()); + + // 3. 执行查询 + Page resultPage; + if (page != null) { + resultPage = this.page(page, wrapper); + } else { + List list = this.list(wrapper); + resultPage = new Page<>(); + resultPage.setRecords(list); + resultPage.setTotal(list.size()); + } + + // 4. 构建结果 + DataSourceResult dataSourceResult = new DataSourceResult<>(); + dataSourceResult.setData(resultPage.getRecords()); + dataSourceResult.setTotal(resultPage.getTotal()); + return dataSourceResult; + } + + @Override + public void saveWithAttachment(SmartImageVo smartImage) { + // 3. 将 Base64 转换为 MultipartFile + MultipartFile file = null; + if (StrUtil.isNotBlank(smartImage.getBsfile())) { + try { + file = convertBase64ToMultipartFile(smartImage.getBsfile(), smartImage.getFileName()); + } catch (Exception e) { + log.error("Base64 文件转换失败", e); + throw new BizException("文件解析失败,请检查 Base64 格式是否正确"); + } + } + + // 如果有附件,上传并获取 attachmentId + if (file != null || !file.isEmpty()) { + try { + List files = new ArrayList<>(); + files.add(file); + List attachmentIds = attachmentUploadService.uploadMultipartFiles(files); + if (attachmentIds != null && !attachmentIds.isEmpty()) { + //如果是一条 + smartImage.setImageKey(attachmentIds.get(0)); + //如果是多条 + //smartImage.setImageKey(String.valueOf(attachmentIds)); + } + } catch (Exception e) { + log.error("附件上传失败", e); + } + } + this.saveOrUpdate(smartImage); + } + + + /** + * 将 Base64 字符串转换为 MultipartFile + * + * @param base64 Base64 字符串(可带 data:image/png;base64, 前缀) + * @param fileName 文件名(可空) + * @return MultipartFile 对象 + */ + private MultipartFile convertBase64ToMultipartFile(String base64, String fileName) { + try { + // 1. 去除 Base64 前缀(如果有) + String[] parts = base64.split(","); + String base64Data = parts.length > 1 ? parts[1] : parts[0]; + + // 2. 解码为字节数组 + byte[] fileBytes = Base64.getDecoder().decode(base64Data); + + // 3. 处理文件名 + String originalFilename = fileName; + if (StrUtil.isBlank(originalFilename)) { + // 自动检测文件扩展名 + String ext = detectExtension(base64); + originalFilename = System.currentTimeMillis() + (ext != null ? "." + ext : ".bin"); + } + + // 4. 检测 MIME 类型 + String contentType = detectContentType(base64); + if (contentType == null) { + contentType = "application/octet-stream"; + } + + // 5. 返回自定义 MultipartFile 实现 + return new Base64MultipartFile( + fileBytes, + "file", // 表单字段名 + originalFilename, + contentType + ); + + } catch (IllegalArgumentException e) { + log.error("Base64 解码失败,可能数据不合法", e); + throw new BizException("Base64 编码数据格式错误,请检查"); + } catch (Exception e) { + log.error("转换 MultipartFile 失败", e); + throw new BizException("文件转换失败:" + e.getMessage()); + } + } + + /** + * 根据 Base64 前缀检测文件扩展名 + */ + private String detectExtension(String base64) { + if (base64.startsWith("data:image/jpeg") || base64.startsWith("data:image/jpg")) { + return "jpg"; + } else if (base64.startsWith("data:image/png")) { + return "png"; + } else if (base64.startsWith("data:image/gif")) { + return "gif"; + } else if (base64.startsWith("data:image/bmp")) { + return "bmp"; + } else if (base64.startsWith("data:application/pdf")) { + return "pdf"; + } else if (base64.startsWith("data:application/msword")) { + return "doc"; + } else if (base64.startsWith("data:application/vnd.openxmlformats-officedocument.wordprocessingml.document")) { + return "docx"; + } + return null; // 默认无扩展名 + } + + /** + * 根据 Base64 前缀检测 MIME 类型 + */ + private String detectContentType(String base64) { + if (base64.startsWith("data:image/jpeg") || base64.startsWith("data:image/jpg")) { + return "image/jpeg"; + } else if (base64.startsWith("data:image/png")) { + return "image/png"; + } else if (base64.startsWith("data:image/gif")) { + return "image/gif"; + } else if (base64.startsWith("data:image/bmp")) { + return "image/bmp"; + } else if (base64.startsWith("data:application/pdf")) { + return "application/pdf"; + } else if (base64.startsWith("data:application/msword")) { + return "application/msword"; + } else if (base64.startsWith("data:application/vnd.openxmlformats-officedocument.wordprocessingml.document")) { + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + } + return null; + } + + @Override + public void deleteByIds(List ids) { + List list = this.listByIds(ids); + if (CollectionUtils.isEmpty(list)) { + return; + } + for (SmartImageVo entity : list) { + attachmentUploadService.deleteFile(entity.getImageKey()); + } + for (String id : ids) { + this.removeById(id); + } + } +}