图库项目开发 阶段一-图片模块

【自定义图库】 首先,如果用MFC拖控件的办法,最简单也最方便,用static或者用picture控件,把bmp贴进去就好了,双击控件的时候,关闭 对话框,然后给cad发送命令就可以了,方便,简单。不好的地方在于自己做bmp图是有大有小的,bmp图贴到控件中的时候, 会有缩放,bmp本来就不是质量很高的图形文件,就会出现失真。那么看来最简单的是用sld来做图库了,cad本身也支持这个,搭配mnu菜单文件,就可以很好的实现图库功能,当然,用这 个做图库,最大的问题是需要你自己做一个完美的sld文件才行,不然也是很丑的。 阅读详情

  • 上传创建图片

  • 图片信息编辑(标签/分类等)

  • 管理图片

  • 查看 / 搜索 图片列表

  • 查看图片详情

  • 图片下载

  • 用户上传创建图片

  • 审核图片

  • 导入图片(URL导入,批量抓取和创建)

  • 图片查询优化

  • 图片上传优化

  • 图片加载优化

  • 图片存储

图片模块

一、需求分析

优先确保用户能够查看图片功能的实现,上传功能暂时仅限管理员使用,保证系统的安全和稳定

管理员

  • 图片上传与创建,支持本地图片上传,填写相关信息,名称、简介、标签、分类等。系统自动解析图片的基础信息,便于检索。

  • 图片管理,管理员对图片信息进行编辑,例如修改名称、简洁、标签、分类等。

  • 图片修改

用户

  • 查看与搜索:用户可以按照关键词搜索图片,并支持按分类、标签筛选条件分页查看图片列表。
  • 查看图片详情:用户点击列表中的图片后,可进入详情页,查看图片的大图及相关信息,如名称、简介、分类、标签、其他图片信息(如宽高和格式等)。
  • 图片下载:用户在详情页可以点击 下载图片按钮,将图片保存本地。

二、方案设计

文件上传下载的实现

最简单的方式就是上传到后端服务器,用Java自带文件读写API就能实现。但是这种方式存在缺点:

  • 不利于扩展:单个服务器的存储是有限的
  • 不利于迁移
  • 不安全
  • 不利于管理

除了存储一些需要清理的临时文件,通常不会将用户的文件直接上传到服务器,更推荐用专业的第三方存储服务,最常用的就是对象存储。

对象存储

对象存储是一种存储海量文件的分布式存储服务,具有高扩展、低成本、可靠安全等优点。

例如Minio,AmazonS3,OSS,COS

创建图片的流程

上传图片文件+补充图片信息并保存到数据库中

  • 先上传再提交数据:用户上传图片,系统生成图片存储的url;然后在用户填写其他相关信息并提交后,才保存图片记录到数据库中。
  • 上传图片时保存记录:用户上传图片后,系统立即生成图片的完整数据记录,包括url和其他元数据,无需等待用户点击提交,图片信息立刻存储数据库,之后用户填写其他信息,相当于编辑了已有图片记录的信息。

第一种方法,如果用户取消提交会有存储残留。

第二种方法比较稳健,可以对图片溯源,并对用户上传做安全保障

解析图片信息

主流获取图片信息的方法主要由2种:

1.在后端服务器直接处理图片,比如Java库ImageIO、Python库pillow,还有更成熟的专业图像处理库OpenCV等。

2.通过第三方服务提取图片元数据。这里我们用腾讯的数据万象

三、后端开发

COS存储桶准备

配置

@Configuration
@ConfigurationProperties(prefix = "cos.client")
@Data
public class CosClientConfig {  
  
    /**  
     * 域名  
     */  
    private String host;  
  
    /**  
     * secretId  
     */  
    private String secretId;  
  
    /**  
     * 密钥(注意不要泄露)  
     */  
    private String secretKey;  
  
    /**  
     * 区域  
     */  
    private String region;  
  
    /**  
     * 桶名  
     */  
    private String bucket;

    @Bean
    public COSClient cosClient() {
        // 1 初始化用户身份信息(secretId, secretKey)。
        // SECRETID 和 SECRETKEY 请登录访问管理控制台 https://console.cloud.tencent.com/cam/capi 进行查看和管理
        COSCredentials cred = new BasicCOSCredentials(secretId, secretKey);
        // 2 设置 bucket 的地域, COS 地域的简称请参见 https://cloud.tencent.com/document/product/436/6224
        // clientConfig 中包含了设置 region, https(默认 http), 超时, 代理等 set 方法, 使用可参见源码或者常见问题 Java SDK 部分。
        ClientConfig clientConfig = new ClientConfig(new Region(region));
        // 这里建议设置使用 https 协议
        // 从 5.6.54 版本开始,默认使用了 https
        clientConfig.setHttpProtocol(HttpProtocol.https);
        // 3 生成 cos 客户端。
        return new COSClient(cred, clientConfig);
    }
}
# 对象存储配置(需要从腾讯云获取)  
cos:  
  client:  
    host: xxx  
    secretId: xxx  
    secretKey: xxx  
    region: xxx  
    bucket: xxx

通用类 manager - CosManager

@Component  
public class CosManager {  
  
    @Resource  
    private CosClientConfig cosClientConfig;  
  
    @Resource  
    private COSClient cosClient;  
  
    // ... 一些操作 COS 的方法  
}

文件上传

public PutObjectResult putObject(String key,File file){
	PutObjectResult putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(),key,file);
    return cosClient.putObject(putObjectRequest);
}


/**  
 * 上传对象(附带图片信息)  
 *  
 * @param key  唯一键  
 * @param file 文件  
 */  
public PutObjectResult putPictureObject(String key, File file) {  
    PutObjectRequest putObjectRequest = new PutObjectRequest(cosClientConfig.getBucket(), key,  
            file);  
    // 对图片进行处理(获取基本信息也被视作为一种处理)  
    PicOperations picOperations = new PicOperations();  
    // 1 表示返回原图信息  
    picOperations.setIsPicInfo(1);  
    // 构造处理参数  
    putObjectRequest.setPicOperations(picOperations);  
    return cosClient.putObject(putObjectRequest);  
}

文件上传测试


@PostMapping("/test/upload")
public BaseResponse<String> testUploadFile(@RequestPart("file")MutipartFile multipartFile){
    //dir
    String filename = multipart.getOriginalFilename();
    String filepath = String.format("/test/%s",filename);
    File file = null;
    try{
        file = File.createTempFile(filepath,null);
        mutipartFile.transferTo(file);
        cosManager.putObject(filepath,file);
        return ResultUtils.success(filepath);
    }catch(Exception e){
        throw
    }finally{
        if(file!=null){
           file.delete()
        }
    }
    
}

文件下载

COS介绍了2种文件下载方式。一种是直接下载COS文件到后端服务器(适合服务端处理文件),另一种是获取文件下载输入流(适合返回给前端用户)。

还可以通过URL路径访问,适用于单一的、可公开的资源,如用户头像,公开图片。

对于安全性要求高的场景,可以先通过后端进行权限校验,然后从COS下载文件到服务器,再返回给前端,这样可以在后端限制只有登录用户才能下载。

也可以后端先进行权限校验,然后返回给前端一个临时密钥,之后前端可以凭借该密钥直接从对象存储下载,不用经过服务端中转,性能更高。

当前的图片都是公开的,直接通过URL进行访问即可。(这里的负载就交给COS了)

服务端文件下载代码demo

/**  
 * 下载对象  
 *  
 * @param key 唯一键  
 */  
public COSObject getObject(String key) {  
    GetObjectRequest getObjectRequest = new GetObjectRequest(cosClientConfig.getBucket(), key);  
    return cosClient.getObject(getObjectRequest);  
}

@GetMapping("/test/download")
public void testDownloadFile(String filepath,HttpServletResponse response)
{
    COSObjectInputStream cosObjectInput = null;
    try{
        cosManager.getObject(filepath).var;
     cosObjectInput =   cosObject.getObjectContent();
        IOUtils.toByteArray(cosObjectInput).var;
        response.setContentType();
        response.setHeader();
        response.getOutPutStream().write(bytes);
        response.getOutPutStream().flush();
        
    }catch{
        
    }finally{
	if(cosObjectInput!=null){
		cosObjectInput.close();
    }
    }
}

picture table 逆向工程

优化picture 实体类

@TableName(value ="picture")  
@Data  
public class Picture implements Serializable {  
    /**  
     * id  
     */  
    @TableId(type = IdType.ASSIGN_ID)  
    private Long id;  
  
    /**  
     * 图片 url  
     */  
    private String url;  
  
    /**  
     * 图片名称  
     */  
    private String name;  
  
    /**  
     * 简介  
     */  
    private String introduction;  
  
    /**  
     * 分类  
     */  
    private String category;  
  
    /**  
     * 标签(JSON 数组)  
     */  
    private String tags;  
  
    /**  
     * 图片体积  
     */  
    private Long picSize;  
  
    /**  
     * 图片宽度  
     */  
    private Integer picWidth;  
  
    /**  
     * 图片高度  
     */  
    private Integer picHeight;  
  
    /**  
     * 图片宽高比例  
     */  
    private Double picScale;  
  
    /**  
     * 图片格式  
     */  
    private String picFormat;  
  
    /**  
     * 创建用户 id  
     */  
    private Long userId;  
  
    /**  
     * 创建时间  
     */  
    private Date createTime;  
  
    /**  
     * 编辑时间  
     */  
    private Date editTime;  
  
    /**  
     * 更新时间  
     */  
    private Date updateTime;  
  
    /**  
     * 是否删除  
     */  
    @TableLogic  
    private Integer isDelete;  
  
    @TableField(exist = false)  
    private static final long serialVersionUID = 1L;  
}

图片上传

1.数据模型

在dto下新建用于请求参数的类。由于图片需要支持重复上传(信息不变,只改变图片文件),所以要添加图片id参数:

@Data  
public class PictureUploadRequest implements Serializable {  
  
    /**  
     * 图片 id(用于修改)  
     */  
    private Long id;  
  
    private static final long serialVersionUID = 1L;  
}

在vo下新建视图类,可以额外关联用户信息。还可以编写实体类和视图类快速转换方法,便于后续快速传值。

@Data  
public class PictureVO implements Serializable {  
  
    /**  
     * id  
     */  
    private Long id;  
  
    /**  
     * 图片 url  
     */  
    private String url;  
  
    /**  
     * 图片名称  
     */  
    private String name;  
  
    /**  
     * 简介  
     */  
    private String introduction;  
  
    /**  
     * 标签  
     */  
    private List<String> tags;  
  
    /**  
     * 分类  
     */  
    private String category;  
  
    /**  
     * 文件体积  
     */  
    private Long picSize;  
  
    /**  
     * 图片宽度  
     */  
    private Integer picWidth;  
  
    /**  
     * 图片高度  
     */  
    private Integer picHeight;  
  
    /**  
     * 图片比例  
     */  
    private Double picScale;  
  
    /**  
     * 图片格式  
     */  
    private String picFormat;  
  
    /**  
     * 用户 id  
     */  
    private Long userId;  
  
    /**  
     * 创建时间  
     */  
    private Date createTime;  
  
    /**  
     * 编辑时间  
     */  
    private Date editTime;  
  
    /**  
     * 更新时间  
     */  
    private Date updateTime;  
  
    /**  
     * 创建用户信息  
     */  
    private UserVO user;  
  
    private static final long serialVersionUID = 1L;  
  
    /**  
     * 封装类转对象  
     */  
    public static Picture voToObj(PictureVO pictureVO) {  
        if (pictureVO == null) {  
            return null;  
        }  
        Picture picture = new Picture();  
        BeanUtils.copyProperties(pictureVO, picture);  
        // 类型不同,需要转换  
        picture.setTags(JSONUtil.toJsonStr(pictureVO.getTags()));  
        return picture;  
    }  
  
    /**  
     * 对象转封装类  
     */  
    public static PictureVO objToVo(Picture picture) {  
        if (picture == null) {  
            return null;  
        }  
        PictureVO pictureVO = new PictureVO();  
        BeanUtils.copyProperties(picture, pictureVO);  
        // 类型不同,需要转换  
        pictureVO.setTags(JSONUtil.toList(picture.getTags(), String.class));  
        return pictureVO;  
    }  
}

2.通用文件上传服务

虽然以及编写过通用对象存储操作类,但是还不够

问题:

  • 图片校验
  • 上传路径指定
  • 解析图片:数据万象服务

FileManager / FileService

1.由于文件校验规则较复杂,单独抽象为validPicture方法,对文件大小、类型校验。

2.文件上传时,会先在本地创建临时文件,无论是否创建成功,最后都要删除临时文件,否则会导致资源泄露。

3.可以跟自己的需求定义文件上传地址,此处添加了上传日期和UUID随机数,便于了解上传时间并防止文件重复(一般处理)。预留了一个uploadPathPrefix参数,由调用方指定上传文件到哪个目录。

4.不建议存储桶共享

public class FileService{
    @Resource
    private CosClientConfig cosClientConfig;
    
    @Resource
    private CosManager cosManager;
    
    /**  
 * 上传图片  
 *  
 * @param multipartFile    文件  
 * @param uploadPathPrefix 上传路径前缀  
 * @return  
 */  
public UploadPictureResult uploadPicture(MultipartFile multipartFile, String uploadPathPrefix) {  
    // 校验图片  
    validPicture(multipartFile);  
    // 图片上传地址  
    String uuid = RandomUtil.randomString(16);  
    String originFilename = multipartFile.getOriginalFilename();  
    String uploadFilename = String.format("%s_%s.%s", DateUtil.formatDate(new Date()), uuid,  
            FileUtil.getSuffix(originFilename));  
    String uploadPath = String.format("/%s/%s", uploadPathPrefix, uploadFilename);  
    File file = null;  
    try {  
        // 创建临时文件  
        file = File.createTempFile(uploadPath, null);  
        multipartFile.transferTo(file);  
        // 上传图片  
        PutObjectResult putObjectResult = cosManager.putPictureObject(uploadPath, file);  
        ImageInfo imageInfo = putObjectResult.getCiUploadResult().getOriginalInfo().getImageInfo();  
        // 封装返回结果  
        UploadPictureResult uploadPictureResult = new UploadPictureResult();  
        int picWidth = imageInfo.getWidth();  
        int picHeight = imageInfo.getHeight();  
        double picScale = NumberUtil.round(picWidth * 1.0 / picHeight, 2).doubleValue();  
        uploadPictureResult.setPicName(FileUtil.mainName(originFilename));  
        uploadPictureResult.setPicWidth(picWidth);  
        uploadPictureResult.setPicHeight(picHeight);  
        uploadPictureResult.setPicScale(picScale);  
        uploadPictureResult.setPicFormat(imageInfo.getFormat());  
        uploadPictureResult.setPicSize(FileUtil.size(file));  
        uploadPictureResult.setUrl(cosClientConfig.getHost() + "/" + uploadPath);  
        return uploadPictureResult;  
    } catch (Exception e) {  
        log.error("图片上传到对象存储失败", e);  
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "上传失败");  
    } finally {  
        this.deleteTempFile(file);  
    }  
}  
  
/**  
 * 校验文件  
 *  
 * @param multipartFile multipart 文件  
 */  
public void validPicture(MultipartFile multipartFile) {  
    ThrowUtils.throwIf(multipartFile == null, ErrorCode.PARAMS_ERROR, "文件不能为空");  
    // 1. 校验文件大小  
    long fileSize = multipartFile.getSize();  
    final long ONE_M = 1024 * 1024L;  
    ThrowUtils.throwIf(fileSize > 2 * ONE_M, ErrorCode.PARAMS_ERROR, "文件大小不能超过 2M");  
    // 2. 校验文件后缀  
    String fileSuffix = FileUtil.getSuffix(multipartFile.getOriginalFilename());  
    // 允许上传的文件后缀  
    final List<String> ALLOW_FORMAT_LIST = Arrays.asList("jpeg", "jpg", "png", "webp");  
    ThrowUtils.throwIf(!ALLOW_FORMAT_LIST.contains(fileSuffix), ErrorCode.PARAMS_ERROR, "文件类型错误");  
}  
  
/**  
 * 删除临时文件  
 */  
public void deleteTempFile(File file) {  
    if (file == null) {  
        return;  
    }  
    // 删除临时文件  
    boolean deleteResult = file.delete();  
    if (!deleteResult) {  
        log.error("file delete error, filepath = {}", file.getAbsolutePath());  
    }  
}

}

图片解析包装类:

@Data  
public class UploadPictureResult {  
  
    /**  
     * 图片地址  
     */  
    private String url;  
  
    /**  
     * 图片名称  
     */  
    private String picName;  
  
    /**  
     * 文件体积  
     */  
    private Long picSize;  
  
    /**  
     * 图片宽度  
     */  
    private int picWidth;  
  
    /**  
     * 图片高度  
     */  
    private int picHeight;  
  
    /**  
     * 图片宽高比  
     */  
    private Double picScale;  
  
    /**  
     * 图片格式  
     */  
    private String picFormat;  
  
}

3.图片上传服务
接口
/**  
 * 上传图片  
 *  
 * @param multipartFile  
 * @param pictureUploadRequest  
 * @param loginUser  
 * @return  
 */  
PictureVO uploadPicture(MultipartFile multipartFile,  
                        PictureUploadRequest pictureUploadRequest,  
                        User loginUser);

实现类

1.我们将所有图片都放到了public 目录下,并且为每个用户的图片存储到public/uid 的目录下

2.如果pid不为空,表示更新已有图片信息,需要判断图片是否存在,并且更新的时候要指定editTime编辑时间。可以调用Mybatis Plus 提供的saveOrUpdate方法兼容创建和更新操作

@Override  
public PictureVO uploadPicture(MultipartFile multipartFile, PictureUploadRequest pictureUploadRequest, User loginUser) {  
    ThrowUtils.throwIf(loginUser == null, ErrorCode.NO_AUTH_ERROR);  
   Long pictureId = null;
    if(pictureUploadRequest!=null){
        pictureId=pictureUploadRequest.getId();
    }
    if(pictureId!=null){
        pictureMapper.lambdaQuery().eq(Picture::getId,pictureId).exists();
        Throwif()
    }
    String.format("public/%s",loginUser.getId()).var; // mkdir by uid
    fileManager.uploadPicutre(mutipartFile,uploadPrefix).var;
    new Picture().var;
    set{url,name,size,width,height,scale,format,uid}
    if(picutreId!=null){
	set{pictureid,edittime}
    }
    mapper.saveOrUpdate()
    ThrowIf()
    return PictureVO.objToVo(picture)
}


4.Controller
/**  
 * 上传图片(可重新上传)  
 */  
@PostMapping("/upload")  
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
public BaseResponse<PictureVO> uploadPicture(  
        @RequestPart("file") MultipartFile multipartFile,  
        PictureUploadRequest pictureUploadRequest,  
        HttpServletRequest request) {  
    User loginUser = userService.getLoginUser(request);  
    PictureVO pictureVO = pictureService.uploadPicture(multipartFile, pictureUploadRequest, loginUser);  
    return ResultUtils.success(pictureVO);  
}

5.测试

当上传图片过大时侯,会触发报错,SpringBoot内嵌tomcat默认限制了请求中文件上传的大小。

spring:  
  # 开放更大的文件上传体积  
  servlet:  
    multipart:  
      max-file-size: 10MB

扩展

1.用枚举类 支持根据业务场景区分文件上传路径、校验规则等,从而复用FileManager。

2.目前文件上传的时候,会先在本地创建临时文件。如果不需要对文件进行额外处理、进一步提高性能,可以直接用流的方式将请求中的文件上传到COS。

3.补充更严格的校验,例如为支持的图片格式定义枚举,仅允许上传枚举定义的格式。

代码demo:

// 上传文件  
public static String uploadToCOS(MultipartFile multipartFile, String bucketName, String key) throws Exception {  
    // 创建 COS 客户端  
    COSClient cosClient = createCOSClient();  
  
    try (InputStream inputStream = multipartFile.getInputStream()) {  
        // 元信息配置  
        ObjectMetadata metadata = new ObjectMetadata();  
        metadata.setContentLength(multipartFile.getSize());  
        metadata.setContentType(multipartFile.getContentType());  
  
        // 创建上传请求  
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, key, inputStream, metadata);  
  
        // 上传文件  
        cosClient.putObject(putObjectRequest);  
  
        // 生成访问链接  
        return "https://" + bucketName + ".cos." + cosClient.getClientConfig().getRegion().getRegionName()  
               + ".myqcloud.com/" + key;  
    } finally {  
        cosClient.shutdown();  
    }  
}

图片管理

  • 管理员 deleteById
  • 管理员 update
  • 管理员 page
  • 管理员 getById
  • page desen
  • getById desen
  • update picture
1.数据模型

每一个请求都需要一个dto

图片更新请求,给管理员使用,tags类型改为List< String>便于前端上传

@Data  
public class PictureUpdateRequest implements Serializable {  
  
    /**  
     * id  
     */  
    private Long id;  
  
    /**  
     * 图片名称  
     */  
    private String name;  
  
    /**  
     * 简介  
     */  
    private String introduction;  
  
    /**  
     * 分类  
     */  
    private String category;  
  
    /**  
     * 标签  
     */  
    private List<String> tags;  
  
    private static final long serialVersionUID = 1L;  
}

图片修改

@Data  
public class PictureEditRequest implements Serializable {  
  
    /**  
     * id  
     */  
    private Long id;  
  
    /**  
     * 图片名称  
     */  
    private String name;  
  
    /**  
     * 简介  
     */  
    private String introduction;  
  
    /**  
     * 分类  
     */  
    private String category;  
  
    /**  
     * 标签  
     */  
    private List<String> tags;  
  
    private static final long serialVersionUID = 1L;  
}

图片查询

@EqualsAndHashCode(callSuper = true)  
@Data  
public class PictureQueryRequest extends PageRequest implements Serializable {  
  
    /**  
     * id  
     */  
    private Long id;  
  
    /**  
     * 图片名称  
     */  
    private String name;  
  
    /**  
     * 简介  
     */  
    private String introduction;  
  
    /**  
     * 分类  
     */  
    private String category;  
  
    /**  
     * 标签  
     */  
    private List<String> tags;  
  
    /**  
     * 文件体积  
     */  
    private Long picSize;  
  
    /**  
     * 图片宽度  
     */  
    private Integer picWidth;  
  
    /**  
     * 图片高度  
     */  
    private Integer picHeight;  
  
    /**  
     * 图片比例  
     */  
    private Double picScale;  
  
    /**  
     * 图片格式  
     */  
    private String picFormat;  
  
    /**  
     * 搜索词(同时搜名称、简介等)  
     */  
    private String searchText;  
  
    /**  
     * 用户 id  
     */  
    private Long userId;  
  
    private static final long serialVersionUID = 1L;  
}

2.服务开发

1.UserService编写判断用户是否为管理员的方法

/**  
 * 是否为管理员  
 *  
 * @param user  
 * @return  
 */  
boolean isAdmin(User user);

@Override  
public boolean isAdmin(User user) {  
    return user != null && UserRoleEnum.ADMIN.getValue().equals(user.getUserRole());  
}

查询包装类转换

@Override  
public QueryWrapper<Picture> getQueryWrapper(PictureQueryRequest pictureQueryRequest) {  
    QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();  
    if (pictureQueryRequest == null) {  
        return queryWrapper;  
    }  
    // 从对象中取值  
    Long id = pictureQueryRequest.getId();  
    String name = pictureQueryRequest.getName();  
    String introduction = pictureQueryRequest.getIntroduction();  
    String category = pictureQueryRequest.getCategory();  
    List<String> tags = pictureQueryRequest.getTags();  
    Long picSize = pictureQueryRequest.getPicSize();  
    Integer picWidth = pictureQueryRequest.getPicWidth();  
    Integer picHeight = pictureQueryRequest.getPicHeight();  
    Double picScale = pictureQueryRequest.getPicScale();  
    String picFormat = pictureQueryRequest.getPicFormat();  
    String searchText = pictureQueryRequest.getSearchText();  
    Long userId = pictureQueryRequest.getUserId();  
    String sortField = pictureQueryRequest.getSortField();  
    String sortOrder = pictureQueryRequest.getSortOrder();  
    // 从多字段中搜索  
    if (StrUtil.isNotBlank(searchText)) {  
        // 需要拼接查询条件  
        queryWrapper.and(qw -> qw.like("name", searchText)  
                .or()  
                .like("introduction", searchText)  
        );  
    }  
    queryWrapper.eq(ObjUtil.isNotEmpty(id), "id", id);  
    queryWrapper.eq(ObjUtil.isNotEmpty(userId), "userId", userId);  
    queryWrapper.like(StrUtil.isNotBlank(name), "name", name);  
    queryWrapper.like(StrUtil.isNotBlank(introduction), "introduction", introduction);  
    queryWrapper.like(StrUtil.isNotBlank(picFormat), "picFormat", picFormat);  
    queryWrapper.eq(StrUtil.isNotBlank(category), "category", category);  
    queryWrapper.eq(ObjUtil.isNotEmpty(picWidth), "picWidth", picWidth);  
    queryWrapper.eq(ObjUtil.isNotEmpty(picHeight), "picHeight", picHeight);  
    queryWrapper.eq(ObjUtil.isNotEmpty(picSize), "picSize", picSize);  
    queryWrapper.eq(ObjUtil.isNotEmpty(picScale), "picScale", picScale);  
    // JSON 数组查询  
    if (CollUtil.isNotEmpty(tags)) {  
        for (String tag : tags) {  
            queryWrapper.like("tags", "\"" + tag + "\"");  
        }  
    }  
    // 排序  
    queryWrapper.orderBy(StrUtil.isNotEmpty(sortField), sortOrder.equals("ascend"), sortField);  
    return queryWrapper;  
}

获取图片封装类相关方法

@Override  
public PictureVO getPictureVO(Picture picture, HttpServletRequest request) {  
    // 对象转封装类  
    PictureVO pictureVO = PictureVO.objToVo(picture);  
    // 关联查询用户信息  
    Long userId = picture.getUserId();  
    if (userId != null && userId > 0) {  
        User user = userService.getById(userId);  
        UserVO userVO = userService.getUserVO(user);  
        pictureVO.setUser(userVO);  
    }  
    return pictureVO;  
}

先获取到要查询的用户id列表,只发送一次查询用户表的请求,再将查询到的值设置到图片对象中。

/**  
 * 分页获取图片封装  
 */  
@Override  
public Page<PictureVO> getPictureVOPage(Page<Picture> picturePage, HttpServletRequest request) {  
    List<Picture> pictureList = picturePage.getRecords();  
    Page<PictureVO> pictureVOPage = new Page<>(picturePage.getCurrent(), picturePage.getSize(), picturePage.getTotal());  
    if (CollUtil.isEmpty(pictureList)) {  
        return pictureVOPage;  
    }  
    // 对象列表 => 封装对象列表  
    List<PictureVO> pictureVOList = pictureList.stream().map(PictureVO::objToVo).collect(Collectors.toList());  
    // 1. 关联查询用户信息  
    Set<Long> userIdSet = pictureList.stream().map(Picture::getUserId).collect(Collectors.toSet());  
    Map<Long, List<User>> userIdUserListMap = userService.listByIds(userIdSet).stream()  
            .collect(Collectors.groupingBy(User::getId));  
    // 2. 填充信息  
    pictureVOList.forEach(pictureVO -> {  
        Long userId = pictureVO.getUserId();  
        User user = null;  
        if (userIdUserListMap.containsKey(userId)) {  
            user = userIdUserListMap.get(userId).get(0);  
        }  
        pictureVO.setUser(userService.getUserVO(user));  
    });  
    pictureVOPage.setRecords(pictureVOList);  
    return pictureVOPage;  
}

图片数据校验

@Override  
public void validPicture(Picture picture) {  
    ThrowUtils.throwIf(picture == null, ErrorCode.PARAMS_ERROR);  
    // 从对象中取值  
    Long id = picture.getId();  
    String url = picture.getUrl();  
    String introduction = picture.getIntroduction();  
    // 修改数据时,id 不能为空,有参数则校验  
    ThrowUtils.throwIf(ObjUtil.isNull(id), ErrorCode.PARAMS_ERROR, "id 不能为空");  
    if (StrUtil.isNotBlank(url)) {  
        ThrowUtils.throwIf(url.length() > 1024, ErrorCode.PARAMS_ERROR, "url 过长");  
    }  
    if (StrUtil.isNotBlank(introduction)) {  
        ThrowUtils.throwIf(introduction.length() > 800, ErrorCode.PARAMS_ERROR, "简介过长");  
    }  
}

3.Controller
/**  
 * 删除图片  
 */  
@PostMapping("/delete")  
public BaseResponse<Boolean> deletePicture(@RequestBody DeleteRequest deleteRequest, HttpServletRequest request) {  
    if (deleteRequest == null || deleteRequest.getId() <= 0) {  
        throw new BusinessException(ErrorCode.PARAMS_ERROR);  
    }  
    User loginUser = userService.getLoginUser(request);  
    long id = deleteRequest.getId();  
    // 判断是否存在  
    Picture oldPicture = pictureService.getById(id);  
    ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);  
    // 仅本人或管理员可删除  
    if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {  
        throw new BusinessException(ErrorCode.NO_AUTH_ERROR);  
    }  
    // 操作数据库  
    boolean result = pictureService.removeById(id);  
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);  
    return ResultUtils.success(true);  
}  
  
/**  
 * 更新图片(仅管理员可用)  
 */  
@PostMapping("/update")  
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
public BaseResponse<Boolean> updatePicture(@RequestBody PictureUpdateRequest pictureUpdateRequest) {  
    if (pictureUpdateRequest == null || pictureUpdateRequest.getId() <= 0) {  
        throw new BusinessException(ErrorCode.PARAMS_ERROR);  
    }  
    // 将实体类和 DTO 进行转换  
    Picture picture = new Picture();  
    BeanUtils.copyProperties(pictureUpdateRequest, picture);  
    // 注意将 list 转为 string  
    picture.setTags(JSONUtil.toJsonStr(pictureUpdateRequest.getTags()));  
    // 数据校验  
    pictureService.validPicture(picture);  
    // 判断是否存在  
    long id = pictureUpdateRequest.getId();  
    Picture oldPicture = pictureService.getById(id);  
    ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);  
    // 操作数据库  
    boolean result = pictureService.updateById(picture);  
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);  
    return ResultUtils.success(true);  
}  
  
/**  
 * 根据 id 获取图片(仅管理员可用)  
 */  
@GetMapping("/get")  
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
public BaseResponse<Picture> getPictureById(long id, HttpServletRequest request) {  
    ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);  
    // 查询数据库  
    Picture picture = pictureService.getById(id);  
    ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);  
    // 获取封装类  
    return ResultUtils.success(picture);  
}  
  
/**  
 * 根据 id 获取图片(封装类)  
 */  
@GetMapping("/get/vo")  
public BaseResponse<PictureVO> getPictureVOById(long id, HttpServletRequest request) {  
    ThrowUtils.throwIf(id <= 0, ErrorCode.PARAMS_ERROR);  
    // 查询数据库  
    Picture picture = pictureService.getById(id);  
    ThrowUtils.throwIf(picture == null, ErrorCode.NOT_FOUND_ERROR);  
    // 获取封装类  
    return ResultUtils.success(pictureService.getPictureVO(picture, request));  
}  
  
/**  
 * 分页获取图片列表(仅管理员可用)  
 */  
@PostMapping("/list/page")  
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)  
public BaseResponse<Page<Picture>> listPictureByPage(@RequestBody PictureQueryRequest pictureQueryRequest) {  
    long current = pictureQueryRequest.getCurrent();  
    long size = pictureQueryRequest.getPageSize();  
    // 查询数据库  
    Page<Picture> picturePage = pictureService.page(new Page<>(current, size),  
            pictureService.getQueryWrapper(pictureQueryRequest));  
    return ResultUtils.success(picturePage);  
}  
  
/**  
 * 分页获取图片列表(封装类)  
 */  
@PostMapping("/list/page/vo")  
public BaseResponse<Page<PictureVO>> listPictureVOByPage(@RequestBody PictureQueryRequest pictureQueryRequest,  
                                                         HttpServletRequest request) {  
    long current = pictureQueryRequest.getCurrent();  
    long size = pictureQueryRequest.getPageSize();  
    // 限制爬虫  
    ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);  
    // 查询数据库  
    Page<Picture> picturePage = pictureService.page(new Page<>(current, size),  
            pictureService.getQueryWrapper(pictureQueryRequest));  
    // 获取封装类  
    return ResultUtils.success(pictureService.getPictureVOPage(picturePage, request));  
}  
  
/**  
 * 编辑图片(给用户使用)  
 */  
@PostMapping("/edit")  
public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) {  
    if (pictureEditRequest == null || pictureEditRequest.getId() <= 0) {  
        throw new BusinessException(ErrorCode.PARAMS_ERROR);  
    }  
    // 在此处将实体类和 DTO 进行转换  
    Picture picture = new Picture();  
    BeanUtils.copyProperties(pictureEditRequest, picture);  
    // 注意将 list 转为 string  
    picture.setTags(JSONUtil.toJsonStr(pictureEditRequest.getTags()));  
    // 设置编辑时间  
    picture.setEditTime(new Date());  
    // 数据校验  
    pictureService.validPicture(picture);  
    User loginUser = userService.getLoginUser(request);  
    // 判断是否存在  
    long id = pictureEditRequest.getId();  
    Picture oldPicture = pictureService.getById(id);  
    ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);  
    // 仅本人或管理员可编辑  
    if (!oldPicture.getUserId().equals(loginUser.getId()) && !userService.isAdmin(loginUser)) {  
        throw new BusinessException(ErrorCode.NO_AUTH_ERROR);  
    }  
    // 操作数据库  
    boolean result = pictureService.updateById(picture);  
    ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);  
    return ResultUtils.success(true);  
}

4.获取预置标签和分类

随着系统规模和数据不断扩大,可以再更改为使用配置中心或数据库动态管理这些数据,或者通过定时任务计算出热门图片分类和标签

@GetMapping("/tag_category")  
public BaseResponse<PictureTagCategory> listPictureTagCategory() {  
    PictureTagCategory pictureTagCategory = new PictureTagCategory();  
    List<String> tagList = Arrays.asList("热门", "搞笑", "生活", "高清", "艺术", "校园", "背景", "简历", "创意");  
    List<String> categoryList = Arrays.asList("模板", "电商", "表情包", "素材", "海报");  
    pictureTagCategory.setTagList(tagList);  
    pictureTagCategory.setCategoryList(categoryList);  
    return ResultUtils.success(pictureTagCategory);  
}

四、模块开发细节

Java中 常见的开源图库介绍 Java 中的开源图库为开发者提供了强大的工具来处理图相关的问题。JGraphT、GraphStream 和 Algorithms4 各有特点,适用于不同的场景。JGraphT 适合需要复杂图算法和自定义图实现的场景;GraphStream 适合需要图可视化和大规模图处理的场景;Algorithms4 适合算法教学和研究以及需要实现特定图算法的场景。 阅读详情

相关推荐

Android图库图片访问与路径获取指南

在本章节中,我们详细介绍了如何处理Android应用中的权限回调和图片选择结果。首先了解了如何监听和解析权限请求的结果,以及如何处理用户拒绝权限的情况。接着,通过代码示例展示了如何从方法中获取图片Uri,并进一步对图片数据进行加载、处理和展示。通过以上步骤,开发者可以确保应用在获得所需权限后,能够顺利地接收用户选择的图片,并提供良好的用户体验。MediaStore是Android系统提供的用于访问媒体文件的API,尤其是在处理图片和视频文件时。

weixin_36282234的博客 1581

图库

Java 数据结构

FeBr2的博客 452

优快云技术库的知识图谱高清合集(50张)

资源来自优快云:http://geek.youkuaiyun.com/news/detail/110921 50份高清知识图谱,只为分享传播,不需资源分

云图库平台(五)——后端图片模块开发

云图库平台(五)——后端图片模块开发

m0_74352571的博客 1539

WEB开发图片资源

有各种小图标,专门用于web开发,也可以用于其他的开发。

新版完整Visio图库

从多个地方搜罗的Viso图标,含有51个文件,分别对应48个领域类别。(2005/2008年资源,酌情下载)

图库项目开发 阶段一-用户传图

*** 搜索词/*** 抓取数量。

Fqxiny的博客 1542

【Java微服务-医疗管理项目】(附源码+资料+教程)

尚医疗是专门为各大医院、门诊提供的一款医疗管理平台。 系统包含:系统管理、药品进销存管理、看病就诊、收费管理、检查管理、数据统计等核心模块。 通过尚医疗系统可以快速、方便的管理病人从挂号到门诊结束所涉及到的全套流程。也可以清晰明了的体现出药品进货、出售的所有记录。.........

m0_72882572的博客 2203

图库项目开发 阶段一-用户模块

GetterPARAMS_ERROR(40000, "请求参数错误"),NOT_LOGIN_ERROR(40100, "未登录"),NO_AUTH_ERROR(40101, "无权限"),NOT_FOUND_ERROR(40400, "请求数据不存在"),FORBIDDEN_ERROR(40300, "禁止访问"),SYSTEM_ERROR(50000, "系统内部异常"),OPERATION_ERROR(50001, "操作失败");/*** 状态码*//*** 信息*/

Fqxiny的博客 756

【物联网】探索NE555:一款经典的集成电路(超详细)

NE555是一种集成电路,其内部结构包括比较器、RS触发器、电压比较器和输出级三个主要功能模块,外部引脚则提供了与其他电路元件进行连接的接口。NE555的设计目的是为了提供一种简单方便的定时器解决方案,它广泛应用于模拟和数字电路中。NE555作为一款经典的集成电路,具备设计简单、易于使用和稳定可靠的特点,在电子领域发展了广泛的应用。从单稳态模式到多稳态模式,再到振荡器模式,NE555可以满足不同的需求,并且可以与其他电路元件灵活组合,扩展其功能。然而,需要注意的是NE555也有一些限制。

Goforyouqp的博客 6万+

S32K144之芯片锁死解锁——关闭debug端口

S32K144之芯片锁死及解锁 经常捣鼓单片机就会发现,时常会因为某些操作不当导致芯片锁死情况的发生,通过Jlink我们可以读到芯片的ID,但是无法进行Debug,这很是让人苦恼。 基于S32K144的芯片锁死,一般有如下几种可能: 1、时钟配置异常,过高的配置时钟频率以及不配置时钟,都有可能造成芯片保护锁死,我就曾经因为不注意把时钟配置代码注释掉,导致芯片锁死。 2、我是微软雅黑烧写断电,如果在...

weixin_41935674的博客 1万+

图库项目开发 阶段二-图片优化

Caffeine(本地缓存)低延迟:本地内存缓存(Caffeine)直接运行在应用服务器上,避免了网络开销,响应时间可缩短到毫秒级(甚至微秒级)。减少 Redis 负载:高频访问的热点数据(如热门图片)存储在本地,减少 Redis 的查询压力。抗抖动:本地缓存能抵御网络波动或 Redis 服务短暂不可用的情况。数据结构灵活:Caffeine 支持按需配置淘汰策略(如 LRU、LFU),适合应用层细粒度控制。Redis(分布式缓存)高并发与分布式支持:Redis 是内存数据库,支持毫秒级响应。

Fqxiny的博客 1228

Vofa+使用笔记(1):用Vofa+通过串口连接显示波形

1. 介绍 Vofa+有三种协议设置 rawdata:你发什么就显示什么,跟普通的串口助手一样 firewater:最简单在VOFA+里绘制波形的协议,建议仅在通道数量不多、发送频率不高的时候使用。 justfloat:有一定的数据格式,此协议非常适合用在通道数量多、发送频率高的时候。 2. 使用步骤 设置号下面三个地方,使用的协议以及传输的方式 拉出控件 (最开始上位机没有识别到你的数据的时候,右边是没有数据显示的,如果你想要y轴显示什么数据,右边设置显示也可以,右键波形图然后Y轴设置也行) 把进

苏释州的博客 5万+

智能云图库项目实战(1)---图片模块

本文深入探索公共图库平台的核心业务流程,揭秘它是如何在海量图片中脱颖而出,成为创意者和用户的宝藏。无论你是开发者、设计师还是图片爱好者,这里都有你想要的秘密。让我们一起开启这场图库狂潮的探索之旅吧!

qq_51749909的博客 2290

SVA学习笔记(1)——property基本语法及巧用define与property

巧用define与property,让代码更加灵活

craftsman2018ship的博客 1574

开关电源的纹波和噪声电压-抑制方法

开关电源(包括AC/DC转换器、DC/DC转换器、AC/DC模块和DC/DC模块)与线性电源相比较,最突出的优点是转换效率高,一般可达80%~85%,高的可达90%~97%;其次,开关电源采用高频变压器替代了笨重的工频变压器,不仅重量减轻,体积也减小了,因此应用范围越来越广。 但开关电源的缺点是由于其开关管工作于高频开关状态,输出的纹波和噪声电压较大,一般为输出电压的1%左右(低的为输出电压的0.5%左右),最好产品的纹波和噪声电压也有几十mV;而线性电源的调整管工作于线性状态,无纹波电压,输出的噪声电压也

ltqdxl的博客 1万+

【智能协同云图库】智能协同云图库第三弹:基于腾讯云 COS 对象存储—开发图片模块

基于腾讯云 COS 对象存储实现:图片文件上传功能、图片文件下载功能、图片管理功能、预留标签列表和分页列表

OuO-1 的博客 3105

9、眼底图像智能分析:糖尿病视网膜病变的自动化识别与分级系统

本博客介绍了一种基于深度学习的糖尿病视网膜病变(DR)自动化识别与分级系统。该系统通过视网膜图像的预处理、数据增强、集成模型的构建与优化,实现了对DR的高效识别与分级。识别系统采用二分类任务,准确率达到97.67%,而分级系统通过四分类策略取得了95.2%的准确率。系统在基层社区和医院中具有广泛的应用前景,为DR的早期筛查和精准诊断提供了有效工具。

pea55的博客 145

智能云图库项目

平台基于 Spring Boot + Redis + COS + AI 绘图 + RabbitMQ + WebSocket,支持图片上 传、存储、协作编辑与智能分析。平台分为公共图库、私有图库和企业空间,支持多账号权限体系与实时协作。第一阶段,开发公共图库平台。实战Spring Boot图片素材网址的快速开发,重点学习文件存管业务的开发和优化技巧。第二阶段,对项目的C端功能进行扩展。用户可开通私有空间,并对空间图片进行多维检索、扫码分享、批量管理、快速编辑和用量分析。

cui_hao_nan的博客 983

使用Gitlab 搭建私有镜像仓库(外置Nginx)

为了让团队尽快使用Gitlab的CI进行工作,必须做好Gitlab的初始化工作,大家都知道拉取镜像由于某些原因,会比较慢,因此构建自己的私有镜像仓库就成为了关键的一步。当然私有镜像仓库的搭建有很多种方式,这里直接使用Gitlab提供的镜像仓库功能。一揽子解决方案比较香。gitlab真的越来越好用了,有了这个镜像仓,也没必要再使用三方的镜像仓库了。Devops 真好用!

网尘的技术专栏 1829

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

智能云图库项目实战(7)---图库分析

上述的需求我们是通过提供的方法实现数据库的分组排序查询,对于更复杂多样的分析需求,其实我们还可以自己在代码中编写SQL语句。可能有部分同学会好奇,MyBatis还能自定义SQL?不都是直接调用xxx.select之类的方法么?这就是典型的“用框架习惯了”,其实为了提高开发效率、避免自己写 SQL,我们之前一直使用的是框架。但别忘了,是MyBatis的增强版,本质还是基于MyBatis的一些能力进行的一些封装简化,自定义 SQL可是MyBatis最最最基础的能力之一。在MyBatis。

qq_51749909的博客 1184

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

智能云图库项目实战(8)---团队空间

本项目的角色:本项目的权限:根据RBAC权限模型,需要定义角色和权限。1)此处选用JSON配置文件来定义角色、权限、角色和权限之间的关系,相比从数据库表中获取,实现更方便,查询也更高效。在目录下新建JSON配置文件"name": "成员管理","description": "管理空间成员,添加或移除成员"},"name": "查看图片","description": "查看空间中的图片内容"},"name": "上传图片","description": "上传图片到空间中"},

qq_51749909的博客 2182

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

【智能协同云图库】智能协同云图库第九弹:实现图库分析功能—空间资源使用、空间图片分类、空间图片标签、空间图片大小、用户上传行为、空间使用排行等信息的分析功能实现

实现图库分析功能—空间资源使用、空间图片分类、空间图片标签、空间图片大小、用户上传行为、空间使用排行等信息的分析功能实现

OuO-1 的博客 2220

MaMage图库项目-No.2 Alpha 阶段计划与分工

在 Alpha Sprint 阶段,我们将从需求分析进入到具体执行阶段。本阶段目标是:完成“微信小程序照片上传 + Web端基础管理 + 标签检索 + 用户认证”核心功能的可演示Alpha版本。项目周期为10个工作日,团队采用Scrum模式,由你(李文宇)担任兼前端开发。每人每天投入约4小时,3人团队总计约120小时工作量。

liwy2019的博客 623

MaMage图库项目-No.7.1 beta 阶段的day 1~5

因为是个人开发,所以日志写的会比较慢,不能一天一出,更不会完全照着燃尽图去做,但我觉得这几天做的东西对我一个在alpha开发前还在解刨小鼠的人,让我学到了很多,beta阶段又找了邹欣老师和刘鑫老师聊了很久,老师们也不介意我的背景和效率,很真诚地提出建议,所以beta阶段这前五天也确实让我做成了一些比较偏工程方面的东西。所以这个博客完全不让Ai帮我写,说说感想。

liwy2019的博客 4497

MaMage图库项目-No.4 Alpha阶段每天开发日志合集

安装 Node.js,排查 curl -sL https://deb.nodesource.com/setup_16.x | sudo -E bash - 报错问题;长期规划迁移至阿里云 OSS)。域名在腾讯云购买、DNS 在 Cloudflare、服务在阿里云,链路略复杂,需要一步步确认解析是否生效;GET /api/photos/raw/:id:根据 photoId 读取本地文件并返回二进制流。DELETE /api/photos/:id 删除单图(当前还在调试文件删除与数据库一致性);

liwy2019的博客 854
上一篇: 图库项目开发 阶段一-用户模块
下一篇: 图库项目开发 阶段一-用户传图
~Yogi
博客等级 码龄5年 414粉丝 99原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

~Yogi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值