[TOC]
LZ-Picture(荔枝云图库)是一个综合性的云端图像管理平台,用户可以上传、整理、分享和发现图片。结合了现代网络技术和AI能力,打造了一个功能丰富的图像存储库,拥有面向用户和管理员的双重界面。
LZ-Picture是一个全栈应用程序,提供以下功能:
- 图像管理:上传、整理、压缩和水印图片
- 用户空间:个性化图片画廊和文件夹组织
- 社交功能:点赞、收藏、分享和下载图片
- 积分系统:基于积分的经济体系,用于下载和高级功能
- AI集成:高级图像分析和处理
- 管理工具:完整后端管理系统
- 数据大屏:echarts/vue3数据统计大屏
该平台采用模块化系统构建,后端服务和前端界面清晰分离,遵循企业应用开发的最佳实践。
本项目基于ruoyi-vue(https://doc.ruoyi.vip/ruoyi-vue/) 二次开发,十分感谢若依的开源精神,大大提高了本项目的开发进度。
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缓存。
- 自定义排序:使用注解自定义排序。
-
- 缓存每个单词统一使用:分割,最后一个使用:Constants.COMMON_SEPARATOR_CACHE公共字段拼接所需内容,因为自定义缓存是有自动添加COMMON_SEPARATOR_CACHE的
- 首个单词必须为每个模块的模块名,比如ai,命名为:ai:ai:....
- 注意:如果是若依自带的,缓存默认会加上:sys_config
- 缓存缓存时间统一为秒
-
- 类字段统一采用驼峰命名
- Constants字段统一全部大写加_分割单词
- 枚举命名:类为每个模块单词的首字母加上该字典类型,字段为该字典类型+值,例:类名PPictureReviewStatus,字段:PICTURE_REVIEW_STATUS_0
- 方法有两个相似的必须提出来当方法,减少代码耦合
- 逻辑复杂或者处理同一个内容需要多个步骤的需要抽出方法,方便调用、在其他地方使用这个逻辑时能够直接调用,降低bug率
- 一个方法行数实际代码数量不得超过一百行
-
-
文件命名规范:每个模块会有模块的文件夹,比如图片模块picture文件夹,下面加上他的文件,picture.vue这样,如果是详情,pictureDetail.vue,采用驼峰命名,同时如果多个相似要放在同一个文件夹,比如图片模块的图库,就可以放在图库picture/space/space.vue
-
vue模块:js必须要有name,模块+文件名,template最外层div必须加class,并且class命名为横杠来分割单词,模块+文件名例:forgetPassword,user-forgot-password
-
css使用scss
-
-
- 采用若依当前规则即可
- css使用scss
-
- 权限标识:路由地址+对应功能
- 配置键名:模块:功能:功能
-
- 公共使用的方法必须提到对应的公共方法类或者ts、js文件
- 参数不能使用基类,必须使用对应的vo、dto,前端同理使用type,防止参数耦合
- 前端使用多的,相似度高的地方要提取为组件
/**
* 自定义缓存注解,父类字段无法拿到
* 坚持是前行的舟
*/
@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 的一部分
}/**
* 自定义缓存删除注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CustomCacheEvict {
String[] keyPrefixes();
String[] keyFields() default {}; // 支持嵌套,如 "request.userId"
boolean useQueryParamsAsKey() default false; // 如果true,加上参数JSON串模糊删除
}- 注解
/**
* 自定义排序注解
* 首先我希望我变好,也希望你
*
* @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 {}; //映射字段,前端只需要传过来字段名,如果多表查询设置映射字段,映射字段为查询时所需字段
}- 查询mapper
<choose>
<when test="params.sortSql != null and params.sortSql != ''">
${params.sortSql}
</when>
<otherwise>
order by create_time desc
</otherwise>
</choose>- 使用
@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);
}-
前端使用,此注解当前主要使用在管理端
-
在el-table加上@sort-change="customSort"自定义排序方法和ref
<el-table ref="tableRef" v-loading="loading" :data="accountInfoList" @selection-change="handleSelectionChange" @sort-change="customSort">- 在字段加上自定义排序标志
<el-table-column label="充值总金额" align="center" prop="rechargeAmount" sortable="custom"
v-if="columns[6].visible" :show-overflow-tooltip="true"/>- 排序字段
const isAsc = ref();
const orderByColumn = ref('');- 新增方法
//自定义排序
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();
}- 在getList添加
if (orderByColumn.value != null && isAsc.value !== null) {
queryParams.value.params["orderByColumn"] = orderByColumn.value;
queryParams.value.params["isAsc"] = isAsc.value;
}- 在重置按钮更新ref
/** 重置按钮操作 */
function resetQuery() {
daterangeCreateTime.value = [];
daterangeUpdateTime.value = [];
orderByColumn.value = null
isAsc.value = null;
proxy.resetForm("queryRef");
proxy.$refs.tableRef.clearSort();
handleQuery();
} 后端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(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());
}
}
}/**
* 异常处理工具类
*
* @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));
}
}-
配置信息存入缓存信息,name是键名,value是值,键值对
-
值可以是文件和值
-
初始化项目加载所有配置信息
-
根据order_num升序
-
配置信息添加是否内置,如果内置则用户端不可以通过接口获取到数据
-
新增及使用方式:
-
首先新增key,例如:user:login:captchaEnabled
-
在类ConfigInfoUtils添加对应字段和默认值
public static final String USER_LOGIN_CAPTCHA_ENABLED_KEY = "user:login:captchaEnabled"; public static Boolean USER_LOGIN_CAPTCHA_ENABLED_VALUE = true;
-
在refreshConfig新增,即完成
USER_LOGIN_CAPTCHA_ENABLED_VALUE = getBool(USER_LOGIN_CAPTCHA_ENABLED_KEY); -
使用直接调用USER_LOGIN_CAPTCHA_ENABLED_VALUE
-
用户前端使用方式
-
用户第一次获取配置之后会存入本地缓存,在utils/config.js
-
存入缓存stores/config.js
-
使用方式:
titleDesc.value = await useConfig('picture:report:content')
-
-
-
- 创建规则:根据提示创建
- 动态路由,为用户端实现动态路由
- 权限管理,菜单作为权限管理
- 缓存,刷新缓存
- 状态和是否显示都可以作为改菜单是否有权限,是否开启权限,是否显示对应接口,如果不显示表示这个接口不开启
- 自定义路由信息,根据用户权限
- 写死的路由不做排序,但是有前后之分,动态路由是中间的,有排序的
当前尚未完全实现。
- 简称是唯一的
- 根据order_num升序
- 键是唯一的
- 后续国际化信息根据国家(简称)+键名,同时组成唯一键 (待完成)
- 信息需要存入缓存,根据国家作为redis key存入 map key(键名)——value(信息)
- 新增国际化信息并存入缓存,项目启动初始化至缓存
- 可以新增,生成事例,根据变量生成事例
- 回退对应版本
- 公告没有模板,基本填写,不为所有用户推送,所有用户可以查看
- 前端每日会刷新一次,每日会通知一次
- 前端位置components/NoticeWindows.vue
-
记录文件上传的日志,防止冗余图片,比如头像、图片、封面
-
使用方式,异步执行:
//异步执行存入文件日志 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); } } }; }
- 账号密码登录,自定义密码加密方式,根据密码加密方式判断密码
- 手机短信登录
- 注册
- 忘记密码
- 多种加密方式加密用户密码
- 用户详细信息
- 用户修改自己的信息
- 用户修改密码
- 查看最近登录记录
- 以及各种日志
-
-
图片上传会压缩图片,同时也会保留原图,添加水印,返回图片基本信息
-
用户上传图片,选择图片文件夹、图片空间、图片标签,如果标签不存在则新增标签
-
用户上传图片后会异步更新对应空间的使用量,会判断用户空间使用量,对于团队空间,团队内的成员也可以创建,创建者和编辑者
-
图片分类,如果用户选择了图片分类,会判断图片分类是否存在,状态是否为正常,只有正常的才可以选择
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); }
-
空间文件夹,和分类同理,会判断是否是该空间内文件夹,判断是否有团队空间对应权限
-
-
用户自己查看详情,获取图片所有信息,接口:
/** * 获取我的图片详细信息 */ @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模块的图片,就是所有的上传信息
-
获取原图链接
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())), "图片不存在");
- 执行下载逻辑:作者或者加入空间的无需使用积分,积分大于0需消耗积分,大于等于10才分成积分,且必须是10的倍数
-
根据下载记录获取原图链接,无需消耗积分,基本逻辑和获取原图链接一样。
- 用户推荐模型:图片推荐是指根据用户的推荐模型推荐图片
- 用户当前点赞,浏览,下载等行为都会给对应图片的标签,分类加分,根据分数为用户推荐图片
- 默认缓存最热门的分类和图片,可以自定义数量
- 图片推荐
- 根据用户推荐模型,计算给用户推荐的图片并存入缓存
- 如果没有了直接推荐热门图片
- 图片详情推荐
- 根据图片的标签和分数进行推荐图片
- 搜索建议
- like查询热门图片
- 搜索推荐:查看热门的图片
-
-
空间信息新增修改以及列表,空间信息的个数的限制
-
空间文件夹的新增修改,最多七层
-
空间封面上传
-
-
-
创建团队空间,用户创建团队空间自己默认成为创建者
-
邀请别人成为空间成员,编辑者、预览者,在自己空间的成员可以免费查看图片和下载图片
-
编辑成除了不可以删除其他的都可以,预览者只可以查看
-
-
-
用户同意进入空间之后删除原有的加入空间缓存,后续重新获取并缓存,也需要删除原有的空间权限
-
获取空间成员的权限、是否包含某权限、任意权限
-
判断是否有编辑权限、是否为创建者
-
注意,需要删除的缓存很多,这里要熟读代码,比如表格的缓存,让用户实时看到,权限缓存
-
关键删除,删除的时候邀请人和用户自己都要删除,不然可能数据不一致
/** * 删除空间权限 用户编号 */ 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());
/** * 检查是否有指定空间权限 */ 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)) }
-
-
积分套餐新增修改:积分套餐也有赠送积分。
-
用户选择积分套餐可以进行充值
-
定时任务,每天定时更新积分套餐
-
用户选择积分套餐后进行充值
-
支付
- 用户首先选择积分套餐,选择完成后选择支付渠道,例:支付宝
- 选择支付渠道后开始进行支付,后台创建支付订单并向支付宝发起支付请求,请求成功后创建支付订单,存订单如缓存并,返回订单号给前端,前端跳转支付页面并轮训获取订单状态,支付成功后支付页面跳转套餐页面,原页面获取到订单支付成功后跳转下一步,用户点击返回返回套餐页面,此时后端任务支付宝回调接口后更新订单状态并充值。
- 如果原来用户没有账号为用户创建账户。
- 充值成功
- 异常捕获
- 捕获支付期间所有的异常
-
积分记录
- 记录积分充值记录,金额,充值数
- 积分使用记录:记录用户积分使用记录,使用之前多少,使用之后多少
AI模块采用深色主题,其实可以实现深色主题和浅色主题切换,但大多前端都是AI生成的页面结构,重新构造比较麻烦,所以当前尚未实现,后续可以实现。
- AI生图接口在图片模块,因为后续需要对图片操作;
- 生成任务创建之后直接扣取用户积分,如果查询任务失败则返回用户积分,如果使用别人的作品参考,奖励作者积分;
- 可以批量选择模型,也可以批量生成图片,每个请求可以自定义最多并发。
- 生成成功之后会自动上传到oss服务器,同时会上传压缩图。
每个配置拥有详细信息,主要是模型参数,对应接口所需要参数。
模型参数:
{
"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为例,生图有同步和异步,同步生成成功后直接成功,异步则需要查询任务,同时,后台会有定时任务每六小时查询一次。
在我的资产可以直接发布作品,可以设置图片为公共还是私有,公共后台会自动申请图片为公共,后台可以直接审核,私有后续想公开则需要自己重新申请,无论公共还是私有,都要选择分类,空间,因为是进入图片模块,当前这里没有实现团队空间,但是也可以把图片上传到团队空间,用户共同管理图片。
我的则是可以查看自己的作品,同理也可以在ai图片详情跳转到别人的主页查看别人的作品,只可以查看公共作品,私有不可以看。
其实和图片模块的主页和搜索一样,只不过是必须查看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 &
这个单纯自己做着玩
根据本项目的基础可知,本项目基础是根据若依为基础二次开发,所以启动基本上和若依一致,以下是启动步骤。
-
数据库初始化:在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
-
前端启动较为简单,若依原本使用yarn,我这里使用的是npm:
npm install -
运行,直接去对应的package.json运行就行
-
注意:我的node版本为18.18.2,如果有问题尽量保证和我版本一致,使用nvm做版本管理node我觉得更好。
本项目设计比较临乱,有一些功能只是设计了数据库,功能尚未实现,但所有功能都是自己独立开发,且有些功能都是后面才想到的,期间也放弃了很多功能,也是自己当前独立开发的最大一个项目,如果有什么疑问,或者缺点,也欢迎您提出,十分感谢。
这里有我们工作室的名片,如果有需要也可以联系我们:








