Skip to content

About

荔枝云图库,在线管理图片,AI生成图片,数据大屏等

Resources

Stars

4 stars

Watchers

1 watching

Forks

Latest commit

 

History

851 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

LZ-Picture

[TOC]

项目介绍

LZ-Picture(荔枝云图库)是一个综合性的云端图像管理平台,用户可以上传、整理、分享和发现图片。结合了现代网络技术和AI能力,打造了一个功能丰富的图像存储库,拥有面向用户和管理员的双重界面。

LZ-Picture是一个全栈应用程序,提供以下功能:

  • 图像管理:上传、整理、压缩和水印图片
  • 用户空间:个性化图片画廊和文件夹组织
  • 社交功能:点赞、收藏、分享和下载图片
  • 积分系统:基于积分的经济体系,用于下载和高级功能
  • AI集成:高级图像分析和处理
  • 管理工具:完整后端管理系统
  • 数据大屏:echarts/vue3数据统计大屏

该平台采用模块化系统构建,后端服务和前端界面清晰分离,遵循企业应用开发的最佳实践。

本项目基于ruoyi-vue(https://doc.ruoyi.vip/ruoyi-vue/) 二次开发,十分感谢若依的开源精神,大大提高了本项目的开发进度。

在线地址

https://springsun.online/

视频地址

https://v.douyin.com/oWdWW6xqM7g/ 1@5.com

系统架构/技术栈

  • 后端:

    • Java 21
    • Spring Boot 3.3.5
    • MyBatis Plus3.5.12
    • Redis (Redisson)
    • MySQL 8
    • JWT用于认证
    • Lombok1.18.30
    • 基于若依SpringBoot3(优化代码生成器,生成MP)
  • 前端:

    前端分为用户端和管理端(基于若依Vue3)

    • Vue3
    • Tailwind CSS(用户端)
    • antdsign (用户端)
    • SCSS(管理端)
    • elementUiPlas(管理端)
    • Vite构建系统
    • pinia
    • echarts
  • 存储:

    • 阿里云OSS用于图像存储
    • Redis用于缓存
    • MySQL用于结构化数据
  • 外部服务:

    • 阿里云短信用于手机验证
    • 支付宝用于支付处理
    • 即梦AI(文生图/图生图,基于https实现)

关键组件

后端模块

模块 用途
picture-admin 主应用程序入口和管理API控制器
picture-framework 提供所有模块基础的核心框架组件
picture-system 系统级功能,包括权限和基础配置
picture-common 共享工具、助手和通用代码
picture-picture 核心图像处理和管理功能
picture-user 用户资料管理和用户数据
picture-user-auth 认证和授权系统
picture-points 平台经济积分/信用系统
picture-ai 图像分析和处理的AI集成
picture-config 系统配置管理
picture-quartz 定时任务和后台处理

前端应用

  • Picture-ui:基于Vue的用户界面,使用Tailwind CSS
  • Picture-back-ui:基于Vue的系统管理界面,若依VUE3

两个前端都是现代单页应用(SPA),通过RESTful API与后端通信。用户端接口前缀user,管理端admin

内置功能

  • 用户管理:用户是系统操作者,该功能主要完成系统用户配置。
  • 部门管理:配置系统组织机构(公司、部门、小组),树结构展现支持数据权限。
  • 岗位管理:配置系统用户所属担任职务。
  • 菜单管理:配置系统菜单,操作权限,按钮权限标识等。
  • 角色管理:角色菜单权限分配、设置角色按机构进行数据范围权限划分。
  • 字典管理:对系统中经常使用的一些较为固定的数据进行维护。
  • 参数管理:对系统动态配置常用参数。
  • 通知公告:系统通知公告信息发布维护。
  • 操作日志:系统正常操作日志记录和查询;系统异常信息日志记录和查询。
  • 登录日志:系统登录日志记录查询包含登录异常。
  • 在线用户:当前系统中活跃用户状态监控。
  • 定时任务:在线(添加、修改、删除)任务调度包含执行结果日志。
  • 代码生成:前后端代码的生成(java、html、xml、sql)支持CRUD下载 。
  • 系统接口:根据业务代码自动生成相关的api接口文档。
  • 服务监控:监视当前系统CPU、内存、磁盘、堆栈等相关信息。
  • 缓存监控:对系统的缓存信息查询,命令统计等。
  • 在线构建器:拖动表单元素生成相应的Vue代码。
  • 连接池监视:监视当期系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈。
  • 自定义缓存:使用注解快速制作redis缓存。
  • 自定义排序:使用注解自定义排序。

开发规范

  1. 缓存

    • 缓存每个单词统一使用:分割,最后一个使用:Constants.COMMON_SEPARATOR_CACHE公共字段拼接所需内容,因为自定义缓存是有自动添加COMMON_SEPARATOR_CACHE的
    • 首个单词必须为每个模块的模块名,比如ai,命名为:ai:ai:....
    • 注意:如果是若依自带的,缓存默认会加上:sys_config
    • 缓存缓存时间统一为秒
  2. 后端

    • 类字段统一采用驼峰命名
    • Constants字段统一全部大写加_分割单词
    • 枚举命名:类为每个模块单词的首字母加上该字典类型,字段为该字典类型+值,例:类名PPictureReviewStatus,字段:PICTURE_REVIEW_STATUS_0
    • 方法有两个相似的必须提出来当方法,减少代码耦合
    • 逻辑复杂或者处理同一个内容需要多个步骤的需要抽出方法,方便调用、在其他地方使用这个逻辑时能够直接调用,降低bug率
    • 一个方法行数实际代码数量不得超过一百行
  3. 用户端前端

    • 文件命名规范:每个模块会有模块的文件夹,比如图片模块picture文件夹,下面加上他的文件,picture.vue这样,如果是详情,pictureDetail.vue,采用驼峰命名,同时如果多个相似要放在同一个文件夹,比如图片模块的图库,就可以放在图库picture/space/space.vue

    • vue模块:js必须要有name,模块+文件名,template最外层div必须加class,并且class命名为横杠来分割单词,模块+文件名例:forgetPassword,user-forgot-password

    • css使用scss

  4. 管理前端

    • 采用若依当前规则即可
    • css使用scss
  5. 配置模块命名规则

    • 权限标识:路由地址+对应功能
    • 配置键名:模块:功能:功能
  6. 其他

    • 公共使用的方法必须提到对应的公共方法类或者ts、js文件
    • 参数不能使用基类,必须使用对应的vo、dto,前端同理使用type,防止参数耦合
    • 前端使用多的,相似度高的地方要提取为组件

公共功能

1.自定义缓存

1.缓存注解

/**
 * 自定义缓存注解,父类字段无法拿到
 * 坚持是前行的舟
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCacheable {
    String keyPrefix();          // 缓存 key 的前缀

    String keyField() default "";           // 从入参中提取 key 的字段路径(支持嵌套字段)

    long expireTime() default 3600L; // 缓存时间(秒)

    boolean paginate() default false; // 是否启用分页缓存

    String pageNumberField() default ""; // 页码字段(如果 paginate 为 true,则使用该字段)

    String pageSizeField() default "";  // 每页大小字段(如果 paginate 为 true,则使用该字段)

    boolean useQueryParamsAsKey() default false; // 是否将整个查询对象转换为 JSON 字符串作为缓存 key 的一部分
}

2. 删除缓存

/**
 * 自定义缓存删除注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomCacheEvict {

    String[] keyPrefixes();

    String[] keyFields() default {}; // 支持嵌套,如 "request.userId"

    boolean useQueryParamsAsKey() default false; // 如果true,加上参数JSON串模糊删除

}

2.自定义排序

  1. 注解
/**
 * 自定义排序注解
 * 首先我希望我变好,也希望你
 *
 * @Project: Picture
 * @Author: YY
 * @CreateTime: 2025-07-27  16:41
 * @Version: 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomSort {
    String[] sortFields() default {};   //排序字段
    String[] sortMappingFields() default {}; //映射字段,前端只需要传过来字段名,如果多表查询设置映射字段,映射字段为查询时所需字段
}
  1. 查询mapper
        <choose>
            <when test="params.sortSql != null and params.sortSql != ''">
                ${params.sortSql}
            </when>
            <otherwise>
                order by create_time desc
            </otherwise>
        </choose>
  1. 使用
    @CustomSort(
            sortFields = {"usageCount", "lookCount", "downloadCount", "createTime", "updateTime", "orderNum"},
            sortMappingFields = {
                    "usage_count", "look_count", "download_count", "create_time", "update_time", "order_num"
            })
    @Override
    public List<PictureCategoryInfo> selectPictureCategoryInfoList(PictureCategoryInfo pictureCategoryInfo) {
        return pictureCategoryInfoMapper.selectPictureCategoryInfoList(pictureCategoryInfo);
    }
  1. 前端使用,此注解当前主要使用在管理端

  2. 在el-table加上@sort-change="customSort"自定义排序方法和ref

    <el-table ref="tableRef" v-loading="loading" :data="accountInfoList" @selection-change="handleSelectionChange" @sort-change="customSort">
  1. 在字段加上自定义排序标志
      <el-table-column label="充值总金额" align="center" prop="rechargeAmount" sortable="custom"
                       v-if="columns[6].visible" :show-overflow-tooltip="true"/>
  1. 排序字段
const isAsc = ref();
const orderByColumn = ref('');
  1. 新增方法
//自定义排序
function customSort({column, prop, order}) {
  if (prop !== undefined && prop !== '' && order !== null && order !== '') {
    orderByColumn.value = prop;
    isAsc.value = order === "ascending";
  } else {
    orderByColumn.value = null;
    isAsc.value = null;
  }
  queryParams.value.pageNum = 1;
  getList();
}
  1. 在getList添加
  if (orderByColumn.value != null && isAsc.value !== null) {
    queryParams.value.params["orderByColumn"] = orderByColumn.value;
    queryParams.value.params["isAsc"] = isAsc.value;
  }
  1. 在重置按钮更新ref
/** 重置按钮操作 */
function resetQuery() {
  daterangeCreateTime.value = [];
  daterangeUpdateTime.value = [];
  orderByColumn.value = null
  isAsc.value = null;
  proxy.resetForm("queryRef");
  proxy.$refs.tableRef.clearSort();
  handleQuery();
}

3.MD编辑器

​ 后端MD编辑器,前端MD文件编辑器,管理端拥有编辑器和预览器,用户端只有预览器(前端基于md-editor-v3),这里只展示后端部分代码。

/**
 * 用于构建 Markdown 文档内容。
 */
public class MarkdownBuilder {

    private final StringBuilder sb = new StringBuilder();

    /**
     * 添加标题(支持1~6级)
     */
    public MarkdownBuilder addHeading(String text, int level) {
        level = Math.max(1, Math.min(6, level)); // 限制在1~6级
        sb.append("#".repeat(level)).append(" ").append(text).append("\n\n");
        return this;
    }

    /**
     * 添加普通段落
     */
    public MarkdownBuilder addParagraph(String text) {
        sb.append(text).append("\n\n");
        return this;
    }

    /**
     * 添加无序列表
     */
    public MarkdownBuilder addBulletList(List<String> items) {
        for (String item : items) {
            sb.append("- ").append(item).append("\n");
        }
        sb.append("\n");
        return this;
    }

    /**
     * 添加图片
     * @param altText 图片的替代文本
     * @param url 图片地址
     */
    public MarkdownBuilder addImage(String altText, String url) {
        sb.append("![").append(altText).append("](").append(url).append(")").append("\n\n");
        return this;
    }

    /**
     * 添加表格
     * @param headers 表头列表
     * @param rows 行数据,每行是一个字符串列表
     */
    public MarkdownBuilder addTable(List<String> headers, List<List<String>> rows) {
        // 表头
        sb.append("| ").append(String.join(" | ", headers)).append(" |\n");
        // 分隔线
        sb.append("|").append(" --- |".repeat(headers.size())).append("\n");
        // 行数据
        for (List<String> row : rows) {
            sb.append("| ").append(String.join(" | ", row)).append(" |\n");
        }
        sb.append("\n");
        return this;
    }

    /**
     * 导出 Markdown 内容为字符串
     */
    public String toMarkdown() {
        return sb.toString();
    }

    /**
     * 写入文件
     */
    public void writeToFile(String filePath) throws IOException {
        try (FileWriter writer = new FileWriter(filePath)) {
            writer.write(toMarkdown());
        }
    }
}

4.异常抛出工具类

/**
 * 异常处理工具类
 *
 * @Author YY
 */
public class ThrowUtils {

    /**
     * 条件成立则抛异常
     *
     * @param condition        条件
     * @param runtimeException 异常
     */
    public static void throwIf(boolean condition, RuntimeException runtimeException) {
        if (condition) {
            throw runtimeException;
        }
    }

    /**
     * 条件成立则抛异常
     *
     * @param condition 条件
     * @param message   错误信息
     */
    public static void throwIf(boolean condition, String message) {
        throwIf(condition, new ServiceException(message));
    }

    /**
     * 条件成立则抛异常
     *
     * @param condition 条件
     * @param errorCode 错误码
     * @param message   错误信息
     */
    public static void throwIf(boolean condition, Integer errorCode, String message) {
        throwIf(condition, new ServiceException(message, errorCode));
    }
}

配置模块

1. 配置信息

  • 配置信息存入缓存信息,name是键名,value是值,键值对

  • 值可以是文件和值

  • 初始化项目加载所有配置信息

  • 根据order_num升序

  • 配置信息添加是否内置,如果内置则用户端不可以通过接口获取到数据

  • 新增及使用方式:

    1. 首先新增key,例如:user:login:captchaEnabled

    2. 在类ConfigInfoUtils添加对应字段和默认值

          public static final String USER_LOGIN_CAPTCHA_ENABLED_KEY = "user:login:captchaEnabled";
          public static Boolean USER_LOGIN_CAPTCHA_ENABLED_VALUE = true;
      1. 在refreshConfig新增,即完成

        USER_LOGIN_CAPTCHA_ENABLED_VALUE = getBool(USER_LOGIN_CAPTCHA_ENABLED_KEY);
        
      2. 使用直接调用USER_LOGIN_CAPTCHA_ENABLED_VALUE

      3. 用户前端使用方式

        1. 用户第一次获取配置之后会存入本地缓存,在utils/config.js

        2. 存入缓存stores/config.js

        3. 使用方式:

          titleDesc.value = await useConfig('picture:report:content')

2. 菜单信息

  • 创建规则:根据提示创建
  • 动态路由,为用户端实现动态路由
  • 权限管理,菜单作为权限管理
  • 缓存,刷新缓存
  • 状态和是否显示都可以作为改菜单是否有权限,是否开启权限,是否显示对应接口,如果不显示表示这个接口不开启
  • 自定义路由信息,根据用户权限
  • 写死的路由不做排序,但是有前后之分,动态路由是中间的,有排序的

3.国际化

​ 当前尚未完全实现。

1.国际化国家

  • 简称是唯一的

2.国际化键名

  • 根据order_num升序
  • 键是唯一的

3.国际化信息

  • 后续国际化信息根据国家(简称)+键名,同时组成唯一键 (待完成)
  • 信息需要存入缓存,根据国家作为redis key存入 map key(键名)——value(信息)
  • 新增国际化信息并存入缓存,项目启动初始化至缓存

4.消息模板

  • 可以新增,生成事例,根据变量生成事例
  • 回退对应版本

5.公告

  • 公告没有模板,基本填写,不为所有用户推送,所有用户可以查看
  • 前端每日会刷新一次,每日会通知一次
  • 前端位置components/NoticeWindows.vue

6.文件日志

  • 记录文件上传的日志,防止冗余图片,比如头像、图片、封面

  • 使用方式,异步执行:

            //异步执行存入文件日志
            PictureAsyncManager.me().execute(PictureFileLogAsyncFactory.recordFileLog(picture,
                    getLoginUser().getUserId(),
                    CFileLogTypeEnum.LOG_TYPE_0.getValue()
            ));
     /**
       * 记录文件日志
       *
       * @param fileResponse 图片信息
       * @param userId              用户编号
       * @param logType             日志类型
       * @param deviceInfo          设备信息
       * @return void
       * @author: YY
       * @date: 2025/5/10 22:56
       **/
      void recordFileLog(FileResponse fileResponse, String userId, String logType, DeviceInfo deviceInfo);

其中逻辑:如果传入原图和压缩图,就执行保存对应的信息

  • 更新逻辑,需要自己实现,当前的直接去看项目注释,例如:

        /**
         * 异步更新文件日志,判断是否和存入数据库日志一样,如果不一样则更新,如果一样则不更新,表示没有新上传图片
         * 因为新增时图片已经为正常
         * 不一样表示新上传图片,要把上传的图片更新为正常,老图片为冗余
         *
         * @param old 老的用户信息
         * @param userInfo 新的用户信息
         * @return TimerTask
         * @author: YY
         * @method: updateUserInfoAvatarFileLog
         * @date: 2025/5/31 16:03
         **/
        public static TimerTask updateUserInfoAvatarFileLog(UserInfo old, UserInfo userInfo) {
            return new TimerTask() {
                @Override
                public void run() {
                    //如果头像地址不一致
                    if (!old.getAvatarUrl().equals(userInfo.getAvatarUrl())) {
                        FileLogUpdate newFileLogUpdate = new FileLogUpdate();
                        newFileLogUpdate.setUserId(userInfo.getUserId());
                        newFileLogUpdate.setTargetId(userInfo.getUserId());
                        newFileLogUpdate.setTargetContent(userInfo.getUserName());
                        //为什么只要压缩图片???因为头像和封面只有压缩图
                        newFileLogUpdate.setThumbnailUrl(userInfo.getAvatarUrl());
                        newFileLogUpdate.setQueryLogType(CFileLogTypeEnum.LOG_TYPE_2.getValue());
                        //新的新增时为冗余,要更新为正常
                        newFileLogUpdate.setQueryLogStatus(CFileLogStatusEnum.LOG_STATUS_0.getValue());
                        newFileLogUpdate.setUpdateLogStatus(CFileLogStatusEnum.LOG_STATUS_1.getValue());
                        SpringUtils.getBean(IFileLogInfoService.class).updateFileLog(newFileLogUpdate);
    
                        FileLogUpdate oldFileLogUpdate = new FileLogUpdate();
                        oldFileLogUpdate.setTargetId(old.getUserId());
                        oldFileLogUpdate.setTargetContent(old.getUserName());
                        oldFileLogUpdate.setThumbnailUrl(old.getAvatarUrl());
                        oldFileLogUpdate.setUserId(old.getUserId());
                        oldFileLogUpdate.setQueryLogType(CFileLogTypeEnum.LOG_TYPE_2.getValue());
                        oldFileLogUpdate.setQueryLogStatus(CFileLogStatusEnum.LOG_STATUS_1.getValue());
                        oldFileLogUpdate.setUpdateLogStatus(CFileLogStatusEnum.LOG_STATUS_0.getValue());
                        SpringUtils.getBean(IFileLogInfoService.class).updateFileLog(oldFileLogUpdate);
                    }
                }
            };
        }

用户模块

1 .登录

  • 账号密码登录,自定义密码加密方式,根据密码加密方式判断密码
  • 手机短信登录
  • 注册
  • 忘记密码

2. 用户详情

  • 多种加密方式加密用户密码
  • 用户详细信息
  • 用户修改自己的信息
  • 用户修改密码
  • 查看最近登录记录
  • 以及各种日志

图片模块

1. 图片信息

  1. 图片上传

    • 图片上传会压缩图片,同时也会保留原图,添加水印,返回图片基本信息

    • 用户上传图片,选择图片文件夹、图片空间、图片标签,如果标签不存在则新增标签

    • 用户上传图片后会异步更新对应空间的使用量,会判断用户空间使用量,对于团队空间,团队内的成员也可以创建,创建者和编辑者

    • 图片分类,如果用户选择了图片分类,会判断图片分类是否存在,状态是否为正常,只有正常的才可以选择

      if (StringUtils.isNotEmpty(pictureInfo.getCategoryId())) {
                  //查询分类是否存在
                  categoryInfo = pictureCategoryInfoService.selectPictureCategoryInfoByCategoryId(pictureInfo.getCategoryId());
                  ThrowUtils.throwIf(StringUtils.isNull(categoryInfo)
                                  || !categoryInfo.getCategoryStatus().equals(PCategoryStatusEnum.CATEGORY_STATUS_0.getValue()),
                          HttpStatus.MOVED_PERM, "分类不存在或不可选");
                  categoryInfo.setUsageCount(categoryInfo.getUsageCount() + 1);
              }
    • 空间文件夹,和分类同理,会判断是否是该空间内文件夹,判断是否有团队空间对应权限

2. 图片详情

  • 用户自己查看详情,获取图片所有信息,接口:

     /**
         * 获取我的图片详细信息
         */
        @PreAuthorize("@uss.hasPermi('picture:upload:detail')")
        @GetMapping("/my/{pictureId}")
        public AjaxResult getMyInfo(@PathVariable("pictureId") String pictureId) {
            UserPictureDetailInfoVo userPictureDetailInfoVo = pictureInfoService.userMySelectPictureInfoByPictureId(pictureId, getUserId());
            return success(userPictureDetailInfoVo);
        }

    会判断做以下判断

      //如果图片不是公共且图片审核状态不是通过,且当前用户不是作者,且不是空间的成员
            if (!userPictureDetailInfoVo.getPictureStatus().equals(PPictureStatusEnum.PICTURE_STATUS_0.getValue())
                    && !userPictureDetailInfoVo.getUserId().equals(UserInfoSecurityUtils.getUserId())
                    && !spaceAuthUtils.checkUserJoinSpace(userPictureDetailInfoVo.getSpaceId())) {
                throw new ServiceException("图片审核不通过,无法查看");
            }
  • 用户查看详情,用户查看详情不会获取所有信息,会判断自己是否点赞收藏

       //查询是否有行为,点赞、收藏
            isBehavior(pictureId, userId, userPictureDetailInfoVo);
            //如果图片不是公共且图片审核状态不是通过,且当前用户不是作者
            if (!userPictureDetailInfoVo.getPictureStatus().equals(PPictureStatusEnum.PICTURE_STATUS_0.getValue())
                    && !userPictureDetailInfoVo.getUserId().equals(UserInfoSecurityUtils.getUserId())
                    && !spaceAuthUtils.checkUserJoinSpace(userPictureDetailInfoVo.getSpaceId())) {
                throw new ServiceException("图片审核不通过,无法查看");
            }
  • 图片缓存,用户查看详情和查看自己图片详情其实本质就是一个,只不过有了不同处理逻辑,其中走的是同一个,都是先查数据库然后存入缓存拿缓存,但是此处缓存因为springAop,所以做了手动缓存。

     //手动设置缓存,这里基本内部调用,spring没有托管,所以需要手动设置缓存
            String key = PICTURE_PICTURE_DETAIL + COMMON_SEPARATOR_CACHE + pictureId;
            if (redisCache.hasKey(key)) {
                return redisCache.getCacheObject(key);
            }
  • 图片更新及快捷更新名字,同理,会删除对应缓存

  • 用户点赞收藏转发会异步更新图片信息,这里使用的是策略模式更新对应信息,每个模式可能不同,需具体实现,同时会存入图片热门统计排行

  • 其他逻辑当前基本同理,没有什么比较复杂逻辑

  • 图片详情增加上传类型,由后端判断,判断是什么类型,比如此AI模块的图片,就是所有的上传信息

3. 图片下载

  • 获取原图链接

    ​ 1.查询图片是否存在且是正常的

            //判断图片1、是否存在,2、是否是作者,如果不是是否是正常,如果也不是判断是否不是空间成员,如果都false判断是否删除
            ThrowUtils.throwIf(StringUtils.isNull(pictureInfo)
                            || ((
                            !pictureInfo.getUserId().equals(userId)
                                    && pictureInfo.getPictureStatus().equals(PSpaceStatusEnum.SPACE_STATUS_1.getValue())
                                    && !spaceAuthUtils.checkUserJoinSpace(pictureInfo.getSpaceId())
                    )
                            || pictureInfo.getIsDelete().equals(CommonDeleteEnum.DELETED.getValue())),
                    "图片不存在");
    1. 执行下载逻辑:作者或者加入空间的无需使用积分,积分大于0需消耗积分,大于等于10才分成积分,且必须是10的倍数
  • 根据下载记录获取原图链接,无需消耗积分,基本逻辑和获取原图链接一样。

4. 图片推荐

  1. 用户推荐模型:图片推荐是指根据用户的推荐模型推荐图片
    • 用户当前点赞,浏览,下载等行为都会给对应图片的标签,分类加分,根据分数为用户推荐图片
    • 默认缓存最热门的分类和图片,可以自定义数量
  2. 图片推荐
    • 根据用户推荐模型,计算给用户推荐的图片并存入缓存
    • 如果没有了直接推荐热门图片
  3. 图片详情推荐
    • 根据图片的标签和分数进行推荐图片
  4. 搜索建议
    • like查询热门图片
    • 搜索推荐:查看热门的图片

5. 空间信息

  1. 空间信息

    • 空间信息新增修改以及列表,空间信息的个数的限制

    • 空间文件夹的新增修改,最多七层

    • 空间封面上传

  2. 团队空间

    • 创建团队空间,用户创建团队空间自己默认成为创建者

    • 邀请别人成为空间成员,编辑者、预览者,在自己空间的成员可以免费查看图片和下载图片

    • 编辑成除了不可以删除其他的都可以,预览者只可以查看

  3. 团队空间权限SpaceAuthUtils

    • 用户同意进入空间之后删除原有的加入空间缓存,后续重新获取并缓存,也需要删除原有的空间权限

    • 获取空间成员的权限、是否包含某权限、任意权限

    • 判断是否有编辑权限、是否为创建者

    • 注意,需要删除的缓存很多,这里要熟读代码,比如表格的缓存,让用户实时看到,权限缓存

    • 关键删除,删除的时候邀请人和用户自己都要删除,不然可能数据不一致

          /**
           * 删除空间权限 用户编号
           */
          public void deleteSpacePerm(String userId) {
              spaceInfoService.deleteSpaceTeamTableCacheByUserId(userId);
              deleteUserJoinSpace(userId);
              deleteSpaceMemberPerm(userId);
          }
          //删除空间成员缓存
          @Override
          public void deleteSpaceMemberCacheBySpaceId(String spaceId) {
              redisCache.deleteObjectsByPattern(PICTURE_SPACE_MEMBER_DATA + COMMON_SEPARATOR_CACHE + spaceId + "*");
          }
            /**
           * 删除团队空间表格缓存
           *
           * @param userId
           * @return void
           * @author: YY
           * @method: deleteSpaceTeamTableCacheByUserId
           * @date: 2025/6/29 18:59
           **/
         @Override
          public void deleteSpaceTeamTableCacheByUserId(String userId) {
              redisCache.deleteObjectsByPattern(PICTURE_SPACE_TEAM_TABLE_DATA + COMMON_SEPARATOR_CACHE + userId + "*");
              redisCache.deleteObjectsByPattern(PICTURE_SPACE_LIST + COMMON_SEPARATOR_CACHE + userId + "*");
          }
                  spaceAuthUtils.deleteSpacePerm(db.getUserId());
                  spaceInfoService.deleteSpaceTeamTableCacheByUserId(db.getUserId());
                  spaceMemberInfoService.deleteSpaceMemberCacheBySpaceId(db.getSpaceId());

    4.前端使用

    /**
     * 检查是否有指定空间权限
     */
    export function checkSpacePerm(perm: string): boolean {
      try {
        // console.log('检查权限:', perm)
        // console.log('已加载权限:', spacePerm.getSpacePerms())
        return spacePerm.getSpacePerms().includes(perm)
      } catch {
        console.warn('权限未加载,暂时拒绝访问')
        return false
      }
    /**
     * 检查是否拥有任意一组权限
     */
    export function checkSpacePermsAny(perms: string[]): boolean {
      try {
        for (const perm of perms) {
          if (checkSpacePerm(perm)) {
            return true
          }
        }
        return false
      } catch {
        return false
      }
    }
    /**
     * 构建空间权限
     * @param spaceId 空间编号
     * @param roleType 角色类型
     */
    export function buildSpacePermByUser(spaceId: string, roleType: string) {
      return buildSpacePerm(useUserStore().userId, spaceId, roleType)
    }
    export function buildSpacePerm(userId: string, spaceId: string, roleType: string) {
      return userId + ':' + spaceId + ':' + roleType
    }
    /**
     * 判断是否可以编辑
     */
    export function checkSpaceEditor(spaceId: string): boolean {
      return checkSpacePermsAny([
        buildSpacePermByUser(spaceId, PSpaceRole.SPACE_ROLE_1),
        buildSpacePermByUser(spaceId, PSpaceRole.SPACE_ROLE_0),
      ])
    }
    /**
     * 判断是否是创建则
     */
    export function checkSpaceCreator(spaceId: string): boolean {
      return checkSpacePerm(buildSpacePermByUser(spaceId, PSpaceRole.SPACE_ROLE_0))
    }

积分模块

1. 积分套餐

  • 积分套餐新增修改:积分套餐也有赠送积分。

  • 用户选择积分套餐可以进行充值

  • 定时任务,每天定时更新积分套餐

2. 积分充值

  1. 用户选择积分套餐后进行充值

  2. 支付

    1. 用户首先选择积分套餐,选择完成后选择支付渠道,例:支付宝
    2. 选择支付渠道后开始进行支付,后台创建支付订单并向支付宝发起支付请求,请求成功后创建支付订单,存订单如缓存并,返回订单号给前端,前端跳转支付页面并轮训获取订单状态,支付成功后支付页面跳转套餐页面,原页面获取到订单支付成功后跳转下一步,用户点击返回返回套餐页面,此时后端任务支付宝回调接口后更新订单状态并充值。
    3. 如果原来用户没有账号为用户创建账户。
    4. 充值成功
    5. 异常捕获
    • 捕获支付期间所有的异常
  3. 积分记录

  • 记录积分充值记录,金额,充值数
  • 积分使用记录:记录用户积分使用记录,使用之前多少,使用之后多少

AI模块

AI模块采用深色主题,其实可以实现深色主题和浅色主题切换,但大多前端都是AI生成的页面结构,重新构造比较麻烦,所以当前尚未实现,后续可以实现。

1. AI生图

  1. AI生图接口在图片模块,因为后续需要对图片操作;
  2. 生成任务创建之后直接扣取用户积分,如果查询任务失败则返回用户积分,如果使用别人的作品参考,奖励作者积分;
  3. 可以批量选择模型,也可以批量生成图片,每个请求可以自定义最多并发。
  4. 生成成功之后会自动上传到oss服务器,同时会上传压缩图。
  • 文生图\图生图

​ 目前主要采用即梦AI生图,采用配置形式生图 image-20250820232355828

每个配置拥有详细信息,主要是模型参数,对应接口所需要参数。

模型参数:

{
   "req_key":"jimeng_i2i_v30",
   "action":"CVSync2AsyncSubmitTask",
   "version":"2022-08-31",
   "queryAction":"CVSync2AsyncGetResult",
   "prompt":"",
   "binary_data_base64":"",
   "image_urls":"",
   "seed": -1,
   "width":512,
   "height":512,
   "use_pre_llm":false,
   "use_sr":true,
   "return_url":true,
   "path":"/",
   "service":"cv",
   "region":"cn-north-1",
   "schema":"https",
   "method":"POST",
   "host":"visual.volcengineapi.com"
}
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class JiMengParams extends Params<JiMengParams> {
    //算法名称
    private String req_key;

    //开启文本扩写
    private Boolean use_pre_llm;

    //随机种子
    private Integer seed;

    //接口名
    private String action;
    private String queryAction;

    //版本号
    private String version;

    private String method;

    private boolean return_url;

    private String region;
    private String service;
    private String schema;
    private String host;
    private String path;
    private String ak;
    private String sk;
    private String task_id;

    private String req_json;

    private String[] image_urls;

    private String[] binary_data_base64;

    @Data
    public static class ReqJson {
        private boolean return_url;
    }

    @Override
    public JiMengParams jsonToObj(String json) {
        return JSONObject.parseObject(json, JiMengParams.class);
    }

    @Override
    public String objToJson(JiMengParams params) {
        return JSONObject.toJSONString(params);
    }
}

扩展配置

{
  "maxWidth":2016,
   "minWidth":512,
   "maxHeight":2016,
   "minHeight":512,
   "promptLength":800,
   "maxNumbers":1,
}
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class JiMengVerify extends Verify<JiMengVerify> {
    //最大数量
    private Integer maxNumbers;
    @Override
    public String objToJson(JiMengVerify verify) {
        return JSONObject.toJSONString(verify);
    }

    @Override
    public JiMengVerify jsonToObj(String json) {
        return JSONObject.parseObject(json, JiMengVerify.class);
    }

}

根据策略模式调用不同的模型,这样只要之后有新的调用接口,可以直接加AI模型参数配置即可,无需编辑代码,同理如果需要不同平台,只需要实现此平台的代码,所有接口都可以调用,提高开发效率。

    public List<GenerateLogInfo> executeUserGenerate(AiGenerateRequest request) {
        //首先查询到所有的模型参数
        List<String> modelKeys = request.getModelKeys();
        List<GenerateLogInfoDto> generateLogInfoDtos = new ArrayList<>();
        ArrayList<GenerateLogInfo> logInfos = new ArrayList<>();
        Long totalPoints = 0L;
        for (String modelKey : modelKeys) {
            //查询每个key是否存在
            ModelParamsInfo modelParamsInfo = modelParamsInfoService.selectModelParamsInfoByModelKey(modelKey);
            ThrowUtils.throwIf(StringUtils.isNull(modelParamsInfo) ||
                            !modelParamsInfo.getParamsStatus().equals(AiModelParamsStatusEnum.MODEL_PARAMS_STATUS_0.getValue()),
                    HttpStatus.NO_CONTENT,
                    "模型参数不存在或者未启用");
            GenerateLogInfoDto generateLogInfoDto = new GenerateLogInfoDto(request, modelParamsInfo);
            generateLogInfoDtos.add(generateLogInfoDto);
            totalPoints += modelParamsInfo.getPointsNeed() * request.getNumbers();
        }
        //判断用户积分是否足够 查询用户账户是否存在,存在判断积分
        AccountInfo accountInfo = accountInfoService.selectAccountInfoByUserId(request.getUserId());

        ThrowUtils.throwIf(StringUtils.isNull(accountInfo)
                || accountInfo.getPointsBalance() < totalPoints, "积分不足");
        for (GenerateLogInfoDto info : generateLogInfoDtos) {
            //遍历执行,拿到对应的执行器
            for (AiGenerateStrategyService strategyService : aiGenerateStrategyServiceList) {
                AiGenerateStrategyConfig annotation = strategyService.getClass().getAnnotation(AiGenerateStrategyConfig.class);
                if (annotation.model().equals(info.getModel())) {
                    List<GenerateLogInfo> generateLogInfos = strategyService.userGenerate(info);
                    logInfos.addAll(generateLogInfos);
                }
            }
        }
        return logInfos;
    }
  • 查询任务

​ 查询任务和生图同理。以实现的即梦AI为例,生图有同步和异步,同步生成成功后直接成功,异步则需要查询任务,同时,后台会有定时任务每六小时查询一次。

2. 发布作品

在我的资产可以直接发布作品,可以设置图片为公共还是私有,公共后台会自动申请图片为公共,后台可以直接审核,私有后续想公开则需要自己重新申请,无论公共还是私有,都要选择分类,空间,因为是进入图片模块,当前这里没有实现团队空间,但是也可以把图片上传到团队空间,用户共同管理图片。

3. 我的

我的则是可以查看自己的作品,同理也可以在ai图片详情跳转到别人的主页查看别人的作品,只可以查看公共作品,私有不可以看。

4. 发现/灵感

其实和图片模块的主页和搜索一样,只不过是必须查看AI生成的而已。

配置信息

docker run -p 6379:6379 --name redis8.0.2 -v /code/container/redis/conf/redis.conf:/etc/redis/redis.conf -v /code/container/redis/data:/data -v /code/container/redis/logs:/logs -d redis:8.0.2 redis-server /etc/redis/redis.conf
docker run -p 3306:3306 --name mysql --restart always -v /code/container/mysql/conf/my.cnf:/etc/mysql/my.cnf -v /code/container/mysql/logs:/logs -v /code/container/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=mysql1234 -d mysql:8.0.36
nohup java \
-Xms256m \
-Xmx512m \
-XX:MaxMetaspaceSize=192m \
-XX:MaxDirectMemorySize=64m \
-XX:+UseParallelGC \
-XX:+TieredCompilation \
-XX:TieredStopAtLevel=1 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=./dump.hprof \
-XX:+ExitOnOutOfMemoryError \
-XX:ThreadStackSize=256 \
-Dfile.encoding=UTF-8 \
-jar picture-admin.jar > nohup.out 2>&1 &

统计大屏

  • 注意数据大屏地图信息在docments/geoJson.zip,使用前你需要把文件解压放在你的本地,并没有使用第三方获取geoJson,需要把文件放在对应的位置image-20250926194528903

    image-20250926194607807

积分模块统计

image-20250926194037815

图片模块统计

image-20250926194129901

用户模块统计

image-20250926194206170

Jay词云图

这个单纯自己做着玩

image-20250926194250957

项目启动

根据本项目的基础可知,本项目基础是根据若依为基础二次开发,所以启动基本上和若依一致,以下是启动步骤。

1. 后端启动

  • 数据库初始化:在docment目录下拿到初始化sql,创建数据库并执行sql

  • 开启redis:修改你的redis配置

  • 注意:还有一个application-config.yml文件(此文件和application.yml同级),都是比较关键的信息,git上并未提交,你可以根据以下内容配置自己的信息,如果尚未改好是不可以运行的。

    aliyun:
      # 阿里云
      access-key-id: *****
      # 阿里云密钥
      access-key-secret: ******
      # 阿里云API
      api-key: sk-******
      # 阿里云OSS文件夹,最顶级
      dir: picture
      # 阿里云OSS
      endpoint: "https://oss-cn-guangzhou.aliyuncs.com"
      # 阿里云OSS域名
      dns-url: "******"
      # 阿里云OSS存储桶
      bucket: ******
      # 阿里云OSS区域
      region: ap-guangzhou
    
    alipay:
      # 支付宝私钥文件路径
      privateKeyPath: E:/Project/Picture/alipay_sb_private.txt
      # 支付宝公钥文件路径
      publicKeyPath: E:/Project/Picture/alipay_sb_public.txt
      # 支付宝网关
      serverUrl: "https://openapi.alipay.com/gateway.do"
    #  serverUrl: "https://openapi-sandbox.dl.alipaydev.com/gateway.do"
      # 应用ID
      appId: "******"
      # 异步回调地址
      notifyUrl: "https://*****/user/points/pay/alipay/callback"
      # 同步回调地址
      returnUrl: "https://******/user/points/pay/alipay/callback"
      # 重定向地址,这里我是直接跳转的
      redirectUrl: "http://localhost/points"
      format: "json"
      charset: "UTF-8"
      signType: "RSA2"
      productCode: "FAST_INSTANT_TRADE_PAY"
      timeoutExpress: "5m"
      connectTimeout: 30000
      readTimeout: 30000

2. 前端启动

  • 前端启动较为简单,若依原本使用yarn,我这里使用的是npm:

    npm install
    
  • 运行,直接去对应的package.json运行就行

  • 注意:我的node版本为18.18.2,如果有问题尽量保证和我版本一致,使用nvm做版本管理node我觉得更好。

最后

​ 本项目设计比较临乱,有一些功能只是设计了数据库,功能尚未实现,但所有功能都是自己独立开发,且有些功能都是后面才想到的,期间也放弃了很多功能,也是自己当前独立开发的最大一个项目,如果有什么疑问,或者缺点,也欢迎您提出,十分感谢。

这里有我们工作室的名片,如果有需要也可以联系我们:

1

2

About

荔枝云图库,在线管理图片,AI生成图片,数据大屏等

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages