Compare commits

...

87 Commits

Author SHA1 Message Date
otto cbb052b3a7 Merge pull request '小强社区初始化版本' (#6) from otto/microservices:xiaoqiang into xiaoqiang 2026-07-16 09:32:55 +08:00
otto 6fda7bdfea feat(分片上传): 优化分片上传功能
1. 如果分片重复上传, 每次上传都 +1, 存在重复计数风险
2. 分片合并时缓冲区较小,使用 BufferedOutputStream 包装,缓冲区更大
3.  临时文件清理使用 new Thread() 而非线程池:复用线程池
4. 上传文件路径安全校验: 禁止路径穿越、禁止路径分隔符、校验文件大小
2026-07-16 09:12:32 +08:00
otto 69d561e6c0 feat(小强社区): 更新README.md文档,更新当前项目的系统架构 2026-07-15 16:44:01 +08:00
otto ff241bd8be feat(小强社区): 将AI资源广场相关代码合并到主干分支,作为小强社区开发的起点分支 2026-07-13 10:56:04 +08:00
otto 197f6c7965 Merge branch 'aiResourceMRCS' of code.gitlink.org.cn:Gitlink/microservices into master_merge
# Conflicts:
#	microservices-modules/microservices-modules-cms/src/main/java/com/microservices/cms/doc/service/impl/CmsDocServiceImpl.java
#	microservices-modules/microservices-modules-file/src/main/java/com/microservices/file/service/impl/SysFileInfoServiceImpl.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/FrontController.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/detail/service/impl/ZoneDetailServiceImpl.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/project/domain/vo/ZoneProjectSearchVo.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/mapper/ZoneResourceMapper.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/service/impl/ZoneResourceServiceImpl.java
#	microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneProjectMapper.xml
#	microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneResourceMapper.xml
2026-07-13 10:29:00 +08:00
otto 194d706469 Merge branch 'bug_fix' of code.gitlink.org.cn:otto/microservices into master_merge
# Conflicts:
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/service/IZoneAsyncService.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/service/impl/ZoneAsyncServiceImpl.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/project/service/impl/ZoneProjectServiceImpl.java
2026-07-10 11:25:34 +08:00
otto c1cb8a6fa7 Merge pull request '创建文章时新增内部参数publishDate' (#2) from otto/microservices:aiResourceMRCS into aiResourceMRCS 2026-07-02 15:35:51 +08:00
otto 318d565cd6 Merge branch 'aiResourceMRCS' of code.gitlink.org.cn:Gitlink/microservices into aiResourceMRCS 2026-07-02 15:33:19 +08:00
otto e9021d5b50 feat(可控开源社区): 创建文章时新增内部参数publishDate,内部调用该接口时可通过修改publishDate来设置文章的发布、创建、更新时间 2026-07-02 15:32:21 +08:00
otto 64d5b72a41 Merge pull request '文件下载记录及资源详情下载用户统计' (#1) from wanjia9506/microservices:aiResourceMRCS into aiResourceMRCS 2026-06-30 16:12:49 +08:00
wanjia 052ede9a3c feat: downloadUserSummary 增加 fileOriginName 字段 2026-06-29 13:52:58 +08:00
wanjia c7be0e383c fix: 参考zone模块通过GitLink Token获取下载用户信息 2026-06-29 11:55:15 +08:00
wanjia 42d1fcf1ac revert: 恢复使用 SecurityUtils.getUsername 获取下载用户信息 2026-06-29 11:47:34 +08:00
wanjia d3c378bad7 fix: 开放接口下载日志从请求token获取用户信息替代SecurityContext 2026-06-29 11:30:28 +08:00
wanjia 36a1f70963 feat: 下载日志增加userId,下载记录返回用户昵称/手机号/邮箱
- sys_file_download_log 表新增 user_id 字段
- recordDownloadLog 记录登录用户的 userId
- SysFileDownloadLogVo 新增 userId/nickName/phonenumber/email 字段
- populateDownloadUsers 通过 RemoteUserService 逐用户查询补全详情
2026-06-29 11:21:44 +08:00
wanjia c9c237c901 fix: 下载日志记录移到文件写入之前,避免写文件异常导致日志丢失 2026-06-29 11:05:57 +08:00
wanjia e388ee1560 refactor: 下载记录改为明细列表,新增SysFileDownloadLogVo
- 新增 SysFileDownloadLogVo 跨服务共享VO(fileId/userName/ipAddr/downloadTime)
- getDownloadUserListByFileIds 返回类型从 Map<Long,List<String>> 改为 List<SysFileDownloadLogVo>
- downloadUserSummary 改为下载明细列表,直接展示每条的附件ID、用户、IP、时间
- 未登录用户不记录用户名,仅记录IP
- fileList[*].downloadUserList 保留(按附件去重的用户名列表)
- 同步更新 Feign接口、FallbackFactory、所有引用处
2026-06-29 10:52:11 +08:00
wanjia 0a39106b05 feat: 新增文件下载记录及资源详情下载用户统计
- 新建 sys_file_download_log 表,记录每次下载的用户名和IP
- 登录用户记录用户名+IP,未登录用户仅记录IP
- 文件下载接口(fileDownloadById/fileDownloadByIdentifier)自动记录下载日志
- 资源详情接口(getInfo)返回附件下载用户列表,通过Feign查询文件微服务
- 新增 RemoteFileService.getDownloadUserList Feign接口及降级实现
2026-06-29 10:26:08 +08:00
otto b72dd69408 fix(超算专区): 修复文章已存在未判断未通过文章的情况 2026-06-26 09:06:43 +08:00
otto d0e44d8d2a fix(超算专区): 修复文章已存在未判断未通过文章的情况 2026-06-26 08:55:58 +08:00
otto e98663e87b fix(AI资源广场): 修正zip组件方法调用 2026-06-01 09:49:49 +08:00
otto a5e2efe2df feat(AI资源广场): 优化AI资源排序 2026-05-28 10:07:47 +08:00
otto 6f881dad5c feat(AI资源广场): 修复解析SKILL.md内容时,部分情况下会出现乱码的问题 2026-05-28 09:06:16 +08:00
otto 1883e78fcb feat(AI资源广场): 解析Skill压缩包时应该仅解析一级目录和二级目录下的Skill.md(大部分Skill压缩包二级目录才为根目录,所以当一级目录不存在Skill.md时再解析二级目录的) 2026-05-27 17:27:13 +08:00
otto 2b2cb706aa feat(AI资源广场): 解析Skill.md的描述时,处理多行文本情况 2026-05-27 16:44:21 +08:00
otto 1c414c2327 feat(AI资源广场): Skill文件大小限制调整为50Mb 2026-05-27 16:24:22 +08:00
otto bb23a02b45 feat(AI资源广场): 暂时不对Skill简介长度进行限制 2026-05-27 16:13:51 +08:00
otto 3b2b3c7d99 feat(AI资源广场): 暂时不对Skill简介长度进行限制 2026-05-27 16:08:28 +08:00
otto b7b2600df6 feat(AI资源广场): 允许资源在不同的资源类型(数据集/模型/SKILL)下可以重名 2026-05-26 14:05:38 +08:00
otto 7357093ab1 feat(AI资源广场): 优化模型/数据集/SKILL压缩包内目录树排序逻辑:文件夹在前、文件在后,同类型按名称字母不区分大小写排序 2026-05-26 09:02:39 +08:00
otto 675a080b29 feat(AI资源广场): 模型/数据集/SKILL简介允许为空 2026-05-25 17:21:08 +08:00
otto a349b2e88f feat(AI资源广场): 优化模型/数据集/SKILL压缩包内目录树排序逻辑:文件夹在前、文件在后,同类型按名称字母不区分大小写排序 2026-05-25 16:57:40 +08:00
otto 0d042f50d6 feat(AI资源广场): SKILL简介允许为空 2026-05-25 16:44:40 +08:00
otto 5459afcf7a feat(AI资源广场): 模型和数据集资源相关功能开发
调整模型/数据集编辑接口逻辑,支持修改模型/数据集的介绍内容(README.md),将覆盖模型/数据集extendData中的content字段
2026-05-25 16:24:35 +08:00
otto 7b112fab92 feat(AI资源广场): 模型和数据集资源相关功能开发
修复查询我的项目聚合统计时漏传专区id的bug
2026-05-25 10:26:28 +08:00
otto 4344217563 feat(AI资源广场): 模型和数据集资源相关功能开发
新增我的项目聚合统计接口开发
2026-05-25 09:41:20 +08:00
otto b73eece682 feat(AI资源广场): 模型和数据集资源相关功能开发
新增资源(SKILL/模型/数据集)统计接口开发
2026-05-22 14:51:01 +08:00
otto c7983bd62c feat(AI资源广场): 模型和数据集资源相关功能开发
资源的extendData中模型/数据集的fileTreeJson使用JSONObject格式存储
2026-05-21 17:30:56 +08:00
otto 7b81b7c73e feat(AI资源广场): 模型和数据集资源相关功能开发
资源的extendData中SKILL/模型/数据集均使用大写
2026-05-21 17:22:18 +08:00
otto 659293e2f6 feat(AI资源广场): 模型和数据集资源相关功能开发
资源领域支持不同领域分类下存在同名资源领域
2026-05-21 16:11:56 +08:00
otto d2826fccbd feat(AI资源广场): 模型和数据集资源相关功能开发
调整公开查询资源列表和资源详情接口,兼容Skill/模型/数据集
2026-05-21 16:00:18 +08:00
otto 266e0a86a7 feat(AI资源广场): 模型和数据集资源相关功能开发
AI资源判断增加模型和数据集
2026-05-21 15:31:37 +08:00
otto eef339d228 feat(AI资源广场): 模型和数据集资源相关功能开发
修复commons包的版本问题
2026-05-21 15:28:13 +08:00
otto 1fa2facb27 feat(AI资源广场): 模型和数据集资源相关功能开发
Skill的提交、撤回、下架、上架接口兼容模型和数据集
2026-05-20 09:43:31 +08:00
otto 7c0572fae1 feat(分片上传功能优化): 分片上传秒传成功时新增一条数据库文件记录,指向同一物理文件 2026-05-20 09:29:27 +08:00
otto cd48af644e fix(微服务架构优化): 修复日志系统的JSON解析错误问题 2026-05-20 08:48:46 +08:00
otto c9f1566094 fix(微服务架构优化): 修复日志系统的JSON解析错误问题 2026-05-20 08:42:11 +08:00
otto 3ad087a325 feat(AI资源广场): 模型和数据集资源相关功能开发
调整模型/数据集前台接口逻辑
2026-05-20 08:32:40 +08:00
otto a9dd3c779e feat(AI资源广场): 模型和数据集资源相关功能开发
Skill的提交、撤回、下架、上架接口兼容模型和数据集
2026-05-19 11:14:13 +08:00
otto 96a11998f3 feat(AI资源广场): 模型和数据集资源相关功能开发
完成资源/数据集新建、修改接口开发
2026-05-18 16:37:32 +08:00
otto ec3249fab4 style(AI资源广场): 模型和数据集资源相关功能开发
AiSkill解析方法重命名为AiResource解析方法
2026-05-18 10:43:28 +08:00
otto fac5f3158f feat(AI资源广场): 模型和数据集资源相关功能开发
检索根目录README.md文件时需要忽略大小写
2026-05-18 10:01:26 +08:00
otto 084042891d feat(AI资源广场): 模型和数据集资源相关功能开发
新增解析已上传的模型/数据集ZIP包分析接口:
使用commons-compress组件可以在不解压压缩包的前提下,对超大压缩包通过读取 Central Directory方式快速获取压缩包内,同时直接从根目录直接提取README.md文件
2026-05-18 09:17:17 +08:00
otto ea17f00fd8 feat(AI资源广场): Skill资源相关功能开发
优化资源及资源领域分类逻辑:
新增SKILL资源时,限制领域必须为SKILL类型
2026-05-13 15:42:43 +08:00
otto 0a31382003 feat(AI资源广场): Skill资源相关功能开发
优化资源及资源领域分类逻辑:
新增SKILL资源时,限制领域必须为SKILL类型
2026-05-13 11:20:36 +08:00
otto fbbe78553d feat(AI资源广场): Skill资源相关功能开发
优化资源及资源领域分类逻辑:
新增SKILL资源时,限制领域必须为SKILL类型
2026-05-13 11:14:31 +08:00
otto fcefe43872 feat(AI资源广场): Skill资源相关功能开发
优化资源及资源领域分类逻辑:
1. 保持原资源逻辑,当用户查询资源/资源领域列表时若未传入Category参数则代表查询原资源/资源领域
2. 支持获取所有资源/资源领域,当用户传入Category参数值为ALL时,则查询所有资源/资源领域
2026-05-13 11:10:15 +08:00
欧涛 3126082f61 feat(AI资源广场): Skill资源相关功能开发
评论附件保持和其他模块评论附件字段一致
2026-05-11 17:02:18 +08:00
欧涛 d20a84f214 feat(AI资源广场): Skill资源相关功能开发
完善专区评论功能
2026-05-11 16:25:26 +08:00
欧涛 2acfd3c80d feat(AI资源广场): Skill资源相关功能开发
完善专区评论功能
2026-05-11 15:24:35 +08:00
欧涛 a07c47b6b7 feat(AI资源广场): Skill资源相关功能开发
完善专区评论功能
2026-05-11 15:09:58 +08:00
欧涛 41ec65b242 feat(AI资源广场): Skill资源相关功能开发
解析Skill压缩包文件时,自动解析压缩包内的name,自动填充,减少用户内容输入
2026-05-11 11:36:32 +08:00
欧涛 0643fda6b6 feat(AI资源广场): Skill资源相关功能开发
当用户在指定专区检查专区权限时,将用户自动加入该专区会员(指定专区id通过Nacos配置)
2026-05-11 10:40:38 +08:00
欧涛 bc7c212bb6 feat(AI资源广场): Skill资源相关功能开发
新增接口:支持传入资源id列表,查询资源是否已点赞
2026-05-11 09:11:00 +08:00
欧涛 904bbf1cab feat(AI资源广场): Skill资源相关功能开发
资源领域新增资源分类属性,支持通过资源分类区分SKILL/MODEL/DATASET/NULL对应的资源领域
2026-05-09 16:33:49 +08:00
欧涛 7628e38d3a feat(AI资源广场): Skill资源相关功能开发
新增上架资源接口,同时已下架资源修改后变成待审核状态
2026-05-09 15:35:38 +08:00
欧涛 fb9da470e1 feat(AI资源广场): Skill资源相关功能开发
资源审核状态参数支持传入all查询所有审核状态的资源
2026-05-09 15:10:43 +08:00
欧涛 22f095b01d feat(AI资源广场): Skill资源相关功能开发
资源领域新增资源分类属性,支持通过资源分类区分SKILL/MODEL/DATASET/NULL对应的资源领域
2026-05-09 14:56:27 +08:00
欧涛 c39c826bdb feat(AI资源广场): Skill资源相关功能开发
获取Skill资源列表时接口不返回Skill详情(Skill.md和Skill文件树),只有在查询Skill资源详情时才返回
2026-05-09 14:38:49 +08:00
欧涛 b7e2541c1d feat(AI资源广场): Skill资源相关功能开发
资源新增草稿状态,撤回审核后资源将变为草稿状态,并且在重新编辑后变为待审核状态
2026-05-09 13:51:01 +08:00
欧涛 91a96be75f feat(AI资源广场): Skill资源相关功能开发
获取Skill资源列表时接口不返回Skill详情(Skill.md和Skill文件树),只有在查询Skill资源详情时才返回
2026-05-09 11:41:28 +08:00
欧涛 9f42c51a5f feat(AI资源广场): Skill资源相关功能开发
获取Skill资源列表时接口不返回Skill详情(Skill.md和Skill文件树),只有在查询Skill资源详情时才返回
2026-05-09 11:24:05 +08:00
欧涛 5a55ea027b feat(AI资源广场): Skill资源相关功能开发
管理员获取项目聚合列表和资源列表时支持查询“我的”相关数据
2026-05-09 11:07:18 +08:00
欧涛 cecc14868a feat(AI资源广场): Skill资源相关功能开发
合并超算相关代码(支持项目聚合审核)
2026-05-09 09:13:02 +08:00
欧涛 b3f5a4898f Merge branch 'chaosuan_zone' into aiResourceMRCS
# Conflicts:
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/FrontController.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/common/controller/OpenController.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/domain/vo/ZoneResourceDataVo.java
#	microservices-modules/microservices-modules-zone/src/main/java/com/microservices/zone/resource/mapper/ZoneResourceMapper.java
#	microservices-modules/microservices-modules-zone/src/main/resources/mapper/zone/ZoneResourceMapper.xml
2026-05-09 08:58:56 +08:00
欧涛 965cedc275 feat(AI资源广场): Skill资源相关功能开发
修复Skill无法下载的问题
2026-05-08 13:59:16 +08:00
欧涛 53ff26058f feat(AI资源广场): Skill资源相关功能开发
删除老版本评论相关内容
2026-05-07 10:34:41 +08:00
欧涛 46b1b9ed81 feat(AI资源广场): Skill资源相关功能开发
1. 新增Skill资源复用原资源新增逻辑
2. 更新Skill资源复用原资源更新逻辑
3. 获取资源列表接口增加资源分类查询参数
2026-05-07 10:23:33 +08:00
欧涛 20bd41ebf7 feat(AI资源广场): Skill资源相关功能开发
资源下载次数基于文件下载次数进行统计
2026-05-07 08:40:08 +08:00
欧涛 a416b7c856 feat(AI资源广场): Skill资源相关功能开发
Skill资源特有字段使用extendData进行存储
2026-05-06 16:32:06 +08:00
欧涛 d662395f1e feat(AI资源广场): Skill资源相关功能开发
Skill资源特有字段使用extendData进行存储
2026-05-06 16:31:49 +08:00
欧涛 58bdc10a4b feat(AI资源广场): Skill资源相关功能开发
从Skill.md中提取简介
2026-05-06 15:28:26 +08:00
欧涛 343d31024e feat(AI资源广场): Skill资源相关功能开发
Skill文件通过fileId关联
2026-05-06 14:47:21 +08:00
欧涛 020472c4c1 feat(AI资源广场): Skill资源相关功能开发
解析Skill压缩包时,同步将文件上传至File微服务,并返回文件标识
2026-05-06 11:30:24 +08:00
欧涛 356ffd8915 feat(AI资源广场): Skill资源相关功能开发
资源评论基于Gitlink评论通用化改造
2026-04-30 16:39:07 +08:00
欧涛 3a2560f700 feat(AI资源广场): Skill资源相关功能开发
1. 完成Skill资源创建、上传、修改、删除及查询功能开发
2. 完成Skill Zip包解析功能开发
3. 完成资源点赞功能开发
4. 完成资源评论功能开发
2026-04-30 14:16:21 +08:00
欧涛 deb149ee7e fix(超算专区): 修复项目聚合被异常删除的问题 2026-04-20 15:59:07 +08:00
86 changed files with 4860 additions and 234 deletions

View File

@ -1,36 +1,58 @@
## 系统模块
~~~
com.microservices
├── microservices-ui // 前端框架
├── microservices-gateway // 网关模块
├── microservices-auth // 认证中心
├── microservices-api // 接口模块
│ └── microservices-api-system // 系统接口
├── microservices-common // 通用模块
│ └── microservices-common-async // 异步线程池配置
│ └── microservices-common-core // 核心模块
│ └── microservices-common-datascope // 权限范围
│ └── microservices-common-datasource // 多数据源
│ └── microservices-common-httpClient // http调用
│ └── microservices-common-log // 日志记录
│ └── microservices-common-redis // 缓存服务
│ └── microservices-common-seata // seata分布式事务
│ └── microservices-common-security // 安全模块
│ └── microservices-common-swagger // 系统接口
├── microservices-modules // 业务模块
│ └── microservices-cms // 内容管理模块
│ └── microservices-dms // 数据管理体系模块
│ └── microservices-dss // 数据统计系统模块
│ └── microservices-file // 文件服务
│ └── microservices-gen // 代码生成
│ └── microservices-job // 定时任务
│ └── microservices-pms // 项目管理系统模块
│ └── microservices-system // 系统模块
│ └── microservices-wiki // Wiki模块
│ └── microservices-zone // 特色专区系统模块
├── microservices-visual // 图形化管理模块
│ └── microservices-visual-monitor // 监控中心
├──pom.xml // 公共依赖
~~~
基于 Spring Cloud Alibaba 的微服务平台,为 GitLink / 小强社区提供后端服务支持。
## 技术栈
| 技术 | 版本 |
|------|------|
| Java | 1.8 |
| Spring Boot | 2.7.7 |
| Spring Cloud | 2021.0.5 |
| Spring Cloud Alibaba | 2021.0.4.0 |
| 平台版本 | 3.6.2 |
## 系统架构
```
microservices/
├── microservices-gateway # Spring Cloud Gateway 网关 [8080]
├── microservices-auth # 认证中心JWT + Redis[9110]
├── microservices-api/ # Feign 远程调用接口定义
│ └── microservices-api-system # 各服务的 RemoteXxxService 接口
├── microservices-common/ # 公共库
│ ├── microservices-common-core # 通用工具、基类、R<T>、常量
│ ├── microservices-common-security # 权限注解(@RequiresPermissions等
│ ├── microservices-common-datascope # 数据权限过滤
│ ├── microservices-common-datasource # 多数据源切换(@Master/@Slave
│ ├── microservices-common-redis # Redis 缓存服务
│ ├── microservices-common-log # 操作日志记录
│ ├── microservices-common-async # 异步线程池
│ ├── microservices-common-httpClient # HTTP 客户端
│ └── microservices-common-swagger # Knife4j/Swagger API 文档
└── microservices-modules/ # 业务服务模块
├── microservices-modules-system # 系统管理 [9114]
├── microservices-modules-zone # 特色专区 [9117](主要开发模块)
├── microservices-modules-cms # 内容管理 [9116]
├── microservices-modules-pms # 项目管理 [9118]
├── microservices-modules-file # 文件服务 [9111]MinIO/FastDFS
├── microservices-modules-gen # 代码生成 [9112]
├── microservices-modules-job # 定时任务 [9113]
├── microservices-modules-dss # 数据统计 [9198]
├── microservices-modules-dms # 数据管理 [9119]
└── microservices-modules-wiki # Wiki [9199]
```
## 服务端口一览
| 服务 | 端口 | 说明 |
|------|------|------|
| microservices-gateway | 8080 | API 网关 |
| microservices-auth | 9110 | 认证中心 |
| microservices-modules-file | 9111 | 文件服务 |
| microservices-modules-gen | 9112 | 代码生成 |
| microservices-modules-job | 9113 | 定时任务 |
| microservices-modules-system | 9114 | 系统管理 |
| microservices-modules-cms | 9116 | 内容管理 |
| microservices-modules-zone | 9117 | 特色专区 |
| microservices-modules-dms | 9119 | 数据管理 |
| microservices-modules-dms | 9198 | 数据统计 |
| microservices-modules-wiki | 9199 | Wiki |

View File

@ -4,6 +4,7 @@ import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.constant.ServiceNameConstants;
import com.microservices.common.core.domain.R;
import com.microservices.system.api.domain.SysFile;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.factory.RemoteFileFallbackFactory;
import feign.Response;
@ -52,6 +53,10 @@ public interface RemoteFileService {
@GetMapping("/open/getFile/{fileId}")
public R<SysFileInfo> getFile(@PathVariable("fileId") Long fileId);
@GetMapping("/common/getFileDetail/{fileId}")
public R<SysFileInfo> getFileDetail(@PathVariable("fileId") Long fileId,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 根据文件Id获取文件信息
*
@ -127,4 +132,15 @@ public interface RemoteFileService {
@PostMapping(value = "/common/uploadFileToForge/{fileIdentifiers}")
R<List<String>> uploadFileToForge(@PathVariable("fileIdentifiers") String fileIdentifiers,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
/**
* 根据文件ID列表获取下载记录内部接口
*
* @param fileIds 文件ID列表逗号分隔
* @param source 内部调用鉴权
* @return 下载记录列表
*/
@GetMapping("/common/getDownloadUserList/{fileIds}")
R<List<SysFileDownloadLogVo>> getDownloadUserList(@PathVariable("fileIds") String fileIds,
@RequestHeader(SecurityConstants.FROM_SOURCE) String source);
}

View File

@ -10,6 +10,10 @@ import lombok.Data;
@Data
public class SysFile
{
/**
* 文件标识
*/
private Long fileId;
/**
* 文件标识
*/

View File

@ -0,0 +1,46 @@
package com.microservices.system.api.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 文件下载记录VO供跨服务调用
*
* @author microservices
* @date 2026-06-29
*/
@Data
@ApiModel("文件下载记录")
public class SysFileDownloadLogVo {
@ApiModelProperty(value = "文件ID")
private Long fileId;
@ApiModelProperty(value = "文件原始名称")
private String fileOriginName;
@ApiModelProperty(value = "下载用户ID")
private Long userId;
@ApiModelProperty(value = "下载用户")
private String userName;
@ApiModelProperty(value = "用户昵称")
private String nickName;
@ApiModelProperty(value = "手机号码")
private String phonenumber;
@ApiModelProperty(value = "邮箱")
private String email;
@ApiModelProperty(value = "IP地址")
private String ipAddr;
@ApiModelProperty(value = "下载时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date downloadTime;
}

View File

@ -4,6 +4,7 @@ import com.microservices.common.core.domain.R;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.domain.SysFile;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import feign.Response;
import org.slf4j.Logger;
@ -46,6 +47,11 @@ public class RemoteFileFallbackFactory implements FallbackFactory<RemoteFileServ
return R.fail("根据文件Id获取文件信息失败:" + throwable.getMessage());
}
@Override
public R<SysFileInfo> getFileDetail(Long fileId, String source) {
return R.fail("根据文件id获取文件详细信息失败:" + throwable.getMessage());
}
@Override
public R<SysFileInfo> getFileByFileIdentifier(String fileIdentifier, String source) {
return R.fail("根据文件标识获取文件信息失败:" + throwable.getMessage());
@ -80,6 +86,11 @@ public class RemoteFileFallbackFactory implements FallbackFactory<RemoteFileServ
public R<List<String>> uploadFileToForge(String fileIdentifiers, String source) {
return R.fail("上传本地文件到Forge失败:" + throwable.getMessage());
}
@Override
public R<List<SysFileDownloadLogVo>> getDownloadUserList(String fileIds, String source) {
return R.fail("获取下载记录列表失败:" + throwable.getMessage());
}
};
}
}

View File

@ -258,6 +258,15 @@ public class CacheConstants {
return PACKAGE_FILE_IS_FINISH + fileIdentifier;
}
/**
* 项目点赞统计缓存 Key前缀
*/
private final static String PROJECT_STATISTICS_LIKE_COUNT = "project_statistics_like_count:";
public static String getProjectStatisticsLikeCount(Long zoneId, String username) {
return PROJECT_STATISTICS_LIKE_COUNT + zoneId + "-" + username;
}
// 全局资源数据缓存最近一年统计数据
public final static String GLOBAL_RESOURCES_DATA_KEY = "GLOBAL_RESOURCES_DATA:";

View File

@ -37,14 +37,17 @@ import java.util.Map;
*/
@Aspect
@Component
public class LogAspect
{
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
/** 排除敏感属性字段 */
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
/**
* 排除敏感属性字段
*/
public static final String[] EXCLUDE_PROPERTIES = {"password", "oldPassword", "newPassword", "confirmPassword"};
/** 计算操作消耗时间 */
/**
* 计算操作消耗时间
*/
private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
@Autowired
@ -54,8 +57,7 @@ public class LogAspect
* 处理请求前执行
*/
@Before(value = "@annotation(controllerLog)")
public void boBefore(JoinPoint joinPoint, Log controllerLog)
{
public void boBefore(JoinPoint joinPoint, Log controllerLog) {
TIME_THREADLOCAL.set(System.currentTimeMillis());
}
@ -65,42 +67,41 @@ public class LogAspect
* @param joinPoint 切点
*/
@AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
{
public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
handleLog(joinPoint, controllerLog, null, jsonResult);
}
/**
* 拦截异常操作
*
*
* @param joinPoint 切点
* @param e 异常
* @param e 异常
*/
@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
{
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
handleLog(joinPoint, controllerLog, e, null);
}
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
{
try
{
protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
try {
// *========数据库日志=========*//
SysOperLog operLog = new SysOperLog();
operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
// 请求的地址
String ip = IpUtils.getIpAddr();
operLog.setOperIp(ip);
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
if (ServletUtils.getRequest() != null) {
operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
}
String username = SecurityUtils.getUsername();
if (StringUtils.isNotBlank(username))
{
if (StringUtils.isNotBlank(username)) {
operLog.setOperName(username);
}
if (e != null)
{
if (e != null) {
operLog.setStatus(BusinessStatus.FAIL.ordinal());
operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
}
@ -108,36 +109,29 @@ public class LogAspect
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
operLog.setMethod(className + "." + methodName + "()");
// 设置请求方式
operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
// 处理设置注解上的参数
getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
// 设置消耗时间
operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
// 保存数据库
asyncLogService.saveSysLog(operLog);
}
catch (Exception exp)
{
} catch (Exception exp) {
// 记录本地异常日志
log.error("异常信息:{}", exp.getMessage());
exp.printStackTrace();
}
finally
{
} finally {
TIME_THREADLOCAL.remove();
}
}
/**
* 获取注解中对方法的描述信息 用于Controller层注解
*
* @param log 日志
*
* @param log 日志
* @param operLog 操作日志
* @throws Exception
*/
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
{
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception {
// 设置action动作
operLog.setBusinessType(log.businessType().ordinal());
// 设置标题
@ -145,36 +139,30 @@ public class LogAspect
// 设置操作人类别
operLog.setOperatorType(log.operatorType().ordinal());
// 是否需要保存request参数和值
if (log.isSaveRequestData())
{
if (log.isSaveRequestData()) {
// 获取参数的信息传入到数据库中
setRequestValue(joinPoint, operLog, log.excludeParamNames());
}
// 是否需要保存response参数和值
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
{
if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
}
}
/**
* 获取请求的参数放到log中
*
*
* @param operLog 操作日志
* @throws Exception 异常
*/
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception
{
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception {
String requestMethod = operLog.getRequestMethod();
Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
if (StringUtils.isEmpty(paramsMap)
&& (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)))
{
&& (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))) {
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
operLog.setOperParam(StringUtils.substring(params, 0, 2000));
}
else
{
} else {
operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
}
}
@ -182,64 +170,53 @@ public class LogAspect
/**
* 参数拼装
*/
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames)
{
String params = "";
if (paramsArray != null && paramsArray.length > 0)
{
for (Object o : paramsArray)
{
if (StringUtils.isNotNull(o) && !isFilterObject(o))
{
try
{
private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
StringBuilder params = new StringBuilder();
if (paramsArray != null) {
for (Object o : paramsArray) {
if (StringUtils.isNotNull(o) && !isFilterObject(o)) {
try {
String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
params += jsonObj.toString() + " ";
}
catch (Exception e)
{
// 清洗控制字符保留常见空白字符\t, \n, \r
// ASCII 0-8, 11-12, 14-31, 127 是控制字符需要去除
String cleaned = jsonObj.replaceAll("[\\x00-\\x08\\x0B-\\x0C\\x0E-\\x1F\\x7F]", "");
params.append(cleaned).append(" ");
} catch (Exception e) {
// 记录日志而非静默吞掉便于排查问题
log.error("序列化操作日志参数失败: {}", e.getMessage());
}
}
}
}
return params.trim();
return params.toString().trim();
}
/**
* 忽略敏感属性
*/
public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames)
{
public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames) {
return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
}
/**
* 判断是否需要过滤的对象
*
*
* @param o 对象信息
* @return 如果是需要过滤的对象则返回true否则返回false
*/
@SuppressWarnings("rawtypes")
public boolean isFilterObject(final Object o)
{
public boolean isFilterObject(final Object o) {
Class<?> clazz = o.getClass();
if (clazz.isArray())
{
if (clazz.isArray()) {
return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
}
else if (Collection.class.isAssignableFrom(clazz))
{
} else if (Collection.class.isAssignableFrom(clazz)) {
Collection collection = (Collection) o;
for (Object value : collection)
{
for (Object value : collection) {
return value instanceof MultipartFile;
}
}
else if (Map.class.isAssignableFrom(clazz))
{
} else if (Map.class.isAssignableFrom(clazz)) {
Map map = (Map) o;
for (Object value : map.entrySet())
{
for (Object value : map.entrySet()) {
Map.Entry entry = (Map.Entry) value;
return entry.getValue() instanceof MultipartFile;
}

View File

@ -3,6 +3,7 @@ package com.microservices.common.security.feign;
import feign.RequestInterceptor;
import feign.codec.Encoder;
import feign.form.FormEncoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
@ -33,6 +34,6 @@ public class FeignAutoConfiguration
// 重要的是返回类型要是 Encoder 并且实现类必须是 FormEncoder 或者其子类
@Bean
public Encoder feignFormEncoder() {
return new FormEncoder(new SpringEncoder(messageConverters));
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}

View File

@ -8,6 +8,7 @@ import lombok.Data;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
/**
* @author otto
@ -51,6 +52,9 @@ public class CmsDocInputDto {
@ApiModelProperty("关键词")
private String keywords;
@ApiModelProperty(value = "发布时间,隐藏参数,内部调用使用", hidden = true)
private Date publishDate;
public String getContent() {
String base64decodedString;
try {

View File

@ -50,11 +50,7 @@ import javax.servlet.http.HttpServletRequest;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@ -400,6 +396,7 @@ public class CmsDocServiceImpl implements ICmsDocService {
// 自动审核通过时也需要增加积分
cmsAsyncService.addPublishDocPointsAsync(cmsDoc.getDeptId(), cmsDoc.getCreateBy(), cmsDoc.getId(), cmsDoc.getName());
}
innerInitPublishDate(cmsDoc,cmsDocInputDto.getPublishDate());
return success ? cmsDoc.getId().intValue() : null;
} else {
//创建临时分支创建文件创建PR
@ -409,11 +406,21 @@ public class CmsDocServiceImpl implements ICmsDocService {
boolean success = insertCmsDocWithAudit(cmsDoc,
"创建文章:" + StringUtils.truncateStringAndConcatenatingSpecifiedString(cmsDoc.getName(), 100, "..."),
cmsDocInputDto.getContent());
innerInitPublishDate(cmsDoc,cmsDocInputDto.getPublishDate());
return success ? cmsDoc.getId().intValue() : null;
}
}
private void innerInitPublishDate(CmsDoc cmsDoc, Date publishDate) {
if(publishDate!=null){
cmsDoc.setCreateTime(publishDate);
cmsDoc.setUpdateTime(publishDate);
cmsDoc.setPublishTime(publishDate);
cmsDocMapper.updateCmsDoc(cmsDoc);
}
}
private boolean isCurrentUserZoneAdmin(Long deptId) {
boolean isZoneAdmin = false;
List<SysUserDeptRole> sysUserDeptRoleList = FeignUtils.getReturnData(
@ -1402,7 +1409,7 @@ public class CmsDocServiceImpl implements ICmsDocService {
private void checkDocName(String docName, Long deptId, String dirName) {
//检查该文件是否已存在
CmsDoc oldCmsDoc = cmsDocMapper.selectCmsDocByNameAndDeptId(docName, deptId, dirName);
if (oldCmsDoc != null) {
if (oldCmsDoc != null && DocAuditStatus.AUDIT_PASS.getAuditCode().equals(oldCmsDoc.getAuditStatus())) {
if (oldCmsDoc.getIsDir()) {
throw new ServiceException("该栏目已存在(栏目名称[%s])", docName);
} else {
@ -1415,7 +1422,7 @@ public class CmsDocServiceImpl implements ICmsDocService {
private boolean insertCmsDocWithAudit(CmsDoc cmsDoc, String message, String content) {
//检查该文件是否已存在
CmsDoc oldCmsDoc = cmsDocMapper.selectCmsDocByNameAndDeptId(cmsDoc.getName(), cmsDoc.getDeptId(), cmsDoc.getDirName());
if (oldCmsDoc != null) {
if (oldCmsDoc != null && DocAuditStatus.AUDIT_PASS.getAuditCode().equals(oldCmsDoc.getAuditStatus())) {
throw new ServiceException("该文章已存在(文章名称[" + cmsDoc.getName() + "])");
}
boolean insertResult = insertCmsDocDatabase(cmsDoc, content);

View File

@ -239,6 +239,7 @@
and is_dir = 1
</otherwise>
</choose>
and audit_status = '1'
</where>
</select>
<select id="selectCmsDocByNameAndDeptIdIgnoreDirName" resultMap="CmsDocResult">

View File

@ -138,6 +138,7 @@ public class ChunkedUploadController extends BaseController {
return genericsSuccess(result);
}
/* 屏蔽取消上传接口
/**
* 取消上传
* <p>
@ -147,7 +148,6 @@ public class ChunkedUploadController extends BaseController {
*
* @param uploadId 上传ID
* @return 取消结果
*/
@DeleteMapping("/cancel/{uploadId}")
@ApiOperation("取消上传")
@ApiImplicitParams({
@ -157,6 +157,7 @@ public class ChunkedUploadController extends BaseController {
boolean result = chunkedUploadService.cancelUpload(uploadId);
return genericsSuccess(result ? CancelUploadResponseVo.success() : CancelUploadResponseVo.failed("取消上传失败"));
}
*/
/**
* 获取上传进度

View File

@ -9,6 +9,7 @@ import com.microservices.common.core.web.controller.BaseController;
import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.security.annotation.InnerAuth;
import com.microservices.file.service.ISysFileInfoService;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
@ -235,4 +236,29 @@ public class CommonController extends BaseController {
R<List<String>> uploadFileToForge(@PathVariable("fileIdentifiers") String fileIdentifiers) {
return R.ok(sysFileInfoService.uploadFileToForge(fileIdentifiers));
}
/**
* 根据文件Id获取文件信息
*/
@InnerAuth
@GetMapping("/getFileDetail/{fileId}")
@ApiOperation(value = "根据文件Id获取文件信息")
public R<SysFileInfo> getFileDetail(@PathVariable("fileId") Long fileId) {
SysFileInfo sysFileInfo = sysFileInfoService.selectSysFileInfoByFileId(fileId);
sysFileInfo.setFilePath(localFilePath + sysFileInfo.getFilePath());
return R.ok(sysFileInfo);
}
/**
* 根据文件ID列表获取下载记录内部接口
*
* @param fileIds 文件ID列表逗号分隔
* @return 下载记录列表
*/
@InnerAuth
@GetMapping("/getDownloadUserList/{fileIds}")
@ApiOperation(value = "根据文件ID列表获取下载记录", hidden = true)
public R<List<SysFileDownloadLogVo>> getDownloadUserList(@PathVariable("fileIds") String fileIds) {
return R.ok(sysFileInfoService.getDownloadUserListByFileIds(fileIds));
}
}

View File

@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import springfox.documentation.annotations.ApiIgnore;
@ -33,7 +34,7 @@ public class SysFileController extends BaseController {
*/
@InnerAuth
@PostMapping("upload")
public R<SysFile> upload(MultipartFile file,
public R<SysFile> upload(@RequestPart(value = "file") MultipartFile file,
@RequestParam("type") String type,
@RequestParam("hierarchy") String hierarchy) {
try {
@ -43,6 +44,7 @@ public class SysFileController extends BaseController {
sysFile.setName(fileInfo.getFilePath());
sysFile.setUrl(fileInfo.getDownloadUrl());
sysFile.setFileIdentifier(fileInfo.getFileIdentifier());
sysFile.setFileId(fileInfo.getFileId());
return R.ok(sysFile);
} catch (Exception e) {
log.error("上传文件失败", e);

View File

@ -0,0 +1,34 @@
package com.microservices.file.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 文件下载记录对象 sys_file_download_log
*
* @author microservices
* @date 2026-06-29
*/
@Data
public class SysFileDownloadLog {
/** 主键 */
private Long id;
/** 文件ID */
private Long fileId;
/** 下载用户ID */
private Long userId;
/** 下载用户 */
private String userName;
/** 下载时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date downloadTime;
/** IP地址 */
private String ipAddr;
}

View File

@ -0,0 +1,33 @@
package com.microservices.file.mapper;
import com.microservices.file.domain.SysFileDownloadLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 文件下载记录Mapper接口
*
* @author microservices
* @date 2026-06-29
*/
@Mapper
public interface SysFileDownloadLogMapper {
/**
* 新增文件下载记录
*
* @param sysFileDownloadLog 文件下载记录
* @return 结果
*/
int insertSysFileDownloadLog(SysFileDownloadLog sysFileDownloadLog);
/**
* 根据文件ID列表查询下载用户列表
*
* @param fileIds 文件ID列表
* @return 文件下载记录列表
*/
List<SysFileDownloadLog> selectDownloadLogByFileIds(@Param("fileIds") Long[] fileIds);
}

View File

@ -3,6 +3,7 @@ package com.microservices.file.mapper;
import com.microservices.system.api.domain.SysFileInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@ -85,4 +86,9 @@ public interface SysFileInfoMapper {
* @return 文件
*/
SysFileInfo selectSysFileInfoByIdentifier(String fileIdentifier);
/**
* 统计引用同一物理文件路径的记录数
*/
int countByFilePath(@Param("filePath") String filePath);
}

View File

@ -1,6 +1,7 @@
package com.microservices.file.service;
import com.microservices.file.domain.FileUploadSession;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import org.springframework.web.multipart.MultipartFile;
@ -8,6 +9,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 文件管理Service接口
@ -177,4 +179,18 @@ public interface ISysFileInfoService {
List<String> uploadFileToForge(String fileIdentifiers);
String downloadFileByUrl(String fileUrl, String type, String hierarchy);
/**
* 基于已有文件信息克隆一条新的数据库记录秒传场景使用
* 新记录拥有独立的fileId和downloadCount(=0)共享物理文件
*/
SysFileInfo cloneFileInfo(SysFileInfo source);
/**
* 根据文件ID列表获取下载记录列表
*
* @param fileIds 文件ID列表逗号分隔
* @return 下载记录列表
*/
List<SysFileDownloadLogVo> getDownloadUserListByFileIds(String fileIds);
}

View File

@ -122,6 +122,21 @@ public class ChunkedUploadServiceImpl implements IChunkedUploadService {
throw new ServiceException("文件大小无效");
}
// 1.1 文件名安全校验
String fileName = input.getFileName();
// 禁止路径穿越
if (fileName.contains("..")) {
throw new ServiceException("文件名包含非法字符");
}
// 禁止路径分隔符
if (fileName.contains("/") || fileName.contains("\\")) {
throw new ServiceException("文件名包含非法字符");
}
// 文件名长度限制
if (fileName.length() > 255) {
throw new ServiceException("文件名过长最大255字符");
}
// 校验文件大小
if (input.getFileSize() > maxFileSize) {
throw new ServiceException("文件大小超过限制: %s(%d字节) ", FileUtils.calcFileSize(maxFileSize), maxFileSize);
@ -215,13 +230,15 @@ public class ChunkedUploadServiceImpl implements IChunkedUploadService {
// 清理临时文件
cleanupTempFiles(session);
// 返回已存在文件信息
// 基于原文件创建新的数据库记录独立downloadCount共享物理文件
SysFileInfo newFileInfo = fileInfoService.cloneFileInfo(fileInfo);
return CheckInstantUploadVo.instantSuccess(
fileInfo.getFileId(),
fileInfo.getFileOriginName(),
fileInfo.getFilePath(),
fileInfo.getDownloadUrl(),
fileInfo.getFileSizeInfo()
newFileInfo.getFileId(),
newFileInfo.getFileOriginName(),
newFileInfo.getFilePath(),
newFileInfo.getDownloadUrl(),
newFileInfo.getFileSizeInfo()
);
}
}
@ -316,16 +333,18 @@ public class ChunkedUploadServiceImpl implements IChunkedUploadService {
throw new ServiceException("分片记录不存在");
}
// 6. 检查是否已上传
if (FileUploadChunk.Status.UPLOADED.getCode().equals(chunkRecord.getStatus())) {
File existingFile = new File(chunkRecord.getChunkPath());
if (existingFile.exists()) {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "分片已存在");
result.put("chunkNumber", chunkNumber);
return result;
}
// 6. 检查是否已上传避免重复计数
boolean isAlreadyUploaded = FileUploadChunk.Status.UPLOADED.getCode().equals(chunkRecord.getStatus())
&& new File(chunkRecord.getChunkPath()).exists();
if (isAlreadyUploaded) {
// 已上传的分片直接返回不重复写入和计数
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "分片已存在");
result.put("chunkNumber", chunkNumber);
result.put("skipped", true);
return result;
}
// 7. 保存分片文件
@ -345,13 +364,14 @@ public class ChunkedUploadServiceImpl implements IChunkedUploadService {
chunkRecord.setUploadTime(DateUtils.getNowDate());
chunkMapper.updateChunk(chunkRecord);
// 9. 更新会话已上传分片数
// 9. 更新会话已上传分片数只有真正写入新分片才+1
sessionMapper.incrementUploadedChunks(uploadId);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "分片上传成功");
result.put("chunkNumber", chunkNumber);
result.put("skipped", false);
return result;
}
@ -408,32 +428,57 @@ public class ChunkedUploadServiceImpl implements IChunkedUploadService {
String fileName = FileUploadUtils.extractFilename(session.getFileName());
String mergedFilePath = FileUploadUtils.getAbsoluteFile(baseFilePath, fileName).getAbsolutePath();
try (FileOutputStream fos = new FileOutputStream(mergedFilePath)) {
List<FileUploadChunk> chunks = chunkMapper.selectByUploadId(session.getUploadId());
for (FileUploadChunk chunk : chunks) {
File chunkFile = new File(chunk.getChunkPath());
if (!chunkFile.exists()) {
throw new ServiceException("分片文件不存在: " + chunk.getChunkPath());
}
List<FileUploadChunk> chunks = null;
try {
// 先获取分片列表已按chunk_number排序
chunks = chunkMapper.selectByUploadId(session.getUploadId());
try (FileInputStream fis = new FileInputStream(chunkFile)) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
// 使用缓冲流提升合并效率
try (FileOutputStream fos = new FileOutputStream(mergedFilePath);
BufferedOutputStream bos = new BufferedOutputStream(fos, 1024 * 1024)) { // 1MB buffer
for (FileUploadChunk chunk : chunks) {
File chunkFile = new File(chunk.getChunkPath());
if (!chunkFile.exists()) {
throw new ServiceException("分片文件不存在: " + chunk.getChunkPath());
}
// 使用 Files.copy 提升性能
Files.copy(chunkFile.toPath(), bos);
}
bos.flush();
}
} catch (IOException e) {
logger.error("合并分片文件失败: uploadId={}", session.getUploadId(), e);
// 合并失败时清理已写入的无效文件
cleanupFailedMerge(mergedFilePath);
throw new ServiceException("合并分片文件失败");
}
// 哈希校验FULL类型进行后置校验
validateFullFileHash(mergedFilePath, session);
return FileUploadUtils.getPathFileName(fileName);
}
/**
* 清理合并失败的文件
*
* @param mergedFilePath 合并后的文件路径
*/
private void cleanupFailedMerge(String mergedFilePath) {
if (mergedFilePath != null && !mergedFilePath.isEmpty()) {
File file = new File(mergedFilePath);
if (file.exists() && file.length() > 0) {
// 仅在文件存在且有内容时删除避免误删空文件或不存在文件
boolean deleted = file.delete();
if (!deleted) {
logger.warn("清理合并失败文件失败: {}", mergedFilePath);
}
}
}
}
@Override
@Transactional
public boolean cancelUpload(String uploadId) {

View File

@ -110,10 +110,16 @@ public class FileCommonAsyncServiceImpl implements IFileCommonAsyncService {
}
/**
* 异步清理临时文件
* 异步清理临时文件复用线程池避免频繁创建线程
*/
private void cleanupTempFilesAsync(FileUploadSession session) {
new Thread(() -> chunkedUploadService.cleanupTempFiles(session)).start();
CustomExecutorFactory.threadPoolExecutor.execute(() -> {
try {
chunkedUploadService.cleanupTempFiles(session);
} catch (Exception e) {
logger.error("异步清理临时文件失败: uploadId={}", session.getUploadId(), e);
}
});
}
/**

View File

@ -13,18 +13,27 @@ import com.microservices.common.core.utils.file.FileTypeUtils;
import com.microservices.common.core.utils.file.FileUtils;
import com.microservices.common.core.utils.html.EscapeUtil;
import com.microservices.common.core.utils.uuid.IdUtils;
import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.utils.ip.IpUtils;
import com.microservices.common.httpClient.domain.GitLinkRequestUrl;
import com.microservices.common.httpClient.util.GitLinkRequestHelper;
import com.microservices.common.redis.service.RedisService;
import com.microservices.common.security.auth.AuthUtil;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.file.domain.FileUploadSession;
import com.microservices.file.domain.SysFileDownloadLog;
import com.microservices.file.mapper.SysFileDownloadLogMapper;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.file.mapper.SysFileInfoMapper;
import com.microservices.file.service.IChunkedUploadService;
import com.microservices.file.service.IFileCommonAsyncService;
import com.microservices.file.service.ISysFileInfoAsyncService;
import com.microservices.file.service.ISysFileInfoService;
import com.microservices.file.utils.FileUploadUtils;
import com.microservices.system.api.RemoteUserService;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.domain.SysUser;
import com.microservices.system.api.utils.FeignUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -44,6 +53,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 文件管理Service业务层处理
@ -57,6 +67,10 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
@Autowired
private SysFileInfoMapper sysFileInfoMapper;
@Autowired
private SysFileDownloadLogMapper sysFileDownloadLogMapper;
@Autowired
private RemoteUserService remoteUserService;
@Autowired
private GitLinkRequestHelper gitLinkRequestHelper;
@Autowired
private ISysFileInfoAsyncService fileInfoAsyncService;
@ -178,8 +192,9 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
for (int i = 0; i < fileIds.length; i++) {
Long fileId = fileIds[i];
SysFileInfo sysFileInfo = sysFileInfoMapper.selectSysFileInfoByFileId(fileId);
String filePath = localFilePath + StringUtils.substringAfter(sysFileInfo.getFilePath(), Constants.RESOURCE_PREFIX);
FileUtils.deleteFile(filePath);
if (sysFileInfo != null) {
safeDeletePhysicalFile(sysFileInfo.getFilePath());
}
}
return sysFileInfoMapper.deleteSysFileInfoByFileIds(fileIds);
}
@ -193,16 +208,18 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
@Override
public int deleteSysFileInfoByFileId(Long fileId) {
SysFileInfo sysFileInfo = sysFileInfoMapper.selectSysFileInfoByFileId(fileId);
String filePath = localFilePath + StringUtils.substringAfter(sysFileInfo.getFilePath(), Constants.RESOURCE_PREFIX);
FileUtils.deleteFile(filePath);
if (sysFileInfo != null) {
safeDeletePhysicalFile(sysFileInfo.getFilePath());
}
return sysFileInfoMapper.deleteSysFileInfoByFileId(fileId);
}
@Override
public int deleteSysFileInfoByFileObjectName(String fileObjectName) {
SysFileInfo sysFileInfo = sysFileInfoMapper.selectSysFileInfoByFileObjectName(fileObjectName);
String filePath = localFilePath + StringUtils.substringAfter(sysFileInfo.getFilePath(), Constants.RESOURCE_PREFIX);
FileUtils.deleteFile(filePath);
if (sysFileInfo != null) {
safeDeletePhysicalFile(sysFileInfo.getFilePath());
}
return sysFileInfoMapper.deleteSysFileInfoByFileObjectName(fileObjectName);
}
@ -276,10 +293,10 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
@Override
public void fileDownloadById(Long fileId, HttpServletResponse response, HttpServletRequest request) {
SysFileInfo sysFileInfo = selectSysFileInfoByFileId(fileId);
if (StringUtils.isNotEmpty(sysFileInfo.getFileIdentifier())) {
throw new ServiceException("当前文件无法下载");
}
fileDownload(sysFileInfo, response);
// if (StringUtils.isNotEmpty(sysFileInfo.getFileIdentifier())) {
// throw new ServiceException("当前文件无法下载");
// }
fileDownload(sysFileInfo, response, request);
}
@Override
@ -288,7 +305,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
if (StringUtils.isEmpty(sysFileInfo.getFilePath())) {
throw new ServiceException("当前文件无法下载");
}
fileDownload(sysFileInfo, response);
fileDownload(sysFileInfo, response, request);
}
@Override
@ -402,6 +419,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
hierarchy = validPathSafety(hierarchy);
}
boolean isLocalUpload = true;
boolean isHierarchyPath = false;
Long fileId = null;
switch (fileType) {
case "pms-gitlink":
@ -436,6 +454,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
sysFileInfo.setFileIdentifier(genFileIdentifier());
baseFilePath += "/" + fileType + "/" + hierarchy + "/";
isCommonFileType = false;
isHierarchyPath = true;
break;
}
case "dms": {
@ -450,6 +469,13 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
isCommonFileType = false;
break;
}
case "ai-resource": {
sysFileInfo.setFileIdentifier(genFileIdentifier());
baseFilePath += "/" + fileType + "/" + hierarchy + "/";
isCommonFileType = false;
isHierarchyPath = true;
break;
}
case "zone-identifier": {
// 生成唯一标识防止用户通过id递增访问
sysFileInfo.setFileIdentifier(genFileIdentifier());
@ -512,7 +538,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
sysFileInfo.setFileOriginName(session.getFileName());
}
if (!isCommonFileType) {
if ("pms".equals(fileType)) {
if (isHierarchyPath) {
filePath = EscapeUtil.removeExtraSlashOfUrl("/" + fileType + "/" + hierarchy + "/" + filePath);
} else {
filePath = EscapeUtil.removeExtraSlashOfUrl("/" + fileType + "/" + filePath);
@ -603,7 +629,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
return fileIdentifier;
}
private void fileDownload(SysFileInfo sysFileInfo, HttpServletResponse response) {
private void fileDownload(SysFileInfo sysFileInfo, HttpServletResponse response, HttpServletRequest request) {
try {
String fileName = sysFileInfo.getFileOriginName();
String filePath = localFilePath + sysFileInfo.getFilePath();
@ -613,6 +639,8 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
}
FileUtils.setAttachmentResponseHeader(response, fileName);
// 在写文件之前记录下载日志避免写文件异常导致日志丢失
recordDownloadLog(sysFileInfo.getFileId(), request);
FileUtils.writeBytes(filePath, response.getOutputStream());
int downloadCount = 1;
if (sysFileInfo.getDownloadCount() != null) {
@ -635,6 +663,38 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
}
}
/**
* 记录文件下载日志登录用户记录用户ID和用户名未登录仅记录IP
* 参考 zone 模块 open 接口获取用户信息的方式通过请求中的 GitLink Token 解析用户
*/
private void recordDownloadLog(Long fileId, HttpServletRequest request) {
try {
SysFileDownloadLog downloadLog = new SysFileDownloadLog();
downloadLog.setFileId(fileId);
// 通过 GitLink Token 获取用户信息参考 zone OpenController.checkZoneRole 实现
String gitLinkToken = AuthUtil.getGitLinkRequestToken(request);
if (StringUtils.isNotEmpty(gitLinkToken)) {
String username = FeignUtils.getReturnData(
remoteUserService.getUserNameByGitLinkToken(gitLinkToken, SecurityConstants.INNER)
);
if (StringUtils.isNotEmpty(username)) {
downloadLog.setUserName(username);
SysUser sysUser = FeignUtils.getReturnData(
remoteUserService.getSysUserByUserName(username, SecurityConstants.INNER)
);
if (sysUser != null) {
downloadLog.setUserId(sysUser.getUserId());
}
}
}
downloadLog.setDownloadTime(DateUtils.getNowDate());
downloadLog.setIpAddr(IpUtils.getIpAddr(request));
sysFileDownloadLogMapper.insertSysFileDownloadLog(downloadLog);
} catch (Exception e) {
logger.error("记录文件下载日志失败:{}", e.getMessage());
}
}
/**
* 文件大小校验
*
@ -684,4 +744,70 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
}
return fileIdentifier;
}
/**
* 安全删除物理文件检查是否有其他数据库记录引用同一物理文件路径
*/
private void safeDeletePhysicalFile(String dbFilePath) {
int refCount = sysFileInfoMapper.countByFilePath(dbFilePath);
if (refCount <= 1) {
String physicalPath = localFilePath + StringUtils.substringAfter(dbFilePath, Constants.RESOURCE_PREFIX);
FileUtils.deleteFile(physicalPath);
}
}
@Override
public List<SysFileDownloadLogVo> getDownloadUserListByFileIds(String fileIds) {
if (StringUtils.isBlank(fileIds)) {
return new ArrayList<>();
}
String[] fileIdStrList = fileIds.split(",");
Long[] fileIdArr = new Long[fileIdStrList.length];
for (int i = 0; i < fileIdStrList.length; i++) {
fileIdArr[i] = Long.valueOf(fileIdStrList[i].trim());
}
// 查询文件信息构建 fileId -> fileOriginName 映射
Map<Long, String> fileNameMap = new HashMap<>();
for (Long fileId : fileIdArr) {
SysFileInfo fileInfo = sysFileInfoMapper.selectSysFileInfoByFileId(fileId);
if (fileInfo != null) {
fileNameMap.put(fileId, fileInfo.getFileOriginName());
}
}
List<SysFileDownloadLog> downloadLogList = sysFileDownloadLogMapper.selectDownloadLogByFileIds(fileIdArr);
if (downloadLogList == null || downloadLogList.isEmpty()) {
return new ArrayList<>();
}
return downloadLogList.stream().map(log -> {
SysFileDownloadLogVo vo = new SysFileDownloadLogVo();
vo.setFileId(log.getFileId());
vo.setFileOriginName(fileNameMap.get(log.getFileId()));
vo.setUserId(log.getUserId());
vo.setUserName(log.getUserName());
vo.setIpAddr(log.getIpAddr());
vo.setDownloadTime(log.getDownloadTime());
return vo;
}).collect(Collectors.toList());
}
@Override
public SysFileInfo cloneFileInfo(SysFileInfo source) {
SysFileInfo clone = new SysFileInfo();
clone.setFileIdentifier(genFileIdentifier());
clone.setFileOriginName(source.getFileOriginName());
clone.setFileSuffix(source.getFileSuffix());
clone.setFileSizeInfo(source.getFileSizeInfo());
clone.setFileObjectName(source.getFileObjectName());
clone.setFilePath(source.getFilePath());
clone.setDownloadUrl(source.getDownloadUrl());
clone.setContentType(source.getContentType());
clone.setContentTypeName(source.getContentTypeName());
clone.setContentTypeMessage(source.getContentTypeMessage());
clone.setMimeType(source.getMimeType());
clone.setIsPartialMatch(source.getIsPartialMatch());
clone.setDelFlag("N");
clone.setDownloadCount(0);
sysFileInfoMapper.insertSysFileInfo(clone);
return clone;
}
}

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.microservices.file.mapper.SysFileDownloadLogMapper">
<resultMap type="com.microservices.file.domain.SysFileDownloadLog" id="SysFileDownloadLogResult">
<result property="id" column="id"/>
<result property="fileId" column="file_id"/>
<result property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="downloadTime" column="download_time"/>
<result property="ipAddr" column="ip_addr"/>
</resultMap>
<sql id="selectSysFileDownloadLogVo">
select id, file_id, user_id, user_name, download_time, ip_addr
from sys_file_download_log
</sql>
<insert id="insertSysFileDownloadLog" parameterType="com.microservices.file.domain.SysFileDownloadLog" useGeneratedKeys="true" keyProperty="id">
insert into sys_file_download_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="fileId != null">file_id,</if>
<if test="userId != null">user_id,</if>
<if test="userName != null and userName != ''">user_name,</if>
<if test="downloadTime != null">download_time,</if>
<if test="ipAddr != null and ipAddr != ''">ip_addr,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="fileId != null">#{fileId},</if>
<if test="userId != null">#{userId},</if>
<if test="userName != null and userName != ''">#{userName},</if>
<if test="downloadTime != null">#{downloadTime},</if>
<if test="ipAddr != null and ipAddr != ''">#{ipAddr},</if>
</trim>
</insert>
<select id="selectDownloadLogByFileIds" resultMap="SysFileDownloadLogResult">
<include refid="selectSysFileDownloadLogVo"/>
where file_id in
<foreach item="fileId" collection="fileIds" open="(" separator="," close=")">
#{fileId}
</foreach>
order by download_time desc
</select>
</mapper>

View File

@ -97,6 +97,7 @@
<if test="contentTypeMessage != null">content_type_message,</if>
<if test="mimeType != null">mime_type,</if>
<if test="isPartialMatch != null">is_partial_match,</if>
<if test="downloadCount != null">download_count,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="fileOriginName != null and fileOriginName != ''">#{fileOriginName},</if>
@ -115,6 +116,7 @@
<if test="contentTypeMessage != null">#{contentTypeMessage},</if>
<if test="mimeType != null">#{mimeType},</if>
<if test="isPartialMatch != null">#{isPartialMatch},</if>
<if test="downloadCount != null">#{downloadCount},</if>
</trim>
</insert>
@ -159,4 +161,8 @@
from sys_file_info
where file_object_name = #{fileObjectName}
</delete>
<select id="countByFilePath" resultType="int">
select count(*) from sys_file_info where file_path = #{filePath} and del_flag = 'N'
</select>
</mapper>

View File

@ -139,6 +139,12 @@
<version>3.6.2</version>
<scope>compile</scope>
</dependency>
<!-- Apache Commons Compress - 解决 ZIP 文件中文文件名 GBK 编码导致的 MALFORMED 问题 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
</dependency>
</dependencies>
<build>

View File

@ -21,13 +21,17 @@ import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceInputVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceSearchVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceUpdateVo;
import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.service.IAiResourceParseService;
import com.microservices.zone.resource.service.IZoneResourceService;
import io.swagger.annotations.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.util.Collections;
import java.util.List;
@ -43,6 +47,9 @@ public class FrontController extends BaseController {
@Autowired
private IZoneResourceService zoneResourceService;
@Autowired
private IAiResourceParseService aiResourceParseService;
@Autowired
private IZoneProjectService zoneProjectService;
@ -162,7 +169,7 @@ public class FrontController extends BaseController {
@Log(title = "专区前台项目操作", businessType = BusinessType.UPDATE)
@PutMapping("/editProject/{projectId}")
@ApiOperation(value = "编辑项目聚合")
public AjaxResult editProject(@PathVariable("projectId") Long projectId,@RequestBody ZoneProjectFrontUpdateVo zoneProjectFrontUpdateVo) {
public AjaxResult editProject(@PathVariable("projectId") Long projectId, @RequestBody ZoneProjectFrontUpdateVo zoneProjectFrontUpdateVo) {
return success(zoneProjectService.updateMyZoneProject(projectId, zoneProjectFrontUpdateVo.toZoneProjectUpdateVo()));
}
@ -194,4 +201,187 @@ public class FrontController extends BaseController {
}
return genericsSuccess(zoneFrontRoleVo);
}
// ==================== Skill资源广场 - 我的资源管理 ====================
/**
* 上传ZIP并解析
*/
@RequiresPermissions("zone:front:skill:add")
@PostMapping("/skill/parse")
@ApiOperation("上传ZIP并解析Skill包")
@ApiImplicitParams({
@ApiImplicitParam(name = "file", value = "文件实体", paramType = "form", dataType = "_file"),
})
public GenericsAjaxResult<AiSkillParseResultVo> parseSkillZip(@RequestPart("file") MultipartFile file) {
return genericsSuccess(aiResourceParseService.parseSkillZip(file));
}
/**
* 创建Skill资源
*/
@RequiresPermissions("zone:front:skill:add")
@Log(title = "创建Skill资源", businessType = BusinessType.INSERT)
@PostMapping("/skill")
@ApiOperation("创建Skill资源")
public AjaxResult addSkill(@RequestBody @Validated AiSkillInputVo inputVo) {
return success(zoneResourceService.insertAiSkillResource(inputVo));
}
@RequiresPermissions("zone:front:resource:add")
@Log(title = "重新提交被驳回的资源", businessType = BusinessType.UPDATE)
@PutMapping("/resource/{id}/resubmit")
@ApiOperation(value = "重新提交被驳回的资源")
public AjaxResult resubmitAiResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id) {
return toAjax(zoneResourceService.resubmitAiResource(id));
}
@RequiresPermissions("zone:front:resource:edit")
@Log(title = "撤回审核中的资源", businessType = BusinessType.UPDATE)
@PutMapping("/resource/{id}/withdraw")
@ApiOperation(value = "撤回审核中的资源")
public AjaxResult withdrawAiResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id) {
return toAjax(zoneResourceService.withdrawAiResource(id));
}
/**
* 下架资源
*/
@RequiresPermissions("zone:front:resource:edit")
@Log(title = "下架资源", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/takeDown")
@ApiOperation("下架资源")
public AjaxResult takeDownResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id,
@RequestParam(value = "reason", required = false) String reason) {
return toAjax(zoneResourceService.takeDownAiResource(id, reason));
}
/**
* 上架资源
*/
@RequiresPermissions("zone:front:resource:edit")
@Log(title = "上架资源", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/takeUp")
@ApiOperation("上架资源")
public AjaxResult takeUpResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id,
@RequestParam(value = "reason", required = false) String reason) {
return toAjax(zoneResourceService.takeUpAiResource(id, reason));
}
/**
* 获取资源统计
*/
@RequiresPermissions("zone:front:resource:query")
@GetMapping("/resourceStatistics")
@ApiOperation("获取资源统计")
@ApiImplicitParams({
@ApiImplicitParam(name = "isMine", value = "是否查询我的资源(true-我的资源,false-专区资源)", paramType = "query", dataType = "boolean"),
@ApiImplicitParam(name = "zoneId", value = "专区ID", paramType = "query", dataType = "Long", required = true),
@ApiImplicitParam(name = "resourceCategory", value = "资源分类(SKILL/MODEL/DATASET),为空则查询所有", paramType = "query", dataType = "String")
})
public GenericsAjaxResult<UserResourceStatisticsVo> resourceStatistics(
@ApiParam(name = "isMine", value = "是否查询我的资源") @RequestParam(value = "isMine", required = false) Boolean isMine,
@ApiParam(name = "zoneId") @RequestParam(value = "zoneId") Long zoneId,
@ApiParam(name = "resourceCategory") @RequestParam(value = "resourceCategory", required = false) String resourceCategory) {
return genericsSuccess(zoneResourceService.getResourceStatistics(zoneId, isMine, resourceCategory));
}
/**
* 获取我的项目聚合统计
*/
@RequiresPermissions("zone:front:project:query")
@GetMapping("/myProjectStatistics")
@ApiOperation("获取我的项目聚合统计")
@ApiImplicitParams({
@ApiImplicitParam(name = "zoneId", value = "专区ID", paramType = "query", dataType = "Long", required = true)
})
public GenericsAjaxResult<UserProjectStatisticsVo> myProjectStatistics(@ApiParam(name = "zoneId") @RequestParam(value = "zoneId") Long zoneId) {
return genericsSuccess(zoneProjectService.getUserProjectStatistics(zoneId,SecurityUtils.getUsername()));
}
/**
* 点赞/取消点赞
*/
@RequiresPermissions("zone:front:resource:query")
@PostMapping("/{id}/like")
@ApiOperation("点赞/取消点赞资源")
public AjaxResult toggleLike(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id) {
boolean liked = zoneResourceService.toggleZoneResourcesLike(id);
return success(liked ? "已点赞" : "已取消点赞");
}
/**
* 修改Skill资源
*/
@RequiresPermissions("zone:front:skill:edit")
@Log(title = "修改Skill资源", businessType = BusinessType.UPDATE)
@PutMapping("/skill")
@ApiOperation("修改Skill资源")
public AjaxResult editSkill(@RequestBody @Validated AiSkillUpdateVo updateVo) {
return success(zoneResourceService.updateAiSkillResource(updateVo));
}
// ==================== 模型资源 ====================
/**
* 创建Model资源
*/
@RequiresPermissions("zone:front:model:add")
@Log(title = "创建Model资源", businessType = BusinessType.INSERT)
@PostMapping("/resource/model")
@ApiOperation("创建Model资源")
public AjaxResult addModel(@RequestBody @Validated AiModelInputVo inputVo) {
return success(zoneResourceService.insertAiModelResource(inputVo));
}
/**
* 修改Model资源
*/
@RequiresPermissions("zone:front:model:edit")
@Log(title = "修改Model资源", businessType = BusinessType.UPDATE)
@PutMapping("/resource/model")
@ApiOperation("修改Model资源")
public AjaxResult editModel(@RequestBody @Validated AiModelUpdateVo updateVo) {
return success(zoneResourceService.updateAiModelResource(updateVo));
}
// ==================== 数据集资源 ====================
/**
* 创建Dataset资源
*/
@RequiresPermissions("zone:front:dataset:add")
@Log(title = "创建Dataset资源", businessType = BusinessType.INSERT)
@PostMapping("/resource/dataset")
@ApiOperation("创建Dataset资源")
public AjaxResult addDataset(@RequestBody @Validated AiDatasetInputVo inputVo) {
return success(zoneResourceService.insertAiDatasetResource(inputVo));
}
/**
* 修改Dataset资源
*/
@RequiresPermissions("zone:front:dataset:edit")
@Log(title = "修改Dataset资源", businessType = BusinessType.UPDATE)
@PutMapping("/resource/dataset")
@ApiOperation("修改Dataset资源")
public AjaxResult editDataset(@RequestBody @Validated AiDatasetUpdateVo updateVo) {
return success(zoneResourceService.updateAiDatasetResource(updateVo));
}
/**
* 查询资源是否点赞
*/
@RequiresPermissions("zone:front:resource:query")
@GetMapping("/{ids}/like")
@ApiOperation(value = "查询资源是否点赞")
public GenericsTableDataInfo<JSONObject> myResource(@PathVariable("ids") String ids) {
return new GenericsTableDataInfo<>(zoneResourceService.selectZoneResourcesIsLike(ids));
}
}

View File

@ -16,6 +16,9 @@ import com.microservices.zone.dataset.service.IZoneDataSetService;
import com.microservices.zone.detail.domain.vo.ZoneBaseDataVo;
import com.microservices.zone.detail.domain.vo.ZoneDetailDataVo;
import com.microservices.zone.detail.service.IZoneDetailService;
import com.microservices.zone.journals.domain.vo.ZoneJournalsDataTableVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsParentDataVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsSearchVo;
import com.microservices.zone.member.domain.vo.ZoneMemberDataVo;
import com.microservices.zone.member.domain.vo.ZoneMemberOpenSearchVo;
import com.microservices.zone.member.domain.vo.ZoneMemberOverviewVo;
@ -31,10 +34,14 @@ import com.microservices.zone.project.domain.vo.ZoneProjectSearchVo;
import com.microservices.zone.project.domain.vo.ZoneStatisticsVo;
import com.microservices.zone.project.service.IZoneProjectService;
import com.microservices.zone.project.service.IZoneProjectTypeService;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.domain.ZoneResourceDomain;
import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceDomainSearchVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceOpenSearchVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceSearchVo;
import com.microservices.zone.resource.enums.AiResourceCategory;
import com.microservices.zone.resource.service.IZoneResourceDomainService;
import com.microservices.zone.resource.service.IZoneResourceService;
import com.microservices.zone.resource.service.IZoneResourceTypeService;
@ -103,6 +110,9 @@ public class OpenController extends BaseController {
@Autowired
private IZoneVisitsService zoneVisitsService;
@Autowired
private IGlobalSearchService globalSearchService;
/**
* 新增特色专区用户浏览
*/
@ -323,14 +333,9 @@ public class OpenController extends BaseController {
*/
@GetMapping("/{zoneId}/resourceDomain/list")
@ApiOperation("查询特色专区资源领域列表")
public GenericsTableDataInfo<ZoneResourceDomain> resourceList(
@ApiParam(name = "zoneId", value = "专区Id", required = true) @PathVariable("zoneId") Long zoneId,
@ApiParam(name = "name", value = "资源领域名称") String name) {
ZoneResourceDomain zoneResourceDomain = new ZoneResourceDomain();
zoneResourceDomain.setName(name);
zoneResourceDomain.setZoneId(zoneId);
public GenericsTableDataInfo<ZoneResourceDomain> resourceList(ZoneResourceDomainSearchVo zoneResourceDomainSearchVo) {
startPage();
List<ZoneResourceDomain> list = zoneResourceDomainService.selectZoneResourceDomainList(zoneResourceDomain);
List<ZoneResourceDomain> list = zoneResourceDomainService.selectZoneResourceDomainList(zoneResourceDomainSearchVo.toZoneResourceDomain());
return getGenericsDataTable(list);
}
@ -441,9 +446,6 @@ public class OpenController extends BaseController {
return getGenericsDataTable(list);
}
@Autowired
private IGlobalSearchService globalSearchService;
/**
* 全局搜索接口POST方法
*
@ -456,4 +458,24 @@ public class OpenController extends BaseController {
GlobalSearchResponse response = globalSearchService.globalSearch(globalSearchRequest);
return new GenericsTableDataInfo<>(response.getResults(), response.getTotal());
}
// ==================== 通用评论 - 开放接口 ====================
@Autowired
private com.microservices.zone.journals.service.IZoneJournalsService zoneJournalsService;
/**
* 公开-资源评论列表
*/
@GetMapping("/journals")
@ApiOperation("公开-评论列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageNum", value = "当前记录起始索引", paramType = "query", dataType = "Integer"),
@ApiImplicitParam(name = "pageSize", value = "每页显示记录数", paramType = "query", dataType = "Integer")
})
public GenericsAjaxResult<ZoneJournalsDataTableVo> journalsList(@Validated ZoneJournalsSearchVo searchVo) {
startPage();
List<ZoneJournalsParentDataVo> list = zoneJournalsService.selectJournalsList(searchVo);
return genericsSuccess(ZoneJournalsDataTableVo.toZoneJournalsDataTableVo(getDataTable(list)));
}
}

View File

@ -111,10 +111,10 @@ public class ZoneDetailServiceImpl implements IZoneDetailService {
private ZonePartnersTypeMapper zonePartnersTypeMapper;
@Autowired
@Lazy
private IZoneSpecialProjectService zoneSpecialProjectService;
private IZoneMemberService zoneMemberService;
@Autowired
@Lazy
private IZoneMemberService zoneMemberService;
private IZoneSpecialProjectService zoneSpecialProjectService;
@Value("${zone.zoneAdminUrl}")
private String zoneAdminUrl;
@ -128,12 +128,12 @@ public class ZoneDetailServiceImpl implements IZoneDetailService {
private String docManagePath;
@Value("${zone.projectManagePath:/zone/project/list}")
private String projectManagePath;
@Value("${zone.autoMemberZoneIds:}")
private String autoMemberZoneIds;
@Value("${zone.resourceManageUrl}")
private String resourceManageUrl;
@Value("${zone.docManageUrl}")
private String docManageUrl;
@Value("${zone.autoMemberZoneIds:}")
private String autoMemberZoneIds;
@Value("${zone.autoMemberTypeName:}")
private String autoMemberTypeName;
@Value("${zone.autoMemberLevelName:}")

View File

@ -0,0 +1,109 @@
package com.microservices.zone.journals.controller;
import com.microservices.common.core.web.controller.BaseController;
import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.core.web.page.GenericsTableDataInfo;
import com.microservices.common.core.web.page.TableDataInfo;
import com.microservices.common.log.annotation.Log;
import com.microservices.common.log.enums.BusinessType;
import com.microservices.common.security.annotation.RequiresPermissions;
import com.microservices.zone.journals.domain.ZoneJournals;
import com.microservices.zone.journals.domain.vo.ZoneJournalsInputVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsParentDataVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsSearchVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsUpdateVo;
import com.microservices.zone.journals.service.IZoneJournalsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 通用评论Controller
*
* @author otto
* @date 2025-04-30
*/
@RestController
@RequestMapping("/journals")
@Api(tags = "通用评论接口")
public class ZoneJournalsController extends BaseController {
@Autowired
private IZoneJournalsService zoneJournalsService;
/**
* 查询评论列表二级结构
*/
// @RequiresPermissions("zone:journals:list")
@GetMapping("/list")
@ApiOperation("查询评论列表")
public GenericsTableDataInfo<ZoneJournalsParentDataVo> list(ZoneJournalsSearchVo searchVo) {
startPage();
List<ZoneJournalsParentDataVo> list = zoneJournalsService.selectJournalsList(searchVo);
return new GenericsTableDataInfo<>(list);
}
/**
* 获取评论详细信息
*/
// @RequiresPermissions("zone:journals:query")
@GetMapping("/{id}")
@ApiOperation("获取评论详细信息")
public AjaxResult getInfo(@ApiParam(value = "评论ID", required = true) @PathVariable("id") Long id) {
ZoneJournals zoneJournals = zoneJournalsService.selectZoneJournalsById(id);
return success(zoneJournals);
}
/**
* 新增评论
*/
// @RequiresPermissions("zone:journals:add")
@Log(title = "新增评论", businessType = BusinessType.INSERT)
@PostMapping
@ApiOperation("新增评论")
public AjaxResult add(@RequestBody @Validated ZoneJournalsInputVo inputVo) {
Long journalId = zoneJournalsService.insertJournals(inputVo);
return success(journalId);
}
/**
* 修改评论
*/
// @RequiresPermissions("zone:journals:edit")
@Log(title = "修改评论", businessType = BusinessType.UPDATE)
@PutMapping
@ApiOperation("修改评论")
public AjaxResult edit(@RequestBody @Validated ZoneJournalsUpdateVo updateVo) {
int result = zoneJournalsService.updateJournals(updateVo);
return toAjax(result);
}
/**
* 删除评论
*/
// @RequiresPermissions("zone:journals:remove")
@Log(title = "删除评论", businessType = BusinessType.DELETE)
@DeleteMapping("/{id}")
@ApiOperation("删除评论")
public AjaxResult remove(@ApiParam(value = "评论ID", required = true) @PathVariable("id") Long id) {
int result = zoneJournalsService.deleteJournalsById(id);
return toAjax(result);
}
/**
* 批量删除评论
*/
// @RequiresPermissions("zone:journals:remove")
@Log(title = "批量删除评论", businessType = BusinessType.DELETE)
@DeleteMapping("/batch/{ids}")
@ApiOperation("批量删除评论")
public AjaxResult removeBatch(@ApiParam(value = "评论ID数组", required = true) @PathVariable Long[] ids) {
int result = zoneJournalsService.deleteJournalsByIds(ids);
return toAjax(result);
}
}

View File

@ -0,0 +1,84 @@
package com.microservices.zone.journals.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.common.core.web.domain.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.util.Date;
/**
* 通用评论对象 zone_journals
*
* @author otto
* @date 2025-04-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("通用评论对象")
public class ZoneJournals extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 评论对象类型常量
*/
public static final String JOURNAL_TYPE_ZONE_RESOURCE = "resource";
/** 评论ID */
@ApiModelProperty(value = "评论ID")
private Long id;
/** 评论对象ID */
@ApiModelProperty(value = "评论对象ID")
private Long journalizedId;
/** 评论对象类型 */
@ApiModelProperty(value = "评论对象类型")
private String journalizedType;
/** GitLink用户ID */
@ApiModelProperty(value = "GitLink用户ID")
private Long userId;
/** 评论内容 */
@ApiModelProperty(value = "评论内容")
private String notes;
/** 父级评论ID */
@ApiModelProperty(value = "父级评论ID")
private Long parentId;
/** 回复的评论ID */
@ApiModelProperty(value = "回复的评论ID")
private Long replyId;
/** 评论数量 */
@ApiModelProperty(value = "评论数量")
private Integer commentsCount;
/** 附件标识列表 */
@ApiModelProperty(value = "附件标识列表")
private String attachmentIdentifiers;
/** @用户列表 */
@ApiModelProperty(value = "@用户列表")
private String receiversLogin;
/** 删除标志 */
@ApiModelProperty(value = "删除标志")
private String delFlag;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date createdOn;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
private Date updatedOn;
}

View File

@ -0,0 +1,23 @@
package com.microservices.zone.journals.domain.vo;
import com.microservices.system.api.domain.SimpleSysUser;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通用评论子级评论数据VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("子级评论数据对象")
public class ZoneJournalsChildrenDataVo extends ZoneJournalsDataVo {
/** 回复的用户 */
@ApiModelProperty(value = "回复的用户")
private SimpleSysUser reply_user;
}

View File

@ -0,0 +1,40 @@
package com.microservices.zone.journals.domain.vo;
import com.microservices.common.core.web.page.TableDataInfo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* 评论列表对象
*/
@Data
@ApiModel("评论列表对象")
public class ZoneJournalsDataTableVo {
@ApiModelProperty(value = "评论列表")
private List<?> journals;
@ApiModelProperty(value = "评论总数量")
private Long total_comment_journals_count;
@ApiModelProperty(value = "当前总数量")
private Long total_count;
@ApiModelProperty(value = "评论+操作总数量")
private Long total_journals_count;
@ApiModelProperty(value = "操作总数量")
private Long total_operate_journals_count;
public static ZoneJournalsDataTableVo toZoneJournalsDataTableVo(TableDataInfo tableDataInfo) {
ZoneJournalsDataTableVo target = new ZoneJournalsDataTableVo();
target.setJournals(tableDataInfo.getRows());
target.setTotal_comment_journals_count(tableDataInfo.getTotal());
target.setTotal_count(tableDataInfo.getTotal());
target.setTotal_journals_count(tableDataInfo.getTotal());
target.setTotal_operate_journals_count(0L);
return target;
}
}

View File

@ -0,0 +1,57 @@
package com.microservices.zone.journals.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.system.api.domain.SimpleGitlinkFileInfo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.domain.SimpleSysUser;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
import java.util.List;
/**
* 通用评论基础数据VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@ApiModel("评论数据对象")
public class ZoneJournalsDataVo {
/** 评论ID */
@ApiModelProperty(value = "评论ID")
private Long id;
/** 评论用户 */
@ApiModelProperty(value = "评论用户")
private SimpleSysUser user;
/** 评论内容 */
@ApiModelProperty(value = "评论内容")
private String notes;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "创建时间")
private Date created_on;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "更新时间")
private Date updated_on;
/** 附件列表 */
@ApiModelProperty(value = "附件列表")
private List<SimpleGitlinkFileInfo> attachments;
/** 评论数量 */
@ApiModelProperty(value = "评论数量")
private Integer comments_count = 0;
/** 是否详情 */
@ApiModelProperty(value = "是否详情")
private Boolean is_journal_detail = false;
}

View File

@ -0,0 +1,52 @@
package com.microservices.zone.journals.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 通用评论输入VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@ApiModel("评论输入对象")
public class ZoneJournalsInputVo {
/** 评论对象ID */
@NotNull(message = "评论对象ID不能为空")
@ApiModelProperty(value = "评论对象ID", required = true)
private Long journalized_id;
/** 评论对象类型 */
@NotBlank(message = "评论对象类型不能为空资源resource")
@ApiModelProperty(value = "评论对象类型", required = true)
private String journalized_type;
/** 父级评论ID */
@ApiModelProperty(value = "父级评论ID")
private Long parent_id;
/** 回复的评论ID */
@ApiModelProperty(value = "回复的评论ID")
private Long reply_id;
/** 评论内容 */
@NotBlank(message = "评论内容不能为空")
@Size(max = 2000, message = "评论内容不能超过2000个字符")
@ApiModelProperty(value = "评论内容", required = true)
private String notes;
/** @用户列表 */
@ApiModelProperty(value = "@用户列表")
private String[] receivers_login;
/** 附件标识列表 */
@ApiModelProperty(value = "附件标识列表")
private String attachment_identifiers;
}

View File

@ -0,0 +1,24 @@
package com.microservices.zone.journals.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 通用评论父级评论数据VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("父级评论数据对象")
public class ZoneJournalsParentDataVo extends ZoneJournalsDataVo {
/** 子级评论列表 */
@ApiModelProperty(value = "子级评论列表")
private List<ZoneJournalsChildrenDataVo> children_journals;
}

View File

@ -0,0 +1,28 @@
package com.microservices.zone.journals.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 通用评论查询VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@ApiModel("评论查询对象")
public class ZoneJournalsSearchVo {
/** 评论对象ID */
@NotNull
@ApiModelProperty(value = "评论对象ID")
private Long journalized_id;
/** 评论对象类型 */
@NotBlank
@ApiModelProperty(value = "评论对象类型资源resource")
private String journalized_type;
}

View File

@ -0,0 +1,39 @@
package com.microservices.zone.journals.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 通用评论更新VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@ApiModel("评论更新对象")
public class ZoneJournalsUpdateVo {
/** 评论ID */
@NotNull(message = "评论ID不能为空")
@ApiModelProperty(value = "评论ID", required = true)
private Long id;
/** 评论内容 */
@NotBlank(message = "评论内容不能为空")
@Size(max = 2000, message = "评论内容不能超过2000个字符")
@ApiModelProperty(value = "评论内容", required = true)
private String notes;
/** @用户列表 */
@ApiModelProperty(value = "@用户列表")
private String[] receivers_login;
/** 附件标识列表 */
@ApiModelProperty(value = "附件标识列表")
private String attachment_identifiers;
}

View File

@ -0,0 +1,115 @@
package com.microservices.zone.journals.mapper;
import com.microservices.zone.journals.domain.ZoneJournals;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 通用评论Mapper接口
*
* @author otto
* @date 2025-04-30
*/
@Mapper
public interface ZoneJournalsMapper {
/**
* 查询评论
*
* @param id 评论主键
* @return 评论
*/
ZoneJournals selectZoneJournalsById(Long id);
/**
* 查询评论列表父评论
*
* @param zoneJournals 评论查询条件
* @return 评论集合
*/
List<ZoneJournals> selectZoneJournalsList(ZoneJournals zoneJournals);
/**
* 查询子评论列表
*
* @param parentId 父评论ID
* @return 子评论集合
*/
List<ZoneJournals> selectChildrenZoneJournalsList(@Param("parentId") Long parentId);
/**
* 新增评论
*
* @param zoneJournals 评论
* @return 结果
*/
int insertZoneJournals(ZoneJournals zoneJournals);
/**
* 修改评论
*
* @param zoneJournals 评论
* @return 结果
*/
int updateZoneJournals(ZoneJournals zoneJournals);
/**
* 删除评论逻辑删除
*
* @param id 评论主键
* @return 结果
*/
int deleteZoneJournalsById(Long id);
/**
* 批量删除评论逻辑删除
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
int deleteZoneJournalsByIds(Long[] ids);
/**
* 删除子评论逻辑删除
*
* @param parentId 父评论ID
* @return 结果
*/
int deleteZoneJournalsByParentId(@Param("parentId") Long parentId);
/**
* 根据类型和评论对象Id删除所有评论
*
* @param type 类型
* @param journalId 评论对象Id
*/
void deleteZoneJournalsByTypeAndJournalId(@Param("type") String type, @Param("journalId") Long journalId);
/**
* 批量更新关联ID
*
* @param type 类型
* @param oldJournalId 旧评论对象Id
* @param journalId 新评论对象Id
*/
void updateJournalsIdByTypeAndOldId(@Param("type") String type, @Param("oldJournalId") Long oldJournalId, @Param("journalId") Long journalId);
/**
* 更新父评论的子评论数量
*
* @param id 父评论ID
* @return 结果
*/
int updateCommentsCountById(@Param("id") Long id);
/**
* 统计评论数量
*
* @param journalizedId 评论对象ID
* @param journalizedType 评论对象类型
* @return 评论数量
*/
int countByJournalized(@Param("journalizedId") Long journalizedId, @Param("journalizedType") String journalizedType);
}

View File

@ -0,0 +1,83 @@
package com.microservices.zone.journals.service;
import com.microservices.zone.journals.domain.ZoneJournals;
import com.microservices.zone.journals.domain.vo.ZoneJournalsInputVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsParentDataVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsSearchVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsUpdateVo;
import java.util.List;
/**
* 通用评论Service接口
*
* @author otto
* @date 2025-04-30
*/
public interface IZoneJournalsService {
/**
* 查询评论
*
* @param id 评论主键
* @return 评论
*/
ZoneJournals selectZoneJournalsById(Long id);
/**
* 查询评论列表二级结构
*
* @param searchVo 查询条件
* @return 评论列表父评论包含子评论
*/
List<ZoneJournalsParentDataVo> selectJournalsList(ZoneJournalsSearchVo searchVo);
/**
* 新增评论
*
* @param inputVo 评论输入信息
* @return 评论ID
*/
Long insertJournals(ZoneJournalsInputVo inputVo);
/**
* 修改评论
*
* @param updateVo 评论更新信息
* @return 结果
*/
int updateJournals(ZoneJournalsUpdateVo updateVo);
/**
* 删除评论级联删除子评论
*
* @param id 评论主键
* @return 结果
*/
int deleteJournalsById(Long id);
/**
* 批量删除评论
*
* @param ids 需要删除的评论主键集合
* @return 结果
*/
int deleteJournalsByIds(Long[] ids);
/**
* 根据类型和评论对象Id删除所有评论
*
* @param type 类型
* @param journalId 评论对象Id
*/
void deleteJournalsByTypeAndJournalId(String type, Long journalId);
/**
* 批量更新关联ID
*
* @param type 类型
* @param oldJournalId 旧评论对象Id
* @param journalId 新评论对象Id
*/
void updateJournalsIdByTypeAndOldId(String type, Long oldJournalId, Long journalId);
}

View File

@ -0,0 +1,406 @@
package com.microservices.zone.journals.service.impl;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.DateUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.domain.SimpleGitlinkFileInfo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.domain.SimpleSysUser;
import com.microservices.system.api.utils.FeignUtils;
import com.microservices.zone.common.service.IZoneCommonService;
import com.microservices.zone.journals.domain.ZoneJournals;
import com.microservices.zone.journals.domain.vo.ZoneJournalsChildrenDataVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsInputVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsParentDataVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsSearchVo;
import com.microservices.zone.journals.domain.vo.ZoneJournalsUpdateVo;
import com.microservices.zone.journals.mapper.ZoneJournalsMapper;
import com.microservices.zone.journals.service.IZoneJournalsService;
import com.microservices.zone.resource.service.IZoneResourceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* 通用评论Service业务层处理
*
* @author otto
* @date 2025-04-30
*/
@Service
public class ZoneJournalsServiceImpl implements IZoneJournalsService {
@Autowired
private ZoneJournalsMapper zoneJournalsMapper;
@Autowired
private IZoneCommonService zoneCommonService;
@Autowired
private IZoneResourceService zoneResourceService;
@Autowired
private RemoteFileService remoteFileService;
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 查询评论
*
* @param id 评论主键
* @return 评论
*/
@Override
public ZoneJournals selectZoneJournalsById(Long id) {
ZoneJournals zoneJournals = zoneJournalsMapper.selectZoneJournalsById(id);
if (zoneJournals == null) {
throw new ServiceException("该评论不存在(评论Id[%d])", id);
}
// 校验评论对象是否存在
checkByType(zoneJournals);
return zoneJournals;
}
/**
* 查询评论列表二级结构
*
* @param searchVo 查询条件
* @return 评论列表父评论包含子评论
*/
@Override
public List<ZoneJournalsParentDataVo> selectJournalsList(ZoneJournalsSearchVo searchVo) {
// 校验评论对象是否存在
ZoneJournals checkJournal = new ZoneJournals();
checkJournal.setJournalizedId(searchVo.getJournalized_id());
checkJournal.setJournalizedType(searchVo.getJournalized_type());
checkByType(checkJournal);
// 查询父评论列表
ZoneJournals query = new ZoneJournals();
query.setJournalizedId(searchVo.getJournalized_id());
query.setJournalizedType(searchVo.getJournalized_type());
List<ZoneJournals> parentJournalsList = zoneJournalsMapper.selectZoneJournalsList(query);
// 转换为VO并组装子评论
return parentJournalsList.stream()
.map(this::toParentDataVo)
.collect(Collectors.toList());
}
/**
* 新增评论
*
* @param inputVo 评论输入信息
* @return 评论ID
*/
@Override
public Long insertJournals(ZoneJournalsInputVo inputVo) {
// 校验评论对象类型
ZoneJournals zoneJournals = new ZoneJournals();
zoneJournals.setJournalizedId(inputVo.getJournalized_id());
zoneJournals.setJournalizedType(inputVo.getJournalized_type());
checkByType(zoneJournals);
// 如果是子评论校验父评论
if (inputVo.getParent_id() != null) {
ZoneJournals parentJournal = selectZoneJournalsById(inputVo.getParent_id());
if (parentJournal.getParentId() != null) {
throw new ServiceException("该评论非父级评论(评论ID[%d])", parentJournal.getId());
}
if (!parentJournal.getJournalizedType().equals(inputVo.getJournalized_type())) {
throw new ServiceException("当前父级评论与当前评论类型不相同");
}
if (!parentJournal.getJournalizedId().equals(inputVo.getJournalized_id())) {
throw new ServiceException("当前父级评论与当前评论对象不相同");
}
}
// 如果有回复评论校验回复评论
if (inputVo.getReply_id() != null) {
if (inputVo.getParent_id() == null) {
throw new ServiceException("一级评论不能进行回复操作");
}
ZoneJournals replyJournal = selectZoneJournalsById(inputVo.getReply_id());
if (!replyJournal.getJournalizedType().equals(inputVo.getJournalized_type())) {
throw new ServiceException("当前回复评论与当前评论类型不相同");
}
if (!replyJournal.getJournalizedId().equals(inputVo.getJournalized_id())) {
throw new ServiceException("当前回复评论与当前评论对象不相同");
}
}
// 校验附件列表
checkAttachments(inputVo.getAttachment_identifiers());
// 构建评论对象
ZoneJournals newJournal = new ZoneJournals();
newJournal.setJournalizedId(inputVo.getJournalized_id());
newJournal.setJournalizedType(inputVo.getJournalized_type());
newJournal.setParentId(inputVo.getParent_id());
newJournal.setReplyId(inputVo.getReply_id());
newJournal.setNotes(inputVo.getNotes());
newJournal.setAttachmentIdentifiers(inputVo.getAttachment_identifiers());
// 处理@用户列表
if (inputVo.getReceivers_login() != null && inputVo.getReceivers_login().length > 0) {
try {
newJournal.setReceiversLogin(objectMapper.writeValueAsString(inputVo.getReceivers_login()));
} catch (Exception e) {
throw new ServiceException("@用户列表格式错误");
}
}
// 获取当前用户ID
Long userId = SecurityUtils.getUserId();
if (userId == null || userId == 0) {
throw new ServiceException("无法获取当前用户信息");
}
newJournal.setUserId(userId);
newJournal.setCreateBy(SecurityUtils.getUsername());
newJournal.setCreatedOn(DateUtils.getNowDate());
// 插入评论
zoneJournalsMapper.insertZoneJournals(newJournal);
// 如果是子评论更新父评论的子评论数量
if (inputVo.getParent_id() != null) {
zoneJournalsMapper.updateCommentsCountById(inputVo.getParent_id());
}
return newJournal.getId();
}
/**
* 修改评论
*
* @param updateVo 评论更新信息
* @return 结果
*/
@Override
public int updateJournals(ZoneJournalsUpdateVo updateVo) {
// 查询原评论并校验
ZoneJournals oldJournal = selectZoneJournalsById(updateVo.getId());
// 校验是否为评论创建者
Long userId = SecurityUtils.getUserId();
if (!oldJournal.getUserId().equals(userId)) {
throw new ServiceException("只能修改自己的评论");
}
// 校验附件列表
checkAttachments(updateVo.getAttachment_identifiers());
// 构建更新对象
ZoneJournals updateJournal = new ZoneJournals();
updateJournal.setId(updateVo.getId());
updateJournal.setNotes(updateVo.getNotes());
updateJournal.setAttachmentIdentifiers(updateVo.getAttachment_identifiers());
// 处理@用户列表
if (updateVo.getReceivers_login() != null && updateVo.getReceivers_login().length > 0) {
try {
updateJournal.setReceiversLogin(objectMapper.writeValueAsString(updateVo.getReceivers_login()));
} catch (Exception e) {
throw new ServiceException("@用户列表格式错误");
}
}
updateJournal.setUpdateBy(SecurityUtils.getUsername());
updateJournal.setUpdatedOn(DateUtils.getNowDate());
return zoneJournalsMapper.updateZoneJournals(updateJournal);
}
/**
* 删除评论级联删除子评论
*
* @param id 评论主键
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteJournalsById(Long id) {
ZoneJournals zoneJournals = selectZoneJournalsById(id);
// 校验是否为评论创建者
Long userId = SecurityUtils.getUserId();
if (!zoneJournals.getUserId().equals(userId)) {
throw new ServiceException("只能删除自己的评论");
}
// 如果是父级评论先删除所有子评论
if (zoneJournals.getParentId() == null) {
List<ZoneJournals> childrenJournalsList = zoneJournalsMapper.selectChildrenZoneJournalsList(zoneJournals.getId());
for (ZoneJournals childrenJournal : childrenJournalsList) {
zoneJournalsMapper.deleteZoneJournalsById(childrenJournal.getId());
}
}
return zoneJournalsMapper.deleteZoneJournalsById(id);
}
/**
* 批量删除评论
*
* @param ids 需要删除的评论主键集合
* @return 结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int deleteJournalsByIds(Long[] ids) {
int success = 0;
for (Long id : ids) {
success += deleteJournalsById(id);
}
return success;
}
/**
* 根据类型和评论对象Id删除所有评论
*
* @param type 类型
* @param journalId 评论对象Id
*/
@Override
public void deleteJournalsByTypeAndJournalId(String type, Long journalId) {
zoneJournalsMapper.deleteZoneJournalsByTypeAndJournalId(type, journalId);
}
/**
* 批量更新关联ID
*
* @param type 类型
* @param oldJournalId 旧评论对象Id
* @param journalId 新评论对象Id
*/
@Override
public void updateJournalsIdByTypeAndOldId(String type, Long oldJournalId, Long journalId) {
zoneJournalsMapper.updateJournalsIdByTypeAndOldId(type, oldJournalId, journalId);
}
/**
* 根据类型校验评论对象是否存在
*
* @param zoneJournals 评论对象
*/
private void checkByType(ZoneJournals zoneJournals) {
if (zoneJournals.getJournalizedType().equals(ZoneJournals.JOURNAL_TYPE_ZONE_RESOURCE)) {// 校验资源是否存在
zoneResourceService.selectZoneResourceById(zoneJournals.getJournalizedId());
} else {
throw new ServiceException("当前不支持该类型评论(评论类型[%s])", zoneJournals.getJournalizedType());
}
}
/**
* 校验附件列表
*
* @param attachmentIdentifiers 附件标识列表
*/
private void checkAttachments(String attachmentIdentifiers) {
if (StringUtils.isEmpty(attachmentIdentifiers)) {
return;
}
try {
List<SysFileInfo> fileInfos = FeignUtils.getReturnData(
remoteFileService.getFileListByIdentifier(attachmentIdentifiers)
);
if (CollectionUtils.isEmpty(fileInfos)) {
throw new ServiceException("附件不存在或已被删除");
}
} catch (Exception e) {
throw new ServiceException("附件校验失败:%s", e.getMessage());
}
}
/**
* 转换为父评论VO
*
* @param zoneJournals 评论实体
* @return 父评论VO
*/
private ZoneJournalsParentDataVo toParentDataVo(ZoneJournals zoneJournals) {
ZoneJournalsParentDataVo parentVo = new ZoneJournalsParentDataVo();
parentVo.setId(zoneJournals.getId());
parentVo.setNotes(zoneJournals.getNotes());
parentVo.setCreated_on(zoneJournals.getCreatedOn());
parentVo.setUpdated_on(zoneJournals.getUpdatedOn());
// 获取用户信息
SimpleSysUser user = zoneCommonService.getSimpleSysUserByUserId(zoneJournals.getUserId());
parentVo.setUser(user);
// 获取附件列表
parentVo.setAttachments(getAttachments(zoneJournals.getAttachmentIdentifiers()));
// 获取子评论列表
List<ZoneJournals> childrenJournalsList = zoneJournalsMapper.selectChildrenZoneJournalsList(zoneJournals.getId());
List<ZoneJournalsChildrenDataVo> childrenVoList = childrenJournalsList.stream()
.map(this::toChildrenDataVo)
.collect(Collectors.toList());
parentVo.setChildren_journals(childrenVoList);
parentVo.setComments_count(childrenVoList.size());
return parentVo;
}
/**
* 转换为子评论VO
*
* @param zoneJournals 评论实体
* @return 子评论VO
*/
private ZoneJournalsChildrenDataVo toChildrenDataVo(ZoneJournals zoneJournals) {
ZoneJournalsChildrenDataVo childrenVo = new ZoneJournalsChildrenDataVo();
childrenVo.setId(zoneJournals.getId());
childrenVo.setNotes(zoneJournals.getNotes());
childrenVo.setCreated_on(zoneJournals.getCreatedOn());
childrenVo.setUpdated_on(zoneJournals.getUpdatedOn());
// 获取用户信息
SimpleSysUser user = zoneCommonService.getSimpleSysUserByUserId(zoneJournals.getUserId());
childrenVo.setUser(user);
// 获取附件列表
childrenVo.setAttachments(getAttachments(zoneJournals.getAttachmentIdentifiers()));
// 获取回复用户信息
if (zoneJournals.getReplyId() != null) {
ZoneJournals replyJournal = zoneJournalsMapper.selectZoneJournalsById(zoneJournals.getReplyId());
if (replyJournal != null) {
SimpleSysUser replyUser = zoneCommonService.getSimpleSysUserByUserId(replyJournal.getUserId());
childrenVo.setReply_user(replyUser);
}
}
return childrenVo;
}
/**
* 获取附件列表
*
* @param attachmentIdentifiers 附件标识列表
* @return 附件列表
*/
private List<SimpleGitlinkFileInfo> getAttachments(String attachmentIdentifiers) {
if (StringUtils.isEmpty(attachmentIdentifiers)) {
return new ArrayList<>();
}
try {
List<SysFileInfo> fileInfos = FeignUtils.getReturnData(
remoteFileService.getFileListByIdentifier(attachmentIdentifiers)
);
return CollectionUtils.isEmpty(fileInfos) ? new ArrayList<>() : fileInfos.stream().map(SysFileInfo::toSimpleGitlinkFileInfo).collect(Collectors.toList());
} catch (Exception e) {
return new ArrayList<>();
}
}
}

View File

@ -1,5 +1,6 @@
package com.microservices.zone.member.domain.vo;
import com.microservices.system.api.domain.SysUser;
import com.microservices.zone.member.domain.ZoneMember;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

View File

@ -8,6 +8,7 @@ import com.microservices.common.core.web.page.GenericsTableDataInfo;
import com.microservices.common.log.annotation.Log;
import com.microservices.common.log.enums.BusinessType;
import com.microservices.common.security.annotation.RequiresPermissions;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.zone.project.domain.vo.*;
import com.microservices.zone.project.service.IZoneProjectService;
import io.swagger.annotations.Api;
@ -39,6 +40,9 @@ public class ZoneProjectController extends BaseController {
@GetMapping("/list")
@ApiOperation("查询项目聚合列表")
public GenericsTableDataInfo<ZoneProjectDataVo> list(ZoneProjectSearchVo zoneProjectSearchVo) {
if (Boolean.TRUE.equals(zoneProjectSearchVo.getIsMine())) {
zoneProjectSearchVo.setCreateBy(SecurityUtils.getUsername());
}
return zoneProjectService.selectZoneProjectList(zoneProjectSearchVo, false);
}

View File

@ -0,0 +1,22 @@
package com.microservices.zone.project.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 用户项目统计信息VO
*/
@Data
@ApiModel("项目统计信息")
public class UserProjectStatisticsVo {
@ApiModelProperty(value = "点赞总数")
private Integer likeCount;
@ApiModelProperty(value = "已发布总数")
private Integer publishedCount;
@ApiModelProperty(value = "待审核总数")
private Integer pendingCount;
}

View File

@ -20,4 +20,9 @@ public class ZoneProjectSearchVo extends ZoneProjectOpenSearchVo {
private String auditStatus;
@ApiModelProperty(value = "排除未通过审核项目", hidden = true)
private Boolean excludesFailure;
@ApiModelProperty(value = "是否为我的项目", hidden = true)
private Boolean isMyProject;
@ApiModelProperty(value = "是否只查询我的")
private Boolean isMine;
}

View File

@ -122,6 +122,15 @@ public interface IZoneProjectService {
*/
GenericsTableDataInfo<ZoneProjectDataVo> selectMyZoneProjectList(ZoneProjectSearchVo zoneProjectSearchVo);
/**
* 查询用户项目统计信息
*
* @param zoneId
* @param username 创建者用户名
* @return 统计信息
*/
UserProjectStatisticsVo getUserProjectStatistics(Long zoneId, String username);
/**
* 批量删除我的项目校验创建者
*

View File

@ -698,6 +698,70 @@ public class ZoneProjectServiceImpl implements IZoneProjectService {
return PageUtils.toListPage(zoneProjectList, this::toZoneProjectDataVoList);
}
@Override
public UserProjectStatisticsVo getUserProjectStatistics(Long zoneId, String username) {
UserProjectStatisticsVo statistics = new UserProjectStatisticsVo();
// 1. 尝试从缓存获取点赞总数
Integer cachedLikeCount = redisService.getCacheObject(CacheConstants.getProjectStatisticsLikeCount(zoneId, username));
if (cachedLikeCount != null) {
statistics.setLikeCount(cachedLikeCount);
} else {
// 缓存未命中查询并计算点赞总数
ZoneProjectSearchVo query = new ZoneProjectSearchVo();
query.setCreateBy(username);
query.setZoneId(zoneId);
List<ZoneProject> allProjects = zoneProjectMapper.selectZoneProjectList(query);
List<String> gitLinkProjectIdList = allProjects.stream()
.map(x -> String.valueOf(x.getGitlinkProjectId()))
.filter(Objects::nonNull)
.collect(Collectors.toList());
int totalLikeCount = 0;
if (!gitLinkProjectIdList.isEmpty()) {
try {
List<JSONObject> gitLinkProjectList = zoneAsyncService.getGitlinkProjectsDetail(gitLinkProjectIdList);
Map<Long, JSONObject> gitLinkProjectMap = gitLinkProjectList.stream()
.collect(Collectors.toMap(x -> x.getLong("id"), x -> x, (a, b) -> a));
for (ZoneProject project : allProjects) {
if (project.getGitlinkProjectId() != null) {
JSONObject gitLinkProject = gitLinkProjectMap.get(project.getGitlinkProjectId());
if (gitLinkProject != null) {
Integer praisesCount = gitLinkProject.getInteger("praises_count");
if (praisesCount != null) {
totalLikeCount += praisesCount;
}
}
}
}
} catch (Exception e) {
logger.error("批量获取用户{}的GitLink项目点赞数失败: {}", username, e.getMessage());
}
}
statistics.setLikeCount(totalLikeCount);
redisService.setCacheObject(CacheConstants.getProjectStatisticsLikeCount(zoneId, username), totalLikeCount, 1L, java.util.concurrent.TimeUnit.HOURS);
}
// 2. 统计已发布数量
ZoneProjectSearchVo publishedQuery = new ZoneProjectSearchVo();
publishedQuery.setCreateBy(username);
publishedQuery.setZoneId(zoneId);
publishedQuery.setAuditStatus(ZoneConstants.PROJECT_AUDIT_PASS);
Integer publishedCount = zoneProjectMapper.selectZoneProjectListCount(publishedQuery);
statistics.setPublishedCount(publishedCount);
// 3. 统计待审核数量
ZoneProjectSearchVo pendingQuery = new ZoneProjectSearchVo();
pendingQuery.setCreateBy(username);
pendingQuery.setZoneId(zoneId);
pendingQuery.setAuditStatus(ZoneConstants.PROJECT_NOT_AUDIT);
Integer pendingCount = zoneProjectMapper.selectZoneProjectListCount(pendingQuery);
statistics.setPendingCount(pendingCount);
return statistics;
}
@Override
public int deleteMyZoneProjectByIds(Long[] ids) {
for (Long id : ids) {

View File

@ -0,0 +1,115 @@
package com.microservices.zone.resource.controller;
import com.microservices.common.core.web.controller.BaseController;
import com.microservices.common.core.web.domain.AjaxResult;
import com.microservices.common.core.web.domain.GenericsAjaxResult;
import com.microservices.common.core.web.page.GenericsTableDataInfo;
import com.microservices.common.log.annotation.Log;
import com.microservices.common.log.enums.BusinessType;
import com.microservices.common.security.annotation.RequiresPermissions;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.service.IAiResourceParseService;
import com.microservices.zone.resource.service.IZoneResourceService;
import io.swagger.annotations.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* AI资源管理Controller
*/
@RestController
@RequestMapping("/ai-resource")
@Api(tags = "AI资源管理接口")
public class AiResourceController extends BaseController {
@Autowired
private IZoneResourceService zoneResourceService;
@Autowired
private IAiResourceParseService aiResourceParseService;
/**
* 上传ZIP并解析
*/
@RequiresPermissions("resource:ai:skill:add")
@PostMapping("/skill/parse")
@ApiOperation("上传ZIP并解析Skill包")
@ApiImplicitParams({
@ApiImplicitParam(name = "file", value = "文件实体", paramType = "form", dataType = "_file"),
})
public GenericsAjaxResult<AiSkillParseResultVo> parseSkillZip(@RequestPart("file") MultipartFile file) {
return genericsSuccess(aiResourceParseService.parseSkillZip(file));
}
/**
* 创建Skill资源
*/
@RequiresPermissions("resource:ai:skill:add")
@Log(title = "创建Skill资源", businessType = BusinessType.INSERT)
@PostMapping("/skill")
@ApiOperation("创建Skill资源")
public AjaxResult addSkill(@RequestBody @Validated AiSkillInputVo inputVo) {
return success(zoneResourceService.insertAiSkillResource(inputVo));
}
/**
* 修改Skill资源
*/
@RequiresPermissions("resource:ai:skill:edit")
@Log(title = "修改Skill资源", businessType = BusinessType.UPDATE)
@PutMapping("/skill")
@ApiOperation("修改Skill资源")
public AjaxResult editSkill(@RequestBody @Validated AiSkillUpdateVo updateVo) {
return success(zoneResourceService.updateAiSkillResource(updateVo));
}
// ==================== Model 资源接口 ====================
/**
* 创建Model资源
*/
@RequiresPermissions("resource:ai:model:add")
@Log(title = "创建Model资源", businessType = BusinessType.INSERT)
@PostMapping("/model")
@ApiOperation("创建Model资源")
public AjaxResult addModel(@RequestBody @Validated AiModelInputVo inputVo) {
return success(zoneResourceService.insertAiModelResource(inputVo));
}
/**
* 修改Model资源
*/
@RequiresPermissions("resource:ai:model:edit")
@Log(title = "修改Model资源", businessType = BusinessType.UPDATE)
@PutMapping("/model")
@ApiOperation("修改Model资源")
public AjaxResult editModel(@RequestBody @Validated AiModelUpdateVo updateVo) {
return success(zoneResourceService.updateAiModelResource(updateVo));
}
// ==================== Dataset 资源接口 ====================
/**
* 创建Dataset资源
*/
@RequiresPermissions("resource:ai:dataset:add")
@Log(title = "创建Dataset资源", businessType = BusinessType.INSERT)
@PostMapping("/dataset")
@ApiOperation("创建Dataset资源")
public AjaxResult addDataset(@RequestBody @Validated AiDatasetInputVo inputVo) {
return success(zoneResourceService.insertAiDatasetResource(inputVo));
}
/**
* 修改Dataset资源
*/
@RequiresPermissions("resource:ai:dataset:edit")
@Log(title = "修改Dataset资源", businessType = BusinessType.UPDATE)
@PutMapping("/dataset")
@ApiOperation("修改Dataset资源")
public AjaxResult editDataset(@RequestBody @Validated AiDatasetUpdateVo updateVo) {
return success(zoneResourceService.updateAiDatasetResource(updateVo));
}
}

View File

@ -12,6 +12,7 @@ import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.service.IZoneResourceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -106,4 +107,17 @@ public class ZoneResourceController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(zoneResourceService.deleteZoneResourceByIds(ids));
}
/**
* 下架资源
*/
// @RequiresPermissions("ai:resource:audit")
@Log(title = "下架资源", businessType = BusinessType.UPDATE)
@PutMapping("/{id}/takeDown")
@ApiOperation("下架资源")
public AjaxResult takeDownResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id,
@RequestParam(value = "reason", required = false) String reason) {
return toAjax(zoneResourceService.takeDownAiResource(id, reason));
}
}

View File

@ -0,0 +1,34 @@
package com.microservices.zone.resource.domain;
import lombok.Data;
import java.util.Date;
/**
* AI资源评论实体
*/
@Data
public class AiResourceComment {
private Long id;
private Long resourceId;
private Long userId;
private String content;
private Long parentId;
private Long replyId;
private String attachmentIdentifiers;
private Integer likeCount;
private String delFlag;
private Date createTime;
private Date updateTime;
}

View File

@ -1,9 +1,11 @@
package com.microservices.zone.resource.domain;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.annotation.Excel;
import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.detail.domain.ZoneSort;
import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -64,7 +66,7 @@ public class ZoneResource extends ZoneSort {
@ApiModelProperty(value = "资源简介")
private String summary;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过")
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架 4草稿")
private String auditStatus;
/**
* 是否首页展示0代表不展示 1代表展示
@ -93,8 +95,23 @@ public class ZoneResource extends ZoneSort {
@ApiModelProperty(value = "资源标签")
private String keywords;
@ApiModelProperty(value = "拓展数据")
private String extendData;
@ApiModelProperty(value = "拓展数据JSON格式")
private JSONObject extendData;
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASETNULL代表原资源")
private String resourceCategory;
/**
* 浏览次数
*/
@ApiModelProperty(value = "浏览次数")
private Integer viewCount;
/**
* 点赞次数
*/
@ApiModelProperty(value = "点赞次数")
private Integer likeCount;
/**
* 已获得积分通过资源下载/浏览等途径获得
@ -111,4 +128,34 @@ public class ZoneResource extends ZoneSort {
return target;
}
// ==================== extendData 便捷方法 ====================
/**
* 获取 SKILL.md 内容 extendData 中提取
*/
public String getSkillContent() {
return ResourceExtendDataUtils.extractSkillContent(this.extendData);
}
/**
* 获取目录树 JSON extendData 中提取
*/
public JSONObject getSkillFileTree() {
return ResourceExtendDataUtils.extractSkillFileTree(this.extendData);
}
/**
* 设置 SKILL.md 内容更新 extendData
*/
public void setSkillContent(String skillContent) {
this.extendData = ResourceExtendDataUtils.updateSkillContent(this.extendData, skillContent);
}
/**
* 设置目录树 JSON更新 extendData
*/
public void setSkillFileTree(JSONObject fileTreeJson) {
this.extendData = ResourceExtendDataUtils.updateSkillFileTree(this.extendData, fileTreeJson);
}
}

View File

@ -2,6 +2,7 @@ package com.microservices.zone.resource.domain;
import com.microservices.common.core.annotation.Excel;
import com.microservices.zone.detail.domain.ZoneSort;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
@ -23,4 +24,7 @@ public class ZoneResourceDomain extends ZoneSort {
*/
@Excel(name = "资源领域简介")
private String introduction;
@ApiModelProperty(value = "领域分类SKILL/MODEL/DATASETNULL代表原资源领域")
private String domainCategory;
}

View File

@ -0,0 +1,20 @@
package com.microservices.zone.resource.domain;
import lombok.Data;
import java.util.Date;
/**
* 资源点赞实体
*/
@Data
public class ZoneResourcesLike {
private Long id;
private Long resourceId;
private Long userId;
private Date createTime;
}

View File

@ -0,0 +1,56 @@
package com.microservices.zone.resource.domain.vo;
import com.microservices.zone.resource.enums.AiResourceCategory;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 数据集提报输入VO
*/
@Data
@ApiModel("数据集提报输入")
public class AiDatasetInputVo {
@ApiModelProperty(value = "数据集名称", required = true)
@NotBlank(message = "数据集名称不能为空")
@Size(max = 100, message = "数据集名称不能超过100个字符")
private String name;
@ApiModelProperty(value = "资源领域Id", required = true)
@NotNull(message = "资源领域Id不能为空")
private Long domainId;
@ApiModelProperty(value = "数据集简介")
@Size(max = 500, message = "数据集简介不能超过500个字符")
private String summary;
@ApiModelProperty(value = "资源标签,逗号分隔")
private String keywords;
@ApiModelProperty(value = "数据集文件Id", required = true)
@NotNull(message = "数据集文件Id不能为空")
private Long fileId;
@ApiModelProperty(value = "所属专区", required = true)
@NotNull(message = "所属专区不能为空")
private Long zoneId;
public ZoneResourceInputVo toZoneResourceInputVo() {
// 构建 ZoneResourceInputVo
ZoneResourceInputVo zoneResourceInputVo = new ZoneResourceInputVo();
zoneResourceInputVo.setName(this.getName());
zoneResourceInputVo.setSummary(this.getSummary());
zoneResourceInputVo.setKeywords(this.getKeywords());
zoneResourceInputVo.setDomainId(this.getDomainId());
zoneResourceInputVo.setZoneId(this.getZoneId());
zoneResourceInputVo.setFileIds(String.valueOf(this.getFileId()));
zoneResourceInputVo.setResourceCategory(AiResourceCategory.DATASET.getCode());
return zoneResourceInputVo;
}
}

View File

@ -0,0 +1,63 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 数据集修改输入VO
*/
@Data
@ApiModel("数据集修改输入")
public class AiDatasetUpdateVo {
@ApiModelProperty(value = "资源Id", required = true)
@NotNull(message = "资源Id不能为空")
private Long id;
@ApiModelProperty(value = "数据集名称")
@Size(max = 100, message = "数据集名称不能超过100个字符")
private String name;
@ApiModelProperty(value = "数据集简介")
@Size(max = 500, message = "数据集简介不能超过500个字符")
private String summary;
@ApiModelProperty(value = "资源标签,逗号分隔")
private String keywords;
@ApiModelProperty(value = "数据集文件Id")
private Long fileId;
@ApiModelProperty(value = "资源领域Id")
private Long domainId;
@ApiModelProperty(value = "README.md内容")
private String readmeContent;
public ZoneResourceUpdateVo toZoneResourceUpdateVo(ZoneResource existing){
ZoneResourceUpdateVo zoneResourceUpdateVo = new ZoneResourceUpdateVo();
zoneResourceUpdateVo.setId(this.getId());
zoneResourceUpdateVo.setName(this.getName());
zoneResourceUpdateVo.setSummary(this.getSummary());
zoneResourceUpdateVo.setKeywords(this.getKeywords());
zoneResourceUpdateVo.setDomainId(this.getDomainId());
if (readmeContent != null) {
JSONObject extendData = existing.getExtendData();
if (extendData == null) {
extendData = new JSONObject();
}
extendData = ResourceExtendDataUtils.updateModelOrDatasetReadmeContent(existing.getResourceCategory(), extendData, readmeContent);
zoneResourceUpdateVo.setExtendData(extendData.toJSONString());
}
return zoneResourceUpdateVo;
}
}

View File

@ -0,0 +1,55 @@
package com.microservices.zone.resource.domain.vo;
import com.microservices.zone.resource.enums.AiResourceCategory;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 模型提报输入VO
*/
@Data
@ApiModel("模型提报输入")
public class AiModelInputVo {
@ApiModelProperty(value = "模型名称", required = true)
@NotBlank(message = "模型名称不能为空")
@Size(max = 100, message = "模型名称不能超过100个字符")
private String name;
@ApiModelProperty(value = "资源领域Id", required = true)
@NotNull(message = "资源领域Id不能为空")
private Long domainId;
@ApiModelProperty(value = "模型简介")
@Size(max = 500, message = "模型简介不能超过500个字符")
private String summary;
@ApiModelProperty(value = "资源标签,逗号分隔")
private String keywords;
@ApiModelProperty(value = "模型文件Id", required = true)
@NotNull(message = "模型文件Id不能为空")
private Long fileId;
@ApiModelProperty(value = "所属专区", required = true)
@NotNull(message = "所属专区不能为空")
private Long zoneId;
public ZoneResourceInputVo toZoneResourceInputVo() {
// 构建 ZoneResourceInputVo
ZoneResourceInputVo zoneResourceInputVo = new ZoneResourceInputVo();
zoneResourceInputVo.setName(this.getName());
zoneResourceInputVo.setSummary(this.getSummary());
zoneResourceInputVo.setKeywords(this.getKeywords());
zoneResourceInputVo.setDomainId(this.getDomainId());
zoneResourceInputVo.setZoneId(this.getZoneId());
zoneResourceInputVo.setFileIds(String.valueOf(this.getFileId()));
zoneResourceInputVo.setResourceCategory(AiResourceCategory.MODEL.getCode());
return zoneResourceInputVo;
}
}

View File

@ -0,0 +1,63 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 模型修改输入VO
*/
@Data
@ApiModel("模型修改输入")
public class AiModelUpdateVo {
@ApiModelProperty(value = "资源Id", required = true)
@NotNull(message = "资源Id不能为空")
private Long id;
@ApiModelProperty(value = "模型名称")
@Size(max = 100, message = "模型名称不能超过100个字符")
private String name;
@ApiModelProperty(value = "模型简介")
@Size(max = 500, message = "模型简介不能超过500个字符")
private String summary;
@ApiModelProperty(value = "资源标签,逗号分隔")
private String keywords;
@ApiModelProperty(value = "模型文件Id")
private Long fileId;
@ApiModelProperty(value = "资源领域Id")
private Long domainId;
@ApiModelProperty(value = "README.md内容")
private String readmeContent;
public ZoneResourceUpdateVo toZoneResourceUpdateVo(ZoneResource existing){
ZoneResourceUpdateVo zoneResourceUpdateVo = new ZoneResourceUpdateVo();
zoneResourceUpdateVo.setId(this.getId());
zoneResourceUpdateVo.setName(this.getName());
zoneResourceUpdateVo.setSummary(this.getSummary());
zoneResourceUpdateVo.setKeywords(this.getKeywords());
zoneResourceUpdateVo.setDomainId(this.getDomainId());
if (readmeContent != null) {
JSONObject extendData = existing.getExtendData();
if (extendData == null) {
extendData = new JSONObject();
}
extendData = ResourceExtendDataUtils.updateModelOrDatasetReadmeContent(existing.getResourceCategory(), extendData, readmeContent);
zoneResourceUpdateVo.setExtendData(extendData.toJSONString());
}
return zoneResourceUpdateVo;
}
}

View File

@ -0,0 +1,66 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.resource.enums.AiResourceCategory;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* Skill提报输入VO
*/
@Data
@ApiModel("Skill提报输入")
public class AiSkillInputVo {
@ApiModelProperty(value = "Skill名称", required = true)
@NotBlank(message = "Skill名称不能为空")
@Size(max = 100, message = "Skill名称不能超过100个字符")
private String name;
@ApiModelProperty(value = "资源领域Id", required = true)
@NotNull
private Long domainId;
@ApiModelProperty(value = "Skill简介")
// @Size(max = 500, message = "Skill简介不能超过500个字符")
private String summary;
@ApiModelProperty(value = "资源标签,逗号分隔")
private String keywords;
@ApiModelProperty(value = "Skill文件Id")
private Long fileId;
@ApiModelProperty(value = "SKILL.md内容", required = true)
@NotBlank(message = "SKILL.md内容不能为空")
private String skillContent;
@ApiModelProperty(value = "目录树JSON")
private String fileTreeJson;
@ApiModelProperty(value = "所属专区", required = true)
@NotNull
private Long zoneId;
public ZoneResourceInputVo toZoneResourceInputVo() {
ZoneResourceInputVo target = new ZoneResourceInputVo();
BeanUtils.copyProperties(this, target, "skillContent", "fileTreeJson");
target.setFileIds(String.valueOf(fileId));
// Skill 特有字段存入 extendDataJSONObject
JSONObject fileTreeJsonObj = StringUtils.isEmpty(fileTreeJson)
? null
: JSONObject.parseObject(fileTreeJson);
JSONObject extendData = ResourceExtendDataUtils.buildSkillExtendData(
skillContent, fileTreeJsonObj);
target.setExtendData(extendData.toJSONString());
target.setResourceCategory(AiResourceCategory.SKILL.getCode());
return target;
}
}

View File

@ -0,0 +1,29 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* Skill ZIP解析结果VO
*/
@Data
@ApiModel("Skill ZIP解析结果")
public class AiSkillParseResultVo {
@ApiModelProperty(value = "SKILL.md文件内容")
private String skillMdContent;
@ApiModelProperty(value = "ZIP包目录树结构JSON")
private String fileTreeJson;
@ApiModelProperty(value = "上传后的文件id")
private Long fileId;
@ApiModelProperty(value = "Skill描述从YAML front matter中提取")
private String summary;
@ApiModelProperty(value = "Skill名称从YAML front matter中提取")
private String name;
}

View File

@ -0,0 +1,80 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* Skill修改输入VO
*/
@Data
@ApiModel("Skill修改输入")
public class AiSkillUpdateVo {
@ApiModelProperty(value = "资源Id", required = true)
@NotNull(message = "资源Id不能为空")
private Long id;
@ApiModelProperty(value = "Skill名称")
@Size(max = 100, message = "Skill名称不能超过100个字符")
private String name;
@ApiModelProperty(value = "Skill简介")
// @Size(max = 500, message = "Skill简介不能超过500个字符")
private String summary;
@ApiModelProperty(value = "Skill文件Id")
private Long fileId;
@ApiModelProperty(value = "SKILL.md内容")
private String skillContent;
@ApiModelProperty(value = "目录树JSON")
private String fileTreeJson;
@ApiModelProperty(value = "资源领域Id")
private Long domainId;
/**
* 转换为 ZoneResource合并 extendData
*
* @param existing 现有资源对象用于获取原有 extendData
* @return 合并后的 ZoneResource
*/
public ZoneResourceUpdateVo toZoneResourceUpdateVo(ZoneResource existing) {
ZoneResourceUpdateVo target = new ZoneResourceUpdateVo();
BeanUtils.copyProperties(this, target);
if (fileId == null) {
target.setFileIds(existing.getFileIds());
} else {
target.setFileIds(String.valueOf(fileId));
}
// 获取现有 extendData 或创建新的
JSONObject extendData = existing.getExtendData();
if (extendData == null) {
extendData = new JSONObject();
}
// 更新 skillContent
if (skillContent != null) {
extendData = ResourceExtendDataUtils.updateSkillContent(extendData, skillContent);
}
// 更新 fileTree
if (fileTreeJson != null) {
JSONObject fileTreeObj = JSONObject.parseObject(fileTreeJson);
extendData = ResourceExtendDataUtils.updateSkillFileTree(extendData, fileTreeObj);
}
target.setExtendData(extendData.toJSONString());
return target;
}
}

View File

@ -0,0 +1,22 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
/**
* 已上传资源ZIP解析结果
*/
@Data
public class AiUploadedResourceParseResultVo {
/**
* README.md 文件内容不存在时为 null
*/
private String readmeContent;
/**
* 目录树 JSON 字符串
*/
private JSONObject fileTreeJson;
private String fileSizeInfo;
}

View File

@ -0,0 +1,22 @@
package com.microservices.zone.resource.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 用户资源统计信息VO
*/
@Data
@ApiModel("资源统计信息")
public class UserResourceStatisticsVo {
@ApiModelProperty(value = "点赞总数")
private Integer likeCount;
@ApiModelProperty(value = "已发布总数")
private Integer publishedCount;
@ApiModelProperty(value = "待审核总数")
private Integer pendingCount;
}

View File

@ -1,8 +1,11 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.system.api.domain.SimpleSysUser;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@ -64,7 +67,7 @@ public class ZoneResourceDataVo {
@ApiModelProperty(value = "资源简介")
private String summary;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过")
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架 4草稿")
private String auditStatus;
/**
* 审核时间
@ -73,8 +76,23 @@ public class ZoneResourceDataVo {
private Date auditTime;
@ApiModelProperty(value = "审核人")
private String auditBy;
@ApiModelProperty(value = "拓展数据")
private String extendData;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "拓展数据JSON格式")
private JSONObject extendData;
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASETNULL代表原资源")
private String resourceCategory;
@ApiModelProperty(value = "审核意见/驳回原因")
private String auditReason;
@ApiModelProperty(value = "下载记录列表所有附件的下载明细含用户名、IP、时间")
private List<SysFileDownloadLogVo> downloadUserSummary;
@ApiModelProperty(value = "浏览次数")
private Integer viewCount;
@ApiModelProperty(value = "点赞次数")
private Integer likeCount;
}

View File

@ -23,6 +23,8 @@ public class ZoneResourceDomainInputVo {
@ApiModelProperty(value = "所属专区", required = true)
@NotNull
private Long zoneId;
@ApiModelProperty(value = "领域分类SKILL/MODEL/DATASETNULL代表原资源")
private String domainCategory;
public ZoneResourceDomain toZoneResourceDomain() {
ZoneResourceDomain target = new ZoneResourceDomain();

View File

@ -5,6 +5,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* @author otto
*/
@ -13,13 +15,17 @@ import lombok.Data;
public class ZoneResourceDomainSearchVo {
@ApiModelProperty(value = "资源领域名称")
private String name;
@ApiModelProperty(value = "所属专区")
@ApiModelProperty(value = "所属专区", required = true)
@NotNull
private Long zoneId;
@ApiModelProperty(value = "领域分类SKILL/MODEL/DATASETNULL或者不传代表所有资源领域")
private String domainCategory;
public ZoneResourceDomain toZoneResourceDomain() {
ZoneResourceDomain zoneResourceDomain = new ZoneResourceDomain();
zoneResourceDomain.setZoneId(zoneId);
zoneResourceDomain.setName(name);
zoneResourceDomain.setDomainCategory(domainCategory);
return zoneResourceDomain;
}
}

View File

@ -7,6 +7,8 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
/**
* @author otto
*/
@ -17,6 +19,9 @@ public class ZoneResourceFileVo extends SysFileInfo {
@ApiModelProperty(value = "资源类型列表")
private ZoneResourceType zoneResourceType;
@ApiModelProperty(value = "下载用户列表")
private List<String> downloadUserList;
public ZoneResourceFileVo(ZoneResourceType zoneResourceType, SysFileInfo sysFileInfo) {
this.zoneResourceType = zoneResourceType;
BeanUtils.copyProperties(sysFileInfo, this);

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.resource.domain.ZoneResource;
import io.swagger.annotations.ApiModel;
@ -19,7 +20,7 @@ public class ZoneResourceInputVo {
@NotNull
private String name;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过", hidden = true)
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架 4草稿", hidden = true)
private String auditStatus = "1";
@ApiModelProperty(value = "资源简介")
private String summary;
@ -34,12 +35,16 @@ public class ZoneResourceInputVo {
@NotNull
private Long zoneId;
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASETNULL代表原资源", hidden = true)
private String resourceCategory = null;
@ApiModelProperty(value = "拓展数据")
private String extendData;
public ZoneResource toZoneResource() {
ZoneResource target = new ZoneResource();
BeanUtils.copyProperties(this, target);
target.setExtendData(JSONObject.parseObject(extendData));
return target;
}
}

View File

@ -53,8 +53,11 @@ public class ZoneResourceOpenSearchVo extends BaseEntity {
@ApiModelProperty(value = "是否根据下载次数降序排序与orderByColumn互斥该属性优先级更高")
private Boolean orderByDownloadCount = false;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过")
private String auditStatus;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架 4草稿",hidden = true)
private String auditStatus="1";
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASETNULL或者不传代表所有资源")
private String resourceCategory;
public ZoneResource toZoneResource() {
ZoneResource target = new ZoneResource();

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.domain.vo;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.zone.resource.domain.ZoneResource;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
@ -18,12 +19,21 @@ public class ZoneResourceSearchVo extends ZoneResourceOpenSearchVo {
@NotNull
private Long zoneId;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过")
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架 4草稿 all所有")
private String auditStatus;
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASETNULL或者不传代表所有资源")
private String resourceCategory;
@ApiModelProperty(value = "是否只查询我的")
private Boolean isMine;
public ZoneResource toZoneResource() {
ZoneResource zoneResource = super.toZoneResource();
zoneResource.setZoneId(zoneId);
if (Boolean.TRUE.equals(isMine)) {
zoneResource.setCreateBy(SecurityUtils.getUsername());
}
return zoneResource;
}
}

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.resource.domain.ZoneResource;
import io.swagger.annotations.ApiModel;
@ -34,7 +35,7 @@ public class ZoneResourceUpdateVo {
@ApiModelProperty(value = "是否首页展示0代表不展示 1代表展示")
private String isHomepage;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过", hidden = true)
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架 4草稿", hidden = true)
private String auditStatus = "1";
@ApiModelProperty(value = "备注", hidden = true)
private String remark;
@ -44,6 +45,7 @@ public class ZoneResourceUpdateVo {
public ZoneResource toZoneResource() {
ZoneResource target = new ZoneResource();
BeanUtils.copyProperties(this, target);
target.setExtendData(JSONObject.parseObject(extendData));
return target;
}
}

View File

@ -0,0 +1,31 @@
package com.microservices.zone.resource.enums;
/**
* AI资源类型枚举
*/
public enum AiResourceCategory {
SKILL("SKILL", "Skill技能"),
MODEL("MODEL", "模型"),
DATASET("DATASET", "数据集");
private final String code;
private final String desc;
AiResourceCategory(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
public static boolean isAiResourceCategory(String code) {
return SKILL.getCode().equals(code) || MODEL.getCode().equals(code) || DATASET.getCode().equals(code);
}
}

View File

@ -0,0 +1,55 @@
package com.microservices.zone.resource.handler;
import com.alibaba.fastjson2.JSONObject;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* JSONObject String 之间的类型转换处理器
* 用于 extendData 字段代码中操作 JSONObject数据库存储 TEXT
*/
@MappedTypes(JSONObject.class)
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonObjectTypeHandler extends BaseTypeHandler<JSONObject> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, JSONObject parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.toJSONString());
}
@Override
public JSONObject getNullableResult(ResultSet rs, String columnName) throws SQLException {
String value = rs.getString(columnName);
return parseJSONObject(value);
}
@Override
public JSONObject getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String value = rs.getString(columnIndex);
return parseJSONObject(value);
}
@Override
public JSONObject getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String value = cs.getString(columnIndex);
return parseJSONObject(value);
}
private JSONObject parseJSONObject(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return JSONObject.parseObject(value);
} catch (Exception e) {
return null;
}
}
}

View File

@ -67,9 +67,10 @@ public interface ZoneResourceDomainMapper {
*
* @param name
* @param zoneId
* @param domainCategory
* @return
*/
ZoneResourceDomain selectZoneResourceDomainByNameAndZoneId(@Param("name") String name, @Param("zoneId") Long zoneId);
ZoneResourceDomain selectZoneResourceDomainByNameAndZoneId(@Param("name") String name, @Param("zoneId") Long zoneId, @Param("domainCategory") String domainCategory);
/**
* 根据专区Id删除资源领域

View File

@ -70,9 +70,10 @@ public interface ZoneResourceMapper {
*
* @param name
* @param zoneId
* @param resourceCategory
* @return
*/
ZoneResource selectZoneResourceByNameAndZoneId(@Param("name") String name, @Param("zoneId") Long zoneId);
ZoneResource selectZoneResourceByNameAndZoneId(@Param("name") String name, @Param("zoneId") Long zoneId, @Param("resourceCategory") String resourceCategory);
/**
* 获取资源数量
@ -120,6 +121,41 @@ public interface ZoneResourceMapper {
@Param("offset") int offset,
@Param("pageSize") int pageSize);
/**
* 通过名称查询Skill资源resource_category='SKILL'
*
* @param name Skill名称
* @return 资源
*/
ZoneResource selectSkillResourceByName(@Param("name") String name);
/**
* 查询已发布的Skill推荐列表
*
* @param resourceCategory 资源分类
* @param auditStatus 审核状态
* @param limit 数量限制
* @return 资源列表
*/
List<ZoneResource> selectSkillRecommendations(@Param("resourceCategory") String resourceCategory,
@Param("auditStatus") String auditStatus,
@Param("limit") int limit);
/**
* 增加浏览次数
*
* @param id 资源ID
*/
int incrementViewCount(@Param("id") Long id);
/**
* 增加点赞次数
*
* @param id 资源ID
* @param delta 变化量正数加负数减
*/
int updateLikeCount(@Param("id") Long id, @Param("delta") int delta);
/**
* 通过文件ID查询资源fileIds字段中包含该fileId的资源
*

View File

@ -0,0 +1,42 @@
package com.microservices.zone.resource.mapper;
import com.microservices.zone.resource.domain.ZoneResourcesLike;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 资源点赞Mapper接口
*/
@Mapper
public interface ZoneResourcesLikeMapper {
/**
* 查询用户对指定资源的点赞记录
*
* @param resourceId 资源ID
* @param userId 用户ID
* @return 点赞记录
*/
ZoneResourcesLike selectByResourceAndUser(@Param("resourceId") Long resourceId, @Param("userId") Long userId);
/**
* 新增点赞记录
*
* @param zoneResourcesLike 点赞记录
* @return 影响行数
*/
int insertLike(ZoneResourcesLike zoneResourcesLike);
/**
* 删除点赞记录
*
* @param resourceId 资源ID
* @param userId 用户ID
* @return 影响行数
*/
int deleteLike(@Param("resourceId") Long resourceId, @Param("userId") Long userId);
List<ZoneResourcesLike> selectByResourceIds(String[] idArr);
}

View File

@ -0,0 +1,31 @@
package com.microservices.zone.resource.service;
import com.microservices.zone.resource.domain.vo.AiSkillParseResultVo;
import com.microservices.zone.resource.domain.vo.AiUploadedResourceParseResultVo;
import org.springframework.web.multipart.MultipartFile;
/**
* AI资源ZIP解析服务接口
*/
public interface IAiResourceParseService {
/**
* 解析Skill ZIP包
* 1. 校验ZIP内必须包含 SKILL.md
* 2. 提取 SKILL.md 内容
* 3. 生成目录树结构 JSON
*
* @param zipFile 上传的ZIP文件
* @return 解析结果
*/
AiSkillParseResultVo parseSkillZip(MultipartFile zipFile);
/**
* 解析已上传的资源ZIP包模型/数据集
* 基于已通过分片上传到服务器的文件解析目录树和 README.md
*
* @param fileId 已上传文件的ID
* @return 解析结果
*/
AiUploadedResourceParseResultVo parseUploadedZip(Long fileId);
}

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.service;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.web.page.GenericsTableDataInfo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.zone.resource.domain.ZoneResource;
@ -138,4 +139,167 @@ public interface IZoneResourceService {
* @return
*/
Integer selectMyZoneResourceCount(ZoneResource zoneResourceSearch);
/**
* 创建Skill资源AI资源广场
*
* @param inputVo Skill提报输入
* @return 资源数据
*/
ZoneResourceDataVo insertAiSkillResource(AiSkillInputVo inputVo);
/**
* 修改Skill资源AI资源广场
*
* @param updateVo Skill修改输入
* @return 资源数据
*/
ZoneResourceDataVo updateAiSkillResource(AiSkillUpdateVo updateVo);
/**
* 查询Skill资源详情AI资源广场
*
* @param id 资源ID
* @return 资源数据
*/
ZoneResourceDataVo selectAiSkillResourceById(Long id);
/**
* 查询Skill资源列表AI资源广场
*
* @param zoneResource 查询条件
* @return 分页数据
*/
GenericsTableDataInfo<ZoneResourceDataVo> selectAiSkillResourceList(ZoneResource zoneResource);
/**
* 查询AI模型资源列表
*
* @param zoneResource 查询条件
* @return 分页数据
*/
GenericsTableDataInfo<ZoneResourceDataVo> selectAiModelResourceList(ZoneResource zoneResource);
/**
* 查询AI数据集资源列表
*
* @param zoneResource 查询条件
* @return 分页数据
*/
GenericsTableDataInfo<ZoneResourceDataVo> selectAiDatasetResourceList(ZoneResource zoneResource);
/**
* 查询资源统计信息
*
* @param zoneId 专区IDisMine=false时使用
* @param isMine 创建者isMine=true时使用
* @param resourceCategory 资源分类SKILL/MODEL/DATASET/null表示所有
* @return 统计信息
*/
UserResourceStatisticsVo getResourceStatistics(Long zoneId, Boolean isMine, String resourceCategory);
/**
* 下架AI资源
*
* @param id 资源ID
* @param reason 下架原因
* @return 影响行数
*/
int takeDownAiResource(Long id, String reason);
/**
* 重新提交被驳回的AI资源
*
* @param id 资源ID
* @return 影响行数
*/
int resubmitAiResource(Long id);
/**
* 撤回审核中的AI资源
*
* @param id 资源ID
* @return 影响行数
*/
int withdrawAiResource(Long id);
/**
* 查询Skill推荐列表
*
* @param limit 数量限制
* @return 资源列表
*/
List<ZoneResourceDataVo> selectSkillRecommendations(int limit);
/**
* 资源点赞/取消点赞
*
* @param resourceId 资源ID
* @return true-已点赞, false-已取消
*/
boolean toggleZoneResourcesLike(Long resourceId);
/**
* 增加AI资源浏览次数
*
* @param id 资源ID
*/
void incrementViewCount(Long id);
int takeUpAiResource(Long id, String reason);
List<JSONObject> selectZoneResourcesIsLike(String ids);
// ==================== AI资源广场 Model 功能 ====================
/**
* 创建Model资源AI资源广场
*
* @param inputVo Model提报输入
* @return 资源数据
*/
ZoneResourceDataVo insertAiModelResource(AiModelInputVo inputVo);
/**
* 修改Model资源AI资源广场
*
* @param updateVo Model修改输入
* @return 资源数据
*/
ZoneResourceDataVo updateAiModelResource(AiModelUpdateVo updateVo);
/**
* 查询Model资源详情AI资源广场
*
* @param id 资源ID
* @return 资源数据
*/
ZoneResourceDataVo selectAiModelResourceById(Long id);
// ==================== AI资源广场 Dataset 功能 ====================
/**
* 创建Dataset资源AI资源广场
*
* @param inputVo Dataset提报输入
* @return 资源数据
*/
ZoneResourceDataVo insertAiDatasetResource(AiDatasetInputVo inputVo);
/**
* 修改Dataset资源AI资源广场
*
* @param updateVo Dataset修改输入
* @return 资源数据
*/
ZoneResourceDataVo updateAiDatasetResource(AiDatasetUpdateVo updateVo);
/**
* 查询Dataset资源详情AI资源广场
*
* @param id 资源ID
* @return 资源数据
*/
ZoneResourceDataVo selectAiDatasetResourceById(Long id);
}

View File

@ -0,0 +1,567 @@
package com.microservices.zone.resource.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.domain.SysFile;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.utils.FeignUtils;
import com.microservices.zone.resource.domain.vo.AiSkillParseResultVo;
import com.microservices.zone.resource.domain.vo.AiUploadedResourceParseResultVo;
import com.microservices.zone.resource.service.IAiResourceParseService;
import com.microservices.zone.utils.ZoneConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* Skill ZIP解析服务实现
*/
@Service
public class AiResourceParseServiceImpl implements IAiResourceParseService {
private static final Logger logger = LoggerFactory.getLogger(AiResourceParseServiceImpl.class);
private static final String SKILL_MD_FILE = "SKILL.md";
private static final String README_MD_FILE = "README.md";
private final RemoteFileService remoteFileService;
public AiResourceParseServiceImpl(RemoteFileService remoteFileService) {
this.remoteFileService = remoteFileService;
}
@Override
public AiSkillParseResultVo parseSkillZip(MultipartFile zipFile) {
// 1. 校验文件大小
if (zipFile.getSize() > ZoneConstants.SKILL_ZIP_MAX_SIZE) {
throw new ServiceException("ZIP包大小不能超过50MB");
}
// 2. 校验文件格式
String originalFilename = zipFile.getOriginalFilename();
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".zip")) {
throw new ServiceException("仅支持ZIP格式文件");
}
try (ZipArchiveInputStream zais = new ZipArchiveInputStream(zipFile.getInputStream(), "GBK", true)) {
ZipArchiveEntry entry;
List<String> skillMdPaths = new ArrayList<>();
List<FileTreeNode> fileTreeNodes = new ArrayList<>();
// 第一遍遍历收集所有一级和二级目录下的 SKILL.md 路径
while ((entry = zais.getNextEntry()) != null) {
String entryName = entry.getName();
if (entry.isDirectory()) {
continue;
}
// 防止路径遍历攻击
if (entryName.contains("..")) {
throw new ServiceException("ZIP包中包含非法路径");
}
// 收集一级或二级目录下的 SKILL.md
if (SKILL_MD_FILE.equalsIgnoreCase(new File(entryName).getName())) {
int slashCount = entryName.split("/").length - 1;
if (slashCount <= 1) { // 一级或二级
skillMdPaths.add(entryName);
}
}
// 收集文件信息构建目录树
fileTreeNodes.add(new FileTreeNode(entryName, entry.getSize()));
}
// 确定要读取的 SKILL.md 路径
String targetSkillMdPath;
if (skillMdPaths.isEmpty()) {
throw new ServiceException("ZIP包中必须包含 SKILL.md 文件");
} else {
// 优先选择根目录的 SKILL.md
targetSkillMdPath = null;
for (String path : skillMdPaths) {
if (path.equalsIgnoreCase("SKILL.md")) {
targetSkillMdPath = path;
break;
}
}
// 如果根目录不存在选择第一个二级目录的 SKILL.md按字母顺序
if (targetSkillMdPath == null) {
Collections.sort(skillMdPaths, String.CASE_INSENSITIVE_ORDER);
targetSkillMdPath = skillMdPaths.get(0);
}
}
// 第二遍遍历读取目标 SKILL.md 的内容
String skillMdContent;
try (ZipArchiveInputStream zais2 = new ZipArchiveInputStream(zipFile.getInputStream(), "GBK", true)) {
ZipArchiveEntry entry2;
skillMdContent = null;
while ((entry2 = zais2.getNextEntry()) != null) {
if (targetSkillMdPath.equals(entry2.getName())) {
skillMdContent = readEntryContent(zais2);
break;
}
}
}
// 3. 校验 SKILL.md 内容
if (skillMdContent == null) {
throw new ServiceException("ZIP包中必须包含 SKILL.md 文件");
}
// 4. 校验 SKILL.md 内容长度
if (skillMdContent.trim().length() < ZoneConstants.SKILL_MD_MIN_LENGTH) {
throw new ServiceException("SKILL.md 内容不得少于" + ZoneConstants.SKILL_MD_MIN_LENGTH + "");
}
// 5. 构建目录树 JSON
JSONObject fileTreeJson = buildFileTreeJson(fileTreeNodes);
AiSkillParseResultVo result = new AiSkillParseResultVo();
// YAML front matter 中提取 name description 字段
String name = extractName(skillMdContent);
result.setName(name);
String description = extractDescription(skillMdContent);
result.setSummary(description);
result.setSkillMdContent(skillMdContent);
result.setFileTreeJson(fileTreeJson.toJSONString());
// 解析成功后自动上传ZIP文件到File微服务上传失败则阻断整个接口
SysFile sysFile = FeignUtils.getReturnData(
remoteFileService.upload(zipFile, "ai-resource", "skills", SecurityConstants.INNER)
);
if (sysFile == null || StringUtils.isEmpty(sysFile.getFileIdentifier())) {
throw new ServiceException("文件上传返回为空,请重试");
}
result.setFileId(sysFile.getFileId());
return result;
} catch (IOException e) {
logger.error("ZIP包解析失败", e);
throw new ServiceException("ZIP包解析失败" + e.getMessage());
}
}
private String readEntryContent(ZipArchiveInputStream zais) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = zais.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
return new String(baos.toByteArray(), StandardCharsets.UTF_8);
}
@Override
public AiUploadedResourceParseResultVo parseUploadedZip(Long fileId) {
if (fileId == null) {
throw new ServiceException("文件ID不能为空");
}
// 1. 获取文件信息
SysFileInfo sysFileInfo = FeignUtils.getReturnData(remoteFileService.getFileDetail(fileId, SecurityConstants.INNER));
if (sysFileInfo == null) {
throw new ServiceException("文件不存在");
}
// 2. 校验文件格式
String fileSuffix = sysFileInfo.getFileSuffix();
if (!"zip".equalsIgnoreCase(fileSuffix)) {
throw new ServiceException("仅支持ZIP格式文件");
}
// 3. 构建绝对路径
String absolutePath = sysFileInfo.getFilePath();
File zipFile = new File(absolutePath);
if (!zipFile.exists()) {
throw new ServiceException("ZIP文件不存在" + sysFileInfo.getFilePath());
}
// 4. 使用 ZipFile 解析读取 Central Directory支持随机访问大文件秒级响应
try (ZipFile zf = new ZipFile(zipFile, "GBK", true)) {
String readmeContent = null;
List<FileTreeNode> fileTreeNodes = new ArrayList<>();
Enumeration<ZipArchiveEntry> entries = zf.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
String entryName = entry.getName();
if (entry.isDirectory()) {
continue;
}
if (entryName.contains("..")) {
throw new ServiceException("ZIP包中包含非法路径");
}
fileTreeNodes.add(new FileTreeNode(entryName, entry.getSize()));
}
// 5. 查找并读取 README.md
readmeContent = findAndReadReadmeMd(zf, fileTreeNodes);
// 6. 构建目录树 JSON
JSONObject fileTreeJson = buildFileTreeJsonFast(fileTreeNodes);
AiUploadedResourceParseResultVo result = new AiUploadedResourceParseResultVo();
result.setReadmeContent(readmeContent);
result.setFileTreeJson(fileTreeJson);
result.setFileSizeInfo(sysFileInfo.getFileSizeInfo());
return result;
} catch (Exception e) {
logger.error("ZIP包解析失败: fileId={}, path={}", fileId, absolutePath, e);
throw new ServiceException("ZIP包解析失败" + e.getMessage());
}
}
private String findAndReadReadmeMd(ZipFile zf, List<FileTreeNode> nodes) throws IOException {
// 优先查找根目录 README.md忽略大小写
Enumeration<ZipArchiveEntry> entries = zf.getEntries();
ZipArchiveEntry readmeEntry = null;
// 查找根目录下的 README.md
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
String fileName = new File(entry.getName()).getName();
if (README_MD_FILE.equalsIgnoreCase(fileName) && entry.getName().indexOf('/') == -1) {
readmeEntry = entry;
break;
}
}
if (readmeEntry != null && !readmeEntry.isDirectory()) {
return readZipEntryContent(zf, readmeEntry);
}
// 其次查找顶层唯一文件夹下的 README.md忽略大小写
String topFolder = findSingleTopFolder(nodes);
if (topFolder != null) {
entries = zf.getEntries(); // 重置枚举
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
String expectedPath = topFolder + "/" + README_MD_FILE;
if (expectedPath.equalsIgnoreCase(entry.getName())) {
readmeEntry = entry;
break;
}
}
if (readmeEntry != null && !readmeEntry.isDirectory()) {
return readZipEntryContent(zf, readmeEntry);
}
}
return null;
}
private String findSingleTopFolder(List<FileTreeNode> nodes) {
Set<String> topFolders = new HashSet<>();
for (FileTreeNode node : nodes) {
int slashIndex = node.path.indexOf('/');
if (slashIndex > 0) {
topFolders.add(node.path.substring(0, slashIndex));
}
}
return topFolders.size() == 1 ? topFolders.iterator().next() : null;
}
private String readZipEntryContent(ZipFile zf, ZipArchiveEntry entry) throws IOException {
try (InputStream is = zf.getInputStream(entry)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int len;
while ((len = is.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
return new String(baos.toByteArray(), StandardCharsets.UTF_8);
}
}
private JSONObject buildFileTreeJsonFast(List<FileTreeNode> nodes) {
JSONObject root = new JSONObject();
root.put("name", "root");
root.put("type", "folder");
JSONArray rootChildren = new JSONArray();
root.put("children", rootChildren);
Map<String, JSONObject> folderCache = new LinkedHashMap<>();
for (FileTreeNode node : nodes) {
String[] parts = node.path.split("/");
JSONArray currentChildren = rootChildren;
StringBuilder pathBuilder = new StringBuilder();
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (StringUtils.isEmpty(part)) {
continue;
}
boolean isFile = (i == parts.length - 1);
if (isFile) {
JSONObject fileNode = new JSONObject();
fileNode.put("name", part);
fileNode.put("type", "file");
fileNode.put("size", node.size);
currentChildren.add(fileNode);
} else {
pathBuilder.append(part);
String cacheKey = pathBuilder.toString();
pathBuilder.append('/');
JSONObject existingFolder = folderCache.get(cacheKey);
if (existingFolder == null) {
existingFolder = new JSONObject();
existingFolder.put("name", part);
existingFolder.put("type", "folder");
existingFolder.put("children", new JSONArray());
currentChildren.add(existingFolder);
folderCache.put(cacheKey, existingFolder);
}
currentChildren = existingFolder.getJSONArray("children");
}
}
}
// 排序文件夹在前文件在后同类型按字母顺序
sortChildren(rootChildren);
// 如果根目录只有一个子文件夹直接返回该子文件夹
JSONArray children = root.getJSONArray("children");
if (children.size() == 1 && "folder".equals(children.getJSONObject(0).get("type"))) {
return children.getJSONObject(0);
}
return root;
}
private JSONObject buildFileTreeJson(List<FileTreeNode> nodes) {
// 构建树形结构的目录
JSONObject root = new JSONObject();
root.put("name", "root");
root.put("type", "folder");
JSONArray rootChildren = new JSONArray();
root.put("children", rootChildren);
for (FileTreeNode node : nodes) {
String[] parts = node.path.split("/");
JSONArray currentChildren = rootChildren;
for (int i = 0; i < parts.length; i++) {
String part = parts[i];
if (StringUtils.isEmpty(part)) {
continue;
}
boolean isFile = (i == parts.length - 1);
if (isFile) {
JSONObject fileNode = new JSONObject();
fileNode.put("name", part);
fileNode.put("type", "file");
fileNode.put("size", node.size);
currentChildren.add(fileNode);
} else {
// 查找是否已存在该文件夹
JSONObject existingFolder = null;
for (int j = 0; j < currentChildren.size(); j++) {
JSONObject child = currentChildren.getJSONObject(j);
if (part.equals(child.get("name")) && "folder".equals(child.get("type"))) {
existingFolder = child;
break;
}
}
if (existingFolder == null) {
existingFolder = new JSONObject();
existingFolder.put("name", part);
existingFolder.put("type", "folder");
existingFolder.put("children", new JSONArray());
currentChildren.add(existingFolder);
}
currentChildren = existingFolder.getJSONArray("children");
}
}
}
// 排序文件夹在前文件在后同类型按字母顺序
sortChildren(rootChildren);
// 如果根目录只有一个子文件夹直接返回该子文件夹
JSONArray children = root.getJSONArray("children");
if (children.size() == 1 && "folder".equals(children.getJSONObject(0).get("type"))) {
return children.getJSONObject(0);
}
return root;
}
private void sortChildren(JSONArray children) {
if (children == null || children.isEmpty()) {
return;
}
if (children.size() > 1) {
children.sort((a, b) -> {
JSONObject ja = (JSONObject) a;
JSONObject jb = (JSONObject) b;
boolean aIsFolder = "folder".equals(ja.getString("type"));
boolean bIsFolder = "folder".equals(jb.getString("type"));
if (aIsFolder != bIsFolder) {
return aIsFolder ? -1 : 1;
}
return ja.getString("name").compareToIgnoreCase(jb.getString("name"));
});
}
for (int i = 0; i < children.size(); i++) {
JSONObject child = children.getJSONObject(i);
if ("folder".equals(child.getString("type"))) {
sortChildren(child.getJSONArray("children"));
}
}
}
private static class FileTreeNode {
final String path;
final long size;
FileTreeNode(String path, long size) {
this.path = path;
this.size = size;
}
}
/**
* SKILL.md 内容中提取 description 字段
*
* @param skillMdContent SKILL.md 文件内容
* @return description 内容如果不存在则返回 null
*/
private String extractDescription(String skillMdContent) {
if (StringUtils.isEmpty(skillMdContent)) {
return null;
}
String[] lines = skillMdContent.split("\\n");
boolean inFrontMatter = false;
boolean foundStart = false;
boolean inMultiLineDesc = false;
StringBuilder multiLineDesc = new StringBuilder();
for (String line : lines) {
String trimmed = line.trim();
// 检测 front matter 开始
if (!foundStart && trimmed.equals("---")) {
foundStart = true;
inFrontMatter = true;
continue;
}
// 检测 front matter 结束
if (foundStart && trimmed.equals("---")) {
break;
}
// front matter 中查找 description
if (inFrontMatter && trimmed.startsWith("description:")) {
String descValue = trimmed.substring("description:".length()).trim();
// 检查是否为多行格式
if (descValue.equals("|") || descValue.equals(">")) {
inMultiLineDesc = true;
continue;
} else {
// 单行格式处理引号包裹的情况
if (descValue.startsWith("\"") && descValue.endsWith("\"")) {
return descValue.substring(1, descValue.length() - 1);
}
if (descValue.startsWith("'") && descValue.endsWith("'")) {
return descValue.substring(1, descValue.length() - 1);
}
return descValue;
}
}
// 处理多行 description 内容
if (inMultiLineDesc) {
// 遇到新的 YAML keyxxx: 格式结束多行模式
if (trimmed.matches("^[a-zA-Z_]+:.*")) {
break;
}
// 追加非空行内容用空格连接
if (!trimmed.isEmpty()) {
if (multiLineDesc.length() > 0) {
multiLineDesc.append(" ");
}
multiLineDesc.append(trimmed);
}
}
}
// 返回多行拼接结果
if (multiLineDesc.length() > 0) {
return multiLineDesc.toString();
}
return null;
}
/**
* SKILL.md 内容中提取 name 字段
*
* @param skillMdContent SKILL.md 文件内容
* @return name 内容如果不存在则返回 null
*/
private String extractName(String skillMdContent) {
if (StringUtils.isEmpty(skillMdContent)) {
return null;
}
String[] lines = skillMdContent.split("\\n");
boolean inFrontMatter = false;
boolean foundStart = false;
for (String line : lines) {
String trimmed = line.trim();
if (!foundStart && trimmed.equals("---")) {
foundStart = true;
inFrontMatter = true;
continue;
}
if (foundStart && trimmed.equals("---")) {
break;
}
if (inFrontMatter && trimmed.startsWith("name:")) {
String name = trimmed.substring("name:".length()).trim();
if (name.startsWith("\"") && name.endsWith("\"")) {
return name.substring(1, name.length() - 1);
}
if (name.startsWith("'") && name.endsWith("'")) {
return name.substring(1, name.length() - 1);
}
return name;
}
}
return null;
}
}

View File

@ -73,7 +73,7 @@ public class ZoneResourceDomainServiceImpl implements IZoneResourceDomainService
@Override
public int insertZoneResourceDomain(ZoneResourceDomainInputVo zoneResourceDomainInputVo) {
ZoneDetail zoneDetail = zoneDetailService.selectZoneDetailById(zoneResourceDomainInputVo.getZoneId());
checkNameInZone(zoneResourceDomainInputVo.getZoneId(), zoneResourceDomainInputVo.getName());
checkNameInZone(zoneResourceDomainInputVo.getZoneId(), zoneResourceDomainInputVo.getName(),zoneResourceDomainInputVo.getDomainCategory());
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainInputVo.toZoneResourceDomain();
zoneResourceDomain.setCreateTime(DateUtils.getNowDate());
zoneResourceDomain.setCreateBy(SecurityUtils.getUsername());
@ -86,11 +86,13 @@ public class ZoneResourceDomainServiceImpl implements IZoneResourceDomainService
/**
* 检查该特色专区下是否已存在该名称的资源领域
*
* @param domainCategory
* @param zoneId
* @param name
* @param domainCategory
*/
private void checkNameInZone(Long zoneId, String name) {
ZoneResourceDomain oldZoneResourceDomain = zoneResourceDomainMapper.selectZoneResourceDomainByNameAndZoneId(name, zoneId);
private void checkNameInZone(Long zoneId, String name, String domainCategory) {
ZoneResourceDomain oldZoneResourceDomain = zoneResourceDomainMapper.selectZoneResourceDomainByNameAndZoneId(name, zoneId,domainCategory);
if (oldZoneResourceDomain != null) {
throw new ServiceException("该特色专区下资源领域名称已存在(资源领域名称[" + name + "])");
}
@ -106,7 +108,7 @@ public class ZoneResourceDomainServiceImpl implements IZoneResourceDomainService
public int updateZoneResourceDomain(ZoneResourceDomainUpdateVo zoneResourceDomainUpdateVo) {
ZoneResourceDomain oldZoneResourceDomain = selectZoneResourceDomainById(zoneResourceDomainUpdateVo.getId());
if (!oldZoneResourceDomain.getName().equals(zoneResourceDomainUpdateVo.getName())) {
checkNameInZone(oldZoneResourceDomain.getZoneId(), zoneResourceDomainUpdateVo.getName());
checkNameInZone(oldZoneResourceDomain.getZoneId(), zoneResourceDomainUpdateVo.getName(), oldZoneResourceDomain.getDomainCategory());
}
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainUpdateVo.toZoneResourceDomain();
zoneResourceDomain.setUpdateTime(DateUtils.getNowDate());

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.HttpStatus;
import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.constant.UserConstants;
@ -16,6 +17,7 @@ import com.microservices.common.security.service.TokenService;
import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.system.api.RemoteFileService;
import com.microservices.system.api.RemoteUserService;
import com.microservices.system.api.domain.SysFileDownloadLogVo;
import com.microservices.system.api.domain.SysFileInfo;
import com.microservices.system.api.domain.SysUser;
import com.microservices.system.api.utils.FeignUtils;
@ -27,12 +29,17 @@ import com.microservices.zone.member.service.IZonePointsAccountService;
import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.domain.ZoneResourceToType;
import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.domain.*;
import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.enums.AiResourceCategory;
import com.microservices.zone.resource.mapper.ZoneResourceMapper;
import com.microservices.zone.resource.mapper.ZoneResourceToTypeMapper;
import com.microservices.zone.resource.mapper.ZoneResourcesLikeMapper;
import com.microservices.zone.resource.service.IZoneResourceDomainService;
import com.microservices.zone.resource.service.IZoneResourceService;
import com.microservices.zone.resource.service.IZoneResourceTypeService;
import com.microservices.zone.resource.service.IAiResourceParseService;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import com.microservices.zone.specialProject.service.IZoneSpecialProjectRelevancyService;
import com.microservices.zone.utils.ZoneConstants;
import org.slf4j.Logger;
@ -41,9 +48,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@ -80,6 +93,10 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
private IZoneSpecialProjectRelevancyService zoneSpecialProjectRelevancyService;
@Autowired
private IZonePointsAccountService zonePointsAccountService;
@Autowired
private ZoneResourcesLikeMapper zoneResourcesLikeMapper;
@Autowired
private IAiResourceParseService aiResourceParseService;
/**
* 查询特色专区资源聚合
@ -107,17 +124,17 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
@DataScope(deptAlias = "zr")
public GenericsTableDataInfo<ZoneResourceDataVo> selectZoneResourceList(ZoneResource zoneResourceSearch) {
List<ZoneResource> zoneResourceList = zoneResourceMapper.selectZoneResourceList(zoneResourceSearch);
return PageUtils.toPage(zoneResourceList, this::zoneResourceToData);
return PageUtils.toPage(zoneResourceList, z -> zoneResourceToData(z, true));
}
@Override
@DataScope(deptAlias = "zr")
public GenericsTableDataInfo<ZoneResourceDataVo> selectZoneResourceList(ZoneResourceOpenSearchVo zoneResourceSearchVo) {
List<ZoneResource> zoneResourceList = zoneResourceMapper.selectZoneResourceListBySearchVo(zoneResourceSearchVo);
return PageUtils.toPage(zoneResourceList, this::zoneResourceToData);
return PageUtils.toPage(zoneResourceList, z -> zoneResourceToData(z, true));
}
private ZoneResourceDataVo zoneResourceToData(ZoneResource zoneResource) {
private ZoneResourceDataVo zoneResourceToData(ZoneResource zoneResource, boolean removeSkill) {
ZoneResourceDataVo zoneResourceDataVo = zoneResource.toZoneResourceDataVo();
int downloadCount = 0;
if (StringUtils.isNotEmpty(zoneResource.getFileIds())) {
@ -156,6 +173,9 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
}
}
zoneResourceDataVo.setDownloadCount(downloadCount);
if (removeSkill && zoneResourceDataVo.getExtendData() != null) {
zoneResourceDataVo.getExtendData().remove(ResourceExtendDataUtils.SKILL_ROOT);
}
return zoneResourceDataVo;
}
@ -175,7 +195,7 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
@Override
public ZoneResourceDataVo insertZoneResource(ZoneResourceInputVo zoneResourceInputVo) {
ZoneDetail zoneDetail = zoneDetailService.selectZoneDetailById(zoneResourceInputVo.getZoneId());
checkNameInZone(zoneResourceInputVo.getZoneId(), zoneResourceInputVo.getName());
checkNameInZone(zoneResourceInputVo.getZoneId(), zoneResourceInputVo.getName(), zoneResourceInputVo.getResourceCategory());
zoneResourceDomainService.selectZoneResourceDomainById(zoneResourceInputVo.getDomainId());
ZoneResource zoneResource = zoneResourceInputVo.toZoneResource();
// 专区开启资源审核后专区会员和专区运营人员新增资源都需要审核
@ -217,12 +237,11 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
public List<String> getAllFileSuffix(String fileIds) {
List<SysFileInfo> sysFileInfoList = getAllFileByFileIds(fileIds);
// 获取文件后缀列表
List<String> suffixList = sysFileInfoList.stream()
return sysFileInfoList.stream()
.map(x -> x.getFileSuffix().toLowerCase())
.distinct()
.collect(Collectors.toList());
return suffixList;
}
public List<SysFileInfo> getAllFileByFileIds(String fileIds) {
@ -233,7 +252,7 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
);
} catch (Exception e) {
logger.error("从文件微服务获取资源附件时失败:{}", e.getMessage());
throw new ServiceException("资源附件列表异常:"+e.getMessage());
throw new ServiceException("资源附件列表异常:" + e.getMessage());
}
return sysFileInfoList;
}
@ -274,8 +293,16 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
@Override
public ZoneResourceDataVo selectOpenZoneResourceDataById(Long resourceId, HttpServletRequest request) {
ZoneResource zoneResource = selectZoneResourceById(resourceId);
if (AiResourceCategory.isAiResourceCategory(zoneResource.getResourceCategory())) {
// 增加浏览次数
incrementViewCount(zoneResource.getId());
}
if (ZoneConstants.RESOURCE_AUDIT_PASS.equals(zoneResource.getAuditStatus())) {
return zoneResourceToData(zoneResource);
ZoneResourceDataVo dataVo = zoneResourceToData(zoneResource, false);
// 填充下载用户信息
populateDownloadUsers(dataVo);
return dataVo;
} else {
String gitLinkToken = AuthUtil.getGitLinkRequestToken(request);
if (StringUtils.isNotEmpty(gitLinkToken)) {
@ -283,7 +310,10 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
remoteUserService.getUserNameByGitLinkToken(gitLinkToken, SecurityConstants.INNER)
);
if (zoneResource.getCreateBy().equals(currentUsername)) {
return zoneResourceToData(zoneResource);
ZoneResourceDataVo dataVo = zoneResourceToData(zoneResource, false);
// 填充下载用户信息
populateDownloadUsers(dataVo);
return dataVo;
}
}
}
@ -291,13 +321,103 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
}
/**
* 填充文件下载用户列表
*/
private void populateDownloadUsers(ZoneResourceDataVo dataVo) {
if (dataVo == null || dataVo.getFileList() == null || dataVo.getFileList().isEmpty()) {
return;
}
try {
// 收集所有文件ID
String fileIds = dataVo.getFileList().stream()
.map(x -> String.valueOf(x.getFileId()))
.collect(Collectors.joining(","));
if (StringUtils.isEmpty(fileIds)) {
return;
}
// 通过Feign调用文件微服务获取下载记录
List<SysFileDownloadLogVo> downloadLogList = FeignUtils.getReturnData(
remoteFileService.getDownloadUserList(fileIds, SecurityConstants.INNER)
);
if (downloadLogList == null) {
downloadLogList = new ArrayList<>();
}
// 补全用户详情昵称手机号邮箱
enrichDownloadLogUserInfo(downloadLogList);
// 按文件ID分组填充到各附件VO中
Map<Long, List<String>> fileUserMap = downloadLogList.stream()
.collect(Collectors.groupingBy(
SysFileDownloadLogVo::getFileId,
Collectors.mapping(SysFileDownloadLogVo::getUserName,
Collectors.toCollection(LinkedHashSet::new))
))
.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> new ArrayList<>(entry.getValue())
));
for (ZoneResourceFileVo fileVo : dataVo.getFileList()) {
List<String> userList = fileUserMap.get(fileVo.getFileId());
fileVo.setDownloadUserList(userList != null ? userList : new ArrayList<>());
}
// 外层直接用原始下载记录列表
dataVo.setDownloadUserSummary(downloadLogList);
} catch (Exception e) {
logger.error("填充下载用户信息失败:{}", e.getMessage());
}
}
/**
* 补全下载记录中的用户详情昵称手机号邮箱
*/
private void enrichDownloadLogUserInfo(List<SysFileDownloadLogVo> downloadLogList) {
if (downloadLogList == null || downloadLogList.isEmpty()) {
return;
}
// 收集所有非空的用户名去重
Set<String> userNames = downloadLogList.stream()
.map(SysFileDownloadLogVo::getUserName)
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toSet());
if (userNames.isEmpty()) {
return;
}
// 逐个查询用户信息并构建映射
Map<String, SysUser> userMap = new HashMap<>();
for (String userName : userNames) {
try {
SysUser sysUser = FeignUtils.getReturnData(
remoteUserService.getSysUserByUserName(userName, SecurityConstants.INNER)
);
if (sysUser != null) {
userMap.put(userName, sysUser);
}
} catch (Exception e) {
logger.warn("查询用户[{}]信息失败:{}", userName, e.getMessage());
}
}
// 填充用户详情到下载记录
for (SysFileDownloadLogVo logVo : downloadLogList) {
if (StringUtils.isEmpty(logVo.getUserName())) {
continue;
}
SysUser sysUser = userMap.get(logVo.getUserName());
if (sysUser != null) {
logVo.setNickName(sysUser.getNickName());
logVo.setPhonenumber(sysUser.getPhonenumber());
logVo.setEmail(sysUser.getEmail());
}
}
}
@Override
public GenericsTableDataInfo<ZoneResourceDataVo> selectMyZoneResourceList(ZoneResource zoneResourceSearch) {
if (zoneResourceSearch == null || zoneResourceSearch.getZoneId() == null) {
throw new ServiceException("获取我的资源列表时必须传入专区Id");
}
List<ZoneResource> zoneResourceList = zoneResourceMapper.selectZoneResourceList(zoneResourceSearch);
return PageUtils.toPage(zoneResourceList, this::zoneResourceToData);
return PageUtils.toPage(zoneResourceList, x -> zoneResourceToData(x, true));
}
@Override
@ -311,8 +431,8 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
* @param zoneId
* @param name
*/
private void checkNameInZone(Long zoneId, String name) {
ZoneResource oldZoneResource = zoneResourceMapper.selectZoneResourceByNameAndZoneId(name, zoneId);
private void checkNameInZone(Long zoneId, String name, String resourceCategory) {
ZoneResource oldZoneResource = zoneResourceMapper.selectZoneResourceByNameAndZoneId(name, zoneId, resourceCategory);
if (oldZoneResource != null) {
throw new ServiceException("该特色专区下资源名称已存在(资源名称[" + name + "])");
}
@ -328,7 +448,7 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
public ZoneResourceDataVo updateZoneResource(ZoneResourceUpdateVo zoneResourceUpdateVo) {
ZoneResource oldZoneResource = selectZoneResourceById(zoneResourceUpdateVo.getId());
if (!oldZoneResource.getName().equals(zoneResourceUpdateVo.getName())) {
checkNameInZone(oldZoneResource.getZoneId(), zoneResourceUpdateVo.getName());
checkNameInZone(oldZoneResource.getZoneId(), zoneResourceUpdateVo.getName(), oldZoneResource.getResourceCategory());
}
if (zoneResourceUpdateVo.getFileIds() != null) {
List<ZoneResourceToType> oldResourceToTypeList = zoneResourceToTypeMapper
@ -354,9 +474,7 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
// 获取删除的类型Id列表
Long[] delZoneResourceTypeIds = newTypeIdList
.stream()
.filter(x -> !newTypeIdList.contains(x))
.collect(Collectors.toList())
.toArray(new Long[0]);
.filter(x -> !newTypeIdList.contains(x)).toArray(Long[]::new);
if (delZoneResourceTypeIds.length > 0) {
// 删除资源与资源类型关联关系
zoneResourceToTypeMapper.deleteResourceToTypesByResourceIdAndTypeIds(oldZoneResource.getId(), delZoneResourceTypeIds);
@ -377,8 +495,10 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
zoneResource.setSort(setSort);
}
ZoneDetail zoneDetail = zoneDetailService.selectZoneDetailById(oldZoneResource.getZoneId());
// 专区开启资源审核后专区会员和专区运营人员编辑资源都需要审核
if (resourceNeedAudit(zoneDetail)) {
// 草稿状态的资源重新编辑后变为待审核
if (ZoneConstants.RESOURCE_DRAFT.equals(oldZoneResource.getAuditStatus())) {
zoneResource.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
} else if (resourceNeedAudit(zoneDetail)) {
zoneResource.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
}
zoneResourceMapper.updateZoneResource(zoneResource);
@ -422,6 +542,462 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
@Override
public ZoneResourceDataVo selectZoneResourceDataById(Long id) {
ZoneResource zoneResource = selectZoneResourceById(id);
return zoneResourceToData(zoneResource);
return zoneResourceToData(zoneResource, false);
}
// ==================== AI资源广场 Skill 功能 ====================
@Override
public ZoneResourceDataVo insertAiSkillResource(AiSkillInputVo inputVo) {
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainService.selectZoneResourceDomainById(inputVo.getDomainId());
if (!AiResourceCategory.SKILL.getCode().equals(zoneResourceDomain.getDomainCategory())) {
throw new ServiceException("当前资源领域非SKILL类型");
}
ZoneResourceInputVo zoneResourceInputVo = inputVo.toZoneResourceInputVo();
return insertZoneResource(zoneResourceInputVo);
}
@Override
public ZoneResourceDataVo updateAiSkillResource(AiSkillUpdateVo updateVo) {
ZoneResource oldResource = getAiResourceById(updateVo.getId());
if (!AiResourceCategory.SKILL.getCode().equals(oldResource.getResourceCategory())) {
throw new ServiceException("当前资源非Skill资源");
}
// 已发布状态不允许直接修改
if (ZoneConstants.RESOURCE_AUDIT_PASS.equals(oldResource.getAuditStatus())) {
throw new ServiceException("已发布的资源不允许直接修改,请先下架后再修改");
}
if (ZoneConstants.RESOURCE_TAKEN_DOWN.equals(oldResource.getAuditStatus())) {
oldResource.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
}
if (updateVo.getDomainId() != null) {
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainService.selectZoneResourceDomainById(updateVo.getDomainId());
if (!AiResourceCategory.SKILL.getCode().equals(zoneResourceDomain.getDomainCategory())) {
throw new ServiceException("当前资源领域非SKILL类型");
}
}
ZoneResourceUpdateVo zoneResourceUpdateVo = updateVo.toZoneResourceUpdateVo(oldResource);
return updateZoneResource(zoneResourceUpdateVo);
}
@Override
public ZoneResourceDataVo selectAiSkillResourceById(Long id) {
ZoneResource resource = getAiResourceById(id);
return zoneResourceToData(resource, false);
}
@Override
public GenericsTableDataInfo<ZoneResourceDataVo> selectAiSkillResourceList(ZoneResource zoneResource) {
zoneResource.setResourceCategory(AiResourceCategory.SKILL.getCode());
List<ZoneResource> list = zoneResourceMapper.selectZoneResourceList(zoneResource);
return PageUtils.toPage(list, x -> zoneResourceToData(x, true));
}
@Override
public GenericsTableDataInfo<ZoneResourceDataVo> selectAiModelResourceList(ZoneResource zoneResource) {
zoneResource.setResourceCategory(AiResourceCategory.MODEL.getCode());
List<ZoneResource> list = zoneResourceMapper.selectZoneResourceList(zoneResource);
return PageUtils.toPage(list, x -> zoneResourceToData(x, true));
}
@Override
public GenericsTableDataInfo<ZoneResourceDataVo> selectAiDatasetResourceList(ZoneResource zoneResource) {
zoneResource.setResourceCategory(AiResourceCategory.DATASET.getCode());
List<ZoneResource> list = zoneResourceMapper.selectZoneResourceList(zoneResource);
return PageUtils.toPage(list, x -> zoneResourceToData(x, true));
}
@Override
public UserResourceStatisticsVo getResourceStatistics(Long zoneId, Boolean isMine, String resourceCategory) {
String createBy = null;
if (isMine) {
createBy = SecurityUtils.getUsername();
}
UserResourceStatisticsVo statistics = new UserResourceStatisticsVo();
// 构造查询条件
ZoneResource likeQuery = new ZoneResource();
likeQuery.setZoneId(zoneId);
if (StringUtils.isNotEmpty(createBy)) {
likeQuery.setCreateBy(createBy);
}
if (StringUtils.isNotEmpty(resourceCategory)) {
likeQuery.setResourceCategory(resourceCategory);
}
// 查询点赞总数
List<ZoneResource> resourceList = zoneResourceMapper.selectZoneResourceList(likeQuery);
int totalLikeCount = 0;
for (ZoneResource resource : resourceList) {
if (resource.getLikeCount() != null) {
totalLikeCount += resource.getLikeCount();
}
}
statistics.setLikeCount(totalLikeCount);
// 统计已发布数量
ZoneResource publishedQuery = new ZoneResource();
publishedQuery.setZoneId(zoneId);
if (StringUtils.isNotEmpty(createBy)) {
publishedQuery.setCreateBy(createBy);
}
if (StringUtils.isNotEmpty(resourceCategory)) {
publishedQuery.setResourceCategory(resourceCategory);
}
publishedQuery.setAuditStatus(ZoneConstants.RESOURCE_AUDIT_PASS);
Integer publishedCount = zoneResourceMapper.selectZoneResourceCount(publishedQuery);
statistics.setPublishedCount(publishedCount);
// 统计待审核数量
ZoneResource pendingQuery = new ZoneResource();
pendingQuery.setZoneId(zoneId);
if (StringUtils.isNotEmpty(createBy)) {
pendingQuery.setCreateBy(createBy);
}
if (StringUtils.isNotEmpty(resourceCategory)) {
pendingQuery.setResourceCategory(resourceCategory);
}
pendingQuery.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
Integer pendingCount = zoneResourceMapper.selectZoneResourceCount(pendingQuery);
statistics.setPendingCount(pendingCount);
return statistics;
}
@Override
public int takeDownAiResource(Long id, String reason) {
ZoneResource resource = getAiResourceById(id);
if (!ZoneConstants.RESOURCE_AUDIT_PASS.equals(resource.getAuditStatus())) {
throw new ServiceException("仅已发布的资源可以下架");
}
resource.setAuditStatus(ZoneConstants.RESOURCE_TAKEN_DOWN);
if (StringUtils.isNotEmpty(reason)) {
resource.setRemark(reason);
}
resource.setUpdateTime(DateUtils.getNowDate());
resource.setUpdateBy(SecurityUtils.getUsername());
return zoneResourceMapper.updateZoneResource(resource);
}
@Override
public int takeUpAiResource(Long id, String reason) {
ZoneResource resource = getAiResourceById(id);
if (!ZoneConstants.RESOURCE_TAKEN_DOWN.equals(resource.getAuditStatus())) {
throw new ServiceException("仅已下架的资源可以重新上架");
}
resource.setAuditStatus(ZoneConstants.RESOURCE_AUDIT_PASS);
if (StringUtils.isNotEmpty(reason)) {
resource.setRemark(reason);
}
resource.setUpdateTime(DateUtils.getNowDate());
resource.setUpdateBy(SecurityUtils.getUsername());
return zoneResourceMapper.updateZoneResource(resource);
}
@Override
public List<JSONObject> selectZoneResourcesIsLike(String ids) {
List<JSONObject> list = new ArrayList<>();
if (StringUtils.isNotEmpty(ids)) {
String[] idArr = ids.split(",");
List<ZoneResourcesLike> zoneResourcesLikeList = zoneResourcesLikeMapper.selectByResourceIds(idArr);
for (int i = 0; i < idArr.length; i++) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("id", idArr[i]);
if (zoneResourcesLikeList != null && !zoneResourcesLikeList.isEmpty()) {
int finalI = i;
jsonObject.put("isLike", zoneResourcesLikeList.stream().anyMatch(x -> idArr[finalI].equals(String.valueOf(x.getResourceId()))));
} else {
jsonObject.put("isLike", false);
}
list.add(jsonObject);
}
}
return list;
}
@Override
public int resubmitAiResource(Long id) {
ZoneResource resource = getAiResourceById(id);
checkResourceOwner(resource);
if (!ZoneConstants.RESOURCE_AUDIT_FAILED.equals(resource.getAuditStatus())
&& !ZoneConstants.RESOURCE_TAKEN_DOWN.equals(resource.getAuditStatus())
&& !ZoneConstants.RESOURCE_DRAFT.equals(resource.getAuditStatus())) {
throw new ServiceException("仅被驳回、已下架或草稿的资源可以重新提交");
}
resource.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
resource.setRemark(null);
resource.setUpdateTime(DateUtils.getNowDate());
return zoneResourceMapper.updateZoneResource(resource);
}
@Override
public int withdrawAiResource(Long id) {
ZoneResource resource = getAiResourceById(id);
checkResourceOwner(resource);
if (!ZoneConstants.RESOURCE_NOT_AUDIT.equals(resource.getAuditStatus())) {
throw new ServiceException("仅待审核的资源可以撤回");
}
resource.setAuditStatus(ZoneConstants.RESOURCE_DRAFT);
resource.setUpdateTime(DateUtils.getNowDate());
return zoneResourceMapper.updateZoneResource(resource);
}
@Override
public List<ZoneResourceDataVo> selectSkillRecommendations(int limit) {
List<ZoneResource> list = zoneResourceMapper.selectSkillRecommendations(
AiResourceCategory.SKILL.getCode(), ZoneConstants.RESOURCE_AUDIT_PASS, limit);
return list.stream().map(z -> zoneResourceToData(z, false)).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleZoneResourcesLike(Long resourceId) {
// 校验资源存在且已发布
ZoneResource resource = getAiResourceById(resourceId);
if (!ZoneConstants.RESOURCE_AUDIT_PASS.equals(resource.getAuditStatus())) {
throw new ServiceException("仅已发布的资源可以点赞");
}
Long userId = SecurityUtils.getGitlinkUserId();
if (userId == null) {
throw new ServiceException("当前用户未绑定平台账号,无法点赞");
}
ZoneResourcesLike existingLike = zoneResourcesLikeMapper.selectByResourceAndUser(resourceId, userId);
if (existingLike != null) {
// 取消点赞
zoneResourcesLikeMapper.deleteLike(resourceId, userId);
zoneResourceMapper.updateLikeCount(resourceId, -1);
return false;
} else {
// 新增点赞
ZoneResourcesLike like = new ZoneResourcesLike();
like.setResourceId(resourceId);
like.setUserId(userId);
like.setCreateTime(DateUtils.getNowDate());
zoneResourcesLikeMapper.insertLike(like);
zoneResourceMapper.updateLikeCount(resourceId, 1);
return true;
}
}
@Override
public void incrementViewCount(Long id) {
zoneResourceMapper.incrementViewCount(id);
}
// ==================== AI资源辅助方法 ====================
private ZoneResource getAiResourceById(Long id) {
ZoneResource resource = zoneResourceMapper.selectZoneResourceById(id);
if (resource == null) {
throw new ServiceException("资源不存在(资源id[" + id + "])");
}
if (!AiResourceCategory.isAiResourceCategory(resource.getResourceCategory())) {
throw new ServiceException("该资源不是AI资源");
}
return resource;
}
private void checkResourceOwner(ZoneResource resource) {
if (!resource.getCreateBy().equals(SecurityUtils.getUsername())) {
throw new ServiceException("仅允许操作自己发布的资源");
}
}
private void validateSkillNameUnique(String name, Long excludeId) {
if (StringUtils.isEmpty(name)) {
return;
}
ZoneResource existing = zoneResourceMapper.selectSkillResourceByName(name);
if (existing != null && !existing.getId().equals(excludeId)) {
throw new ServiceException("Skill名称已存在(名称[" + name + "])");
}
}
private void validateKeywords(String keywords) {
if (StringUtils.isEmpty(keywords)) {
return;
}
String[] tags = keywords.split(",");
if (tags.length > ZoneConstants.KEYWORD_MAX_COUNT) {
throw new ServiceException("标签数量不能超过" + ZoneConstants.KEYWORD_MAX_COUNT + "");
}
for (String tag : tags) {
if (tag.trim().length() > ZoneConstants.KEYWORD_MAX_LENGTH) {
throw new ServiceException("单个标签不能超过" + ZoneConstants.KEYWORD_MAX_LENGTH + "个字符");
}
}
}
// ==================== AI资源广场 Model 功能 ====================
@Override
public ZoneResourceDataVo insertAiModelResource(AiModelInputVo inputVo) {
// 1. 校验领域类型是否为 MODEL
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainService.selectZoneResourceDomainById(inputVo.getDomainId());
if (zoneResourceDomain == null) {
throw new ServiceException("资源领域不存在");
}
if (!AiResourceCategory.MODEL.getCode().equals(zoneResourceDomain.getDomainCategory())) {
throw new ServiceException("当前资源领域非MODEL类型");
}
return insertModelOrDatasetResource(inputVo.toZoneResourceInputVo(), inputVo.getFileId());
}
@Override
public ZoneResourceDataVo updateAiModelResource(AiModelUpdateVo updateVo) {
// 1. 获取现有资源
ZoneResource oldResource = getAiResourceById(updateVo.getId());
if (!AiResourceCategory.MODEL.getCode().equals(oldResource.getResourceCategory())) {
throw new ServiceException("当前资源非Model资源");
}
// 2. 状态校验
if (ZoneConstants.RESOURCE_AUDIT_PASS.equals(oldResource.getAuditStatus())) {
throw new ServiceException("已发布的资源不允许直接修改,请先下架后再修改");
}
if (ZoneConstants.RESOURCE_TAKEN_DOWN.equals(oldResource.getAuditStatus())) {
oldResource.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
}
// 3. 领域校验
if (updateVo.getDomainId() != null) {
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainService.selectZoneResourceDomainById(updateVo.getDomainId());
if (zoneResourceDomain == null) {
throw new ServiceException("资源领域不存在");
}
if (!AiResourceCategory.MODEL.getCode().equals(zoneResourceDomain.getDomainCategory())) {
throw new ServiceException("当前资源领域非MODEL类型");
}
}
return updateModelOrDatasetResource(oldResource, updateVo.toZoneResourceUpdateVo(oldResource), updateVo.getFileId());
}
private ZoneResourceDataVo updateModelOrDatasetResource(ZoneResource oldResource, ZoneResourceUpdateVo updateVo, Long fileId) {
// 4. 获取 VO 层已合并的 extendData可能包含 readmeContent或使用旧的
JSONObject extendData;
if (updateVo.getExtendData() != null) {
extendData = JSONObject.parseObject(updateVo.getExtendData());
} else {
extendData = oldResource.getExtendData();
}
if (extendData == null) {
extendData = new JSONObject();
}
// 5. 如果更新了文件重新解析
if (fileId != null) {
AiUploadedResourceParseResultVo parseResult = aiResourceParseService.parseUploadedZip(fileId);
// 更新 extendData
JSONObject fileTreeJsonObj = parseResult.getFileTreeJson();
extendData = ResourceExtendDataUtils.updateModelOrDatasetReadmeContent(oldResource.getResourceCategory(), extendData, parseResult.getReadmeContent());
extendData = ResourceExtendDataUtils.updateModelOrDatasetFileTree(oldResource.getResourceCategory(), extendData, fileTreeJsonObj);
// 更新 fileSize
if (!extendData.containsKey(oldResource.getResourceCategory())) {
extendData.put(oldResource.getResourceCategory(), new JSONObject());
}
extendData.getJSONObject(oldResource.getResourceCategory()).put(ResourceExtendDataUtils.FILE_SIZE, parseResult.getFileSizeInfo());
}
if (fileId == null) {
updateVo.setFileIds(oldResource.getFileIds());
} else {
updateVo.setFileIds(String.valueOf(fileId));
}
updateVo.setExtendData(extendData.toJSONString());
// 7. 更新资源
return updateZoneResource(updateVo);
}
@Override
public ZoneResourceDataVo selectAiModelResourceById(Long id) {
ZoneResource resource = getAiResourceById(id);
if (!AiResourceCategory.MODEL.getCode().equals(resource.getResourceCategory())) {
throw new ServiceException("当前资源非Model资源");
}
return zoneResourceToData(resource, false);
}
// ==================== AI资源广场 Dataset 功能 ====================
@Override
public ZoneResourceDataVo insertAiDatasetResource(AiDatasetInputVo inputVo) {
// 1. 校验领域类型是否为 DATASET
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainService.selectZoneResourceDomainById(inputVo.getDomainId());
if (zoneResourceDomain == null) {
throw new ServiceException("资源领域不存在");
}
if (!AiResourceCategory.DATASET.getCode().equals(zoneResourceDomain.getDomainCategory())) {
throw new ServiceException("当前资源领域非DATASET类型");
}
return insertModelOrDatasetResource(inputVo.toZoneResourceInputVo(), inputVo.getFileId());
}
private ZoneResourceDataVo insertModelOrDatasetResource(ZoneResourceInputVo inputVo, Long fileId) {
// 2. 调用 parseUploadedZip 解析文件获取 readmeContent fileTreeJson
AiUploadedResourceParseResultVo parseResult = aiResourceParseService.parseUploadedZip(fileId);
JSONObject extendData = ResourceExtendDataUtils.buildModelOrDataSetExtendData(
inputVo.getResourceCategory(), parseResult);
inputVo.setExtendData(extendData.toJSONString());
// 6. 创建资源
return insertZoneResource(inputVo);
}
@Override
public ZoneResourceDataVo updateAiDatasetResource(AiDatasetUpdateVo updateVo) {
// 1. 获取现有资源
ZoneResource oldResource = getAiResourceById(updateVo.getId());
if (!AiResourceCategory.DATASET.getCode().equals(oldResource.getResourceCategory())) {
throw new ServiceException("当前资源非Dataset资源");
}
// 2. 状态校验
if (ZoneConstants.RESOURCE_AUDIT_PASS.equals(oldResource.getAuditStatus())) {
throw new ServiceException("已发布的资源不允许直接修改,请先下架后再修改");
}
if (ZoneConstants.RESOURCE_TAKEN_DOWN.equals(oldResource.getAuditStatus())) {
oldResource.setAuditStatus(ZoneConstants.RESOURCE_NOT_AUDIT);
}
// 3. 领域校验
if (updateVo.getDomainId() != null) {
ZoneResourceDomain zoneResourceDomain = zoneResourceDomainService.selectZoneResourceDomainById(updateVo.getDomainId());
if (zoneResourceDomain == null) {
throw new ServiceException("资源领域不存在");
}
if (!AiResourceCategory.DATASET.getCode().equals(zoneResourceDomain.getDomainCategory())) {
throw new ServiceException("当前资源领域非DATASET类型");
}
}
return updateModelOrDatasetResource(oldResource, updateVo.toZoneResourceUpdateVo(oldResource), updateVo.getFileId());
}
@Override
public ZoneResourceDataVo selectAiDatasetResourceById(Long id) {
ZoneResource resource = getAiResourceById(id);
if (!AiResourceCategory.DATASET.getCode().equals(resource.getResourceCategory())) {
throw new ServiceException("当前资源非Dataset资源");
}
return zoneResourceToData(resource, false);
}
}

View File

@ -0,0 +1,234 @@
package com.microservices.zone.resource.utils;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.zone.resource.domain.vo.AiUploadedResourceParseResultVo;
/**
* 资源 extendData 工具类
* 用于处理不同资源类型的扩展数据
*/
public class ResourceExtendDataUtils {
public static final String SKILL_ROOT = "SKILL";
private static final String SKILL_CONTENT = "content";
private static final String MODEL_ROOT = "MODEL";
private static final String DATASET_ROOT = "DATASET";
private static final String CONTENT = "content";
public static final String FILE_TREE = "fileTree";
public static final String FILE_SIZE = "fileSize";
/**
* 构建 Skill 资源的 extendData JSONObject
*
* @param skillContent SKILL.md 内容
* @param fileTreeJson 目录树 JSONObject
* @return extendData JSONObject
*/
public static JSONObject buildSkillExtendData(String skillContent, JSONObject fileTreeJson) {
JSONObject extendData = new JSONObject();
JSONObject skill = new JSONObject();
skill.put(SKILL_CONTENT, skillContent);
if (fileTreeJson != null) {
skill.put(FILE_TREE, fileTreeJson);
}
extendData.put(SKILL_ROOT, skill);
return extendData;
}
/**
* extendData 中提取 skillContent
*
* @param extendData extendData JSONObject
* @return skillContent不存在返回 null
*/
public static String extractSkillContent(JSONObject extendData) {
if (extendData == null || !extendData.containsKey(SKILL_ROOT)) {
return null;
}
JSONObject skill = extendData.getJSONObject(SKILL_ROOT);
return skill.getString(SKILL_CONTENT);
}
/**
* extendData 中提取 fileTree
*
* @param extendData extendData JSONObject
* @return fileTree JSONObject不存在返回 null
*/
public static JSONObject extractSkillFileTree(JSONObject extendData) {
if (extendData == null || !extendData.containsKey(SKILL_ROOT)) {
return null;
}
JSONObject skill = extendData.getJSONObject(SKILL_ROOT);
return skill.getJSONObject(FILE_TREE);
}
/**
* 更新 extendData 中的 skillContent
*
* @param extendData extendData JSONObject
* @param skillContent 新的 skillContent
* @return 更新后的 extendData
*/
public static JSONObject updateSkillContent(JSONObject extendData, String skillContent) {
if (extendData == null) {
extendData = new JSONObject();
}
if (!extendData.containsKey(SKILL_ROOT)) {
extendData.put(SKILL_ROOT, new JSONObject());
}
JSONObject skill = extendData.getJSONObject(SKILL_ROOT);
skill.put(SKILL_CONTENT, skillContent);
return extendData;
}
/**
* 更新 extendData 中的 fileTree
*
* @param extendData extendData JSONObject
* @param fileTreeJson 新的 fileTree
* @return 更新后的 extendData
*/
public static JSONObject updateSkillFileTree(JSONObject extendData, JSONObject fileTreeJson) {
if (extendData == null) {
extendData = new JSONObject();
}
if (!extendData.containsKey(SKILL_ROOT)) {
extendData.put(SKILL_ROOT, new JSONObject());
}
JSONObject skill = extendData.getJSONObject(SKILL_ROOT);
skill.put(FILE_TREE, fileTreeJson);
return extendData;
}
// ==================== Model 资源 extendData 方法 ====================
/**
* 构建 Model/数据集 资源的 extendData JSONObject
*
* @return extendData JSONObject
*/
public static JSONObject buildModelOrDataSetExtendData(String resourceCategory, AiUploadedResourceParseResultVo parseResult) {
JSONObject extendData = new JSONObject();
JSONObject data = new JSONObject();
if (parseResult.getReadmeContent() != null) {
data.put(CONTENT, parseResult.getReadmeContent());
}
if (parseResult.getFileTreeJson() != null) {
data.put(FILE_TREE, parseResult.getFileTreeJson());
}
if (parseResult.getFileSizeInfo() != null) {
data.put(FILE_SIZE, parseResult.getFileSizeInfo());
}
extendData.put(resourceCategory, data);
return extendData;
}
/**
* extendData 中提取 model readmeContent
*/
public static String extractModelReadmeContent(JSONObject extendData) {
if (extendData == null || !extendData.containsKey(MODEL_ROOT)) {
return null;
}
JSONObject model = extendData.getJSONObject(MODEL_ROOT);
return model.getString(CONTENT);
}
/**
* extendData 中提取 model fileTree
*/
public static JSONObject extractModelFileTree(JSONObject extendData) {
if (extendData == null || !extendData.containsKey(MODEL_ROOT)) {
return null;
}
JSONObject model = extendData.getJSONObject(MODEL_ROOT);
return model.getJSONObject(FILE_TREE);
}
/**
* 更新 extendData 中的 readmeContent
*/
public static JSONObject updateModelOrDatasetReadmeContent(String resourceCategory, JSONObject extendData, String readmeContent) {
if (extendData == null) {
extendData = new JSONObject();
}
if (!extendData.containsKey(resourceCategory)) {
extendData.put(resourceCategory, new JSONObject());
}
JSONObject data = extendData.getJSONObject(resourceCategory);
data.put(CONTENT, readmeContent);
return extendData;
}
/**
* 更新 extendData 中的 model fileTree
*/
public static JSONObject updateModelOrDatasetFileTree(String resourceCategory, JSONObject extendData, JSONObject fileTreeJson) {
if (extendData == null) {
extendData = new JSONObject();
}
if (!extendData.containsKey(resourceCategory)) {
extendData.put(resourceCategory, new JSONObject());
}
JSONObject model = extendData.getJSONObject(resourceCategory);
model.put(FILE_TREE, fileTreeJson);
return extendData;
}
/**
* extendData 中提取 dataset readmeContent
*/
public static String extractDatasetReadmeContent(JSONObject extendData) {
if (extendData == null || !extendData.containsKey(DATASET_ROOT)) {
return null;
}
JSONObject dataset = extendData.getJSONObject(DATASET_ROOT);
return dataset.getString(CONTENT);
}
/**
* extendData 中提取 dataset fileTree
*/
public static JSONObject extractDatasetFileTree(JSONObject extendData) {
if (extendData == null || !extendData.containsKey(DATASET_ROOT)) {
return null;
}
JSONObject dataset = extendData.getJSONObject(DATASET_ROOT);
return dataset.getJSONObject(FILE_TREE);
}
/**
* 更新 extendData 中的 dataset readmeContent
*/
public static JSONObject updateDatasetReadmeContent(JSONObject extendData, String readmeContent) {
if (extendData == null) {
extendData = new JSONObject();
}
if (!extendData.containsKey(DATASET_ROOT)) {
extendData.put(DATASET_ROOT, new JSONObject());
}
JSONObject dataset = extendData.getJSONObject(DATASET_ROOT);
dataset.put(CONTENT, readmeContent);
return extendData;
}
/**
* 更新 extendData 中的 dataset fileTree
*/
public static JSONObject updateDatasetFileTree(JSONObject extendData, JSONObject fileTreeJson) {
if (extendData == null) {
extendData = new JSONObject();
}
if (!extendData.containsKey(DATASET_ROOT)) {
extendData.put(DATASET_ROOT, new JSONObject());
}
JSONObject dataset = extendData.getJSONObject(DATASET_ROOT);
dataset.put(FILE_TREE, fileTreeJson);
return extendData;
}
}

View File

@ -235,10 +235,18 @@ public class ZoneConstants {
* 资源审核状态-审核通过
*/
public final static String RESOURCE_AUDIT_PASS = "1";
/**
* 资源审核状态-所有状态
*/
public final static String RESOURCE_AUDIT_ALL = "all";
/**
* 资源审核状态-审核未通过
*/
public final static String RESOURCE_AUDIT_FAILED = "2";
/**
* 资源审核状态-草稿
*/
public final static String RESOURCE_DRAFT = "4";
/**
* 资源审核状态-待审核
*/
@ -256,6 +264,10 @@ public class ZoneConstants {
* 项目审核状态-待审核
*/
public final static String PROJECT_NOT_AUDIT = "0";
/**
* 资源审核状态-已下架
*/
public final static String RESOURCE_TAKEN_DOWN = "3";
/**
* 前台权限-管理者
*/
@ -272,4 +284,24 @@ public class ZoneConstants {
public static String getFileSuffixOfResourceTypeRedisKey(Long zoneId, String fileSuffix) {
return REDIS_KEY_FILE_SUFFIX_OF_RESOURCE_TYPE_PREFIX + zoneId + "_" + fileSuffix;
}
/**
* Skill ZIP文件最大大小50MB
*/
public final static long SKILL_ZIP_MAX_SIZE = 50 * 1024 * 1024;
/**
* SKILL.md 最少内容长度
*/
public final static int SKILL_MD_MIN_LENGTH = 100;
/**
* 标签最大数量
*/
public final static int KEYWORD_MAX_COUNT = 5;
/**
* 单个标签最大字符数
*/
public final static int KEYWORD_MAX_LENGTH = 10;
}

View File

@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.microservices.zone.journals.mapper.ZoneJournalsMapper">
<resultMap type="com.microservices.zone.journals.domain.ZoneJournals" id="ZoneJournalsResult">
<result property="id" column="id" />
<result property="journalizedId" column="journalized_id" />
<result property="journalizedType" column="journalized_type" />
<result property="userId" column="user_id" />
<result property="notes" column="notes" />
<result property="parentId" column="parent_id" />
<result property="replyId" column="reply_id" />
<result property="commentsCount" column="comments_count" />
<result property="attachmentIdentifiers" column="attachment_identifiers"/>
<result property="receiversLogin" column="receivers_login" />
<result property="delFlag" column="del_flag" />
<result property="createBy" column="create_by" />
<result property="createdOn" column="created_on" />
<result property="updateBy" column="update_by" />
<result property="updatedOn" column="updated_on" />
</resultMap>
<sql id="selectZoneJournalsVo">
select id,
journalized_id,
journalized_type,
user_id,
notes,
parent_id,
reply_id,
comments_count,
attachment_identifiers,
receivers_login,
del_flag,
create_by,
created_on,
update_by,
updated_on
from zone_journals
</sql>
<select id="selectZoneJournalsList" parameterType="com.microservices.zone.journals.domain.ZoneJournals" resultMap="ZoneJournalsResult">
<include refid="selectZoneJournalsVo"/>
<where>
<if test="journalizedId != null "> and journalized_id = #{journalizedId}</if>
<if test="journalizedType != null and journalizedType != ''"> and journalized_type = #{journalizedType}</if>
<if test="userId != null "> and user_id = #{userId}</if>
and del_flag = '0'
and parent_id is NULL
</where>
order by created_on desc
</select>
<select id="selectZoneJournalsById" parameterType="Long" resultMap="ZoneJournalsResult">
<include refid="selectZoneJournalsVo"/>
where id = #{id}
</select>
<select id="selectChildrenZoneJournalsList" resultMap="ZoneJournalsResult">
<include refid="selectZoneJournalsVo"/>
where parent_id = #{parentId}
and del_flag = '0'
order by created_on asc
</select>
<select id="countByJournalized" resultType="int">
select count(*)
from zone_journals
where journalized_id = #{journalizedId}
and journalized_type = #{journalizedType}
and del_flag = '0'
</select>
<insert id="insertZoneJournals" parameterType="com.microservices.zone.journals.domain.ZoneJournals" useGeneratedKeys="true" keyProperty="id">
insert into zone_journals
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="journalizedId != null">journalized_id,</if>
<if test="journalizedType != null and journalizedType != ''">journalized_type,</if>
<if test="userId != null">user_id,</if>
<if test="notes != null and notes != ''">notes,</if>
<if test="parentId != null">parent_id,</if>
<if test="replyId != null">reply_id,</if>
<if test="commentsCount != null">comments_count,</if>
<if test="attachmentIdentifiers != null">attachment_identifiers,</if>
<if test="receiversLogin != null">receivers_login,</if>
<if test="delFlag != null">del_flag,</if>
<if test="createBy != null">create_by,</if>
<if test="createdOn != null">created_on,</if>
<if test="updateBy != null">update_by,</if>
<if test="updatedOn != null">updated_on,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="journalizedId != null">#{journalizedId},</if>
<if test="journalizedType != null and journalizedType != ''">#{journalizedType},</if>
<if test="userId != null">#{userId},</if>
<if test="notes != null and notes != ''">#{notes},</if>
<if test="parentId != null">#{parentId},</if>
<if test="replyId != null">#{replyId},</if>
<if test="commentsCount != null">#{commentsCount},</if>
<if test="attachmentIdentifiers != null">#{attachmentIdentifiers},</if>
<if test="receiversLogin != null">#{receiversLogin},</if>
<if test="delFlag != null">#{delFlag},</if>
<if test="createBy != null">#{createBy},</if>
<if test="createdOn != null">#{createdOn},</if>
<if test="updateBy != null">#{updateBy},</if>
<if test="updatedOn != null">#{updatedOn},</if>
</trim>
</insert>
<update id="updateZoneJournals" parameterType="com.microservices.zone.journals.domain.ZoneJournals">
update zone_journals
<trim prefix="SET" suffixOverrides=",">
<if test="notes != null and notes != ''">notes = #{notes},</if>
<if test="attachmentIdentifiers != null">attachment_identifiers = #{attachmentIdentifiers},</if>
<if test="receiversLogin != null">receivers_login = #{receiversLogin},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="updatedOn != null">updated_on = #{updatedOn},</if>
</trim>
where id = #{id}
</update>
<update id="updateCommentsCountById">
UPDATE zone_journals
SET comments_count = (
SELECT COUNT(*)
FROM (
SELECT 1
FROM zone_journals
WHERE parent_id = #{id}
AND del_flag = '0'
) AS tmp
)
WHERE id = #{id}
</update>
<update id="updateJournalsIdByTypeAndOldId">
update zone_journals
set journalized_id = #{journalId}
where journalized_type = #{type}
and journalized_id = #{oldJournalId}
</update>
<update id="deleteZoneJournalsById" parameterType="Long">
update zone_journals set del_flag = '2' where id = #{id}
</update>
<update id="deleteZoneJournalsByIds" parameterType="String">
update zone_journals set del_flag = '2' where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</update>
<update id="deleteZoneJournalsByParentId">
update zone_journals set del_flag = '2' where parent_id = #{parentId}
</update>
<update id="deleteZoneJournalsByTypeAndJournalId">
update zone_journals set del_flag = '2'
where journalized_id = #{journalId}
and journalized_type = #{type}
</update>
</mapper>

View File

@ -155,6 +155,7 @@
</choose>
<if test="isHomepage != null ">and zp.is_homepage = #{isHomepage}</if>
<if test="excludeId != null ">and zp.id != #{excludeId}</if>
<if test="createBy != null and createBy != ''">and zp.create_by = #{createBy}</if>
<!-- 数据范围过滤 -->
${params.dataScope}
@ -172,6 +173,7 @@
<if test="name != null and name != ''">and zp.name like concat('%', #{name}, '%')</if>
<if test="projectTypeId != null ">and zp.project_type_id = #{projectTypeId}</if>
<if test="zoneId != null ">and zp.zone_id = #{zoneId}</if>
<if test="createBy != null and createBy != ''">and zp.create_by = #{createBy}</if>
<choose>
<when test="excludesFailure != null and excludesFailure">
and zp.audit_status != '2'

View File

@ -16,6 +16,7 @@
<result property="updateTime" column="update_time"/>
<result property="zoneId" column="zone_id"/>
<result property="introduction" column="introduction"/>
<result property="domainCategory" column="domain_category"/>
</resultMap>
<sql id="selectZoneResourceDomainVo">
@ -29,7 +30,8 @@
update_by,
update_time,
introduction,
zone_id
zone_id,
domain_category
from zone_resource_domain
</sql>
@ -44,7 +46,8 @@
zrd.update_by,
zrd.update_time,
zrd.introduction,
zrd.zone_id
zrd.zone_id,
zrd.domain_category
from zone_resource_domain zrd
</sql>
@ -52,6 +55,7 @@
<include refid="selectZoneResourceDomainAliasVo"/>
<where>
<if test="name != null and name != ''">and zrd.name like concat('%', #{name}, '%')</if>
<if test="domainCategory != null and domainCategory !=''">and zrd.domain_category = #{domainCategory}</if>
<if test="sort != null ">and zrd.sort = #{sort}</if>
<if test="deptId != null ">and zrd.dept_id = #{deptId}</if>
<if test="zoneId != null ">and zrd.zone_id = #{zoneId}</if>
@ -70,6 +74,7 @@
<include refid="selectZoneResourceDomainVo"/>
where name = #{name}
and zone_id = #{zoneId}
and domain_category = #{domainCategory}
</select>
<insert id="insertZoneResourceDomain" parameterType="ZoneResourceDomain" useGeneratedKeys="true" keyProperty="id">
@ -85,6 +90,7 @@
<if test="updateTime != null">update_time,</if>
<if test="zoneId != null">zone_id,</if>
<if test="introduction != null">introduction,</if>
<if test="domainCategory != null">domain_category,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null and name != ''">#{name},</if>
@ -97,6 +103,7 @@
<if test="updateTime != null">#{updateTime},</if>
<if test="zoneId != null">#{zoneId},</if>
<if test="introduction != null">#{introduction},</if>
<if test="domainCategory != null">#{domainCategory},</if>
</trim>
</insert>
@ -113,6 +120,7 @@
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="zoneId != null">zone_id = #{zoneId},</if>
<if test="introduction != null">introduction = #{introduction},</if>
<if test="domainCategory != null">domain_category = #{domainCategory},</if>
</trim>
where id = #{id}
</update>

View File

@ -28,6 +28,11 @@
<result property="keywords" column="keywords"/>
<result property="extendData" column="extend_data"/>
<result property="earnedPoints" column="earned_points"/>
<result property="extendData" column="extend_data"
typeHandler="com.microservices.zone.resource.handler.JsonObjectTypeHandler"/>
<result property="resourceCategory" column="resource_category"/>
<result property="viewCount" column="view_count"/>
<result property="likeCount" column="like_count"/>
<association property="zoneResourceDomain" column="domain_id"
javaType="com.microservices.zone.resource.domain.ZoneResourceDomain"
resultMap="domainResult"/>
@ -67,7 +72,10 @@
update_time,
keywords,
extend_data,
earned_points
earned_points,
resource_category,
view_count,
like_count
from zone_resource
</sql>
@ -93,6 +101,9 @@
zr.remark,
zr.keywords,
zr.extend_data,
zr.resource_category,
zr.view_count,
zr.like_count,
zr.earned_points,
zrd.name as domain_name
from zone_resource zr
@ -123,6 +134,9 @@
zr.keywords,
zr.extend_data,
zr.earned_points,
zr.resource_category,
zr.view_count,
zr.like_count,
zrd.name as domain_name
from zone_resource zr
left join zone_resource_domain zrd on zr.domain_id = zrd.id
@ -137,9 +151,12 @@
<if test="deptId != null ">and zr.dept_id = #{deptId}</if>
<if test="createBy != null and createBy != ''">and zr.create_by = #{createBy}</if>
<if test="keywords != null and keywords != ''">and zr.keywords like concat('%', #{keywords}, '%')</if>
<if test="resourceCategory != null and resourceCategory !=''">and zr.resource_category =
#{resourceCategory}
</if>
<choose>
<when test="auditStatus != null and auditStatus !=''">
and zr.audit_status = #{auditStatus}
<if test="auditStatus != 'all'">and zr.audit_status = #{auditStatus}</if>
</when>
<otherwise>
and zr.audit_status = '1'
@ -150,7 +167,20 @@
</if>
<!-- 数据范围过滤 -->
${params.dataScope}
ORDER BY zr.sort DESC
<choose>
<when test="resourceCategory != null and resourceCategory !=''">
ORDER BY
zr.like_count DESC,
(select sum(download_count) from sys_file_info sf where FIND_IN_SET(sf.file_id,zr.file_ids)) desc,
zr.view_count DESC,
zr.update_time DESC,
zr.create_time DESC
</when>
<otherwise>
ORDER BY zr.sort DESC
</otherwise>
</choose>
</where>
</sql>
@ -166,6 +196,7 @@
<if test="searchTypeId != null ">
and zr.id in (select resource_id from zone_resource_to_type where type_id=#{searchTypeId})
</if>
<if test="resourceCategory != null and resourceCategory !=''">and zr.resource_category = #{resourceCategory}</if>
<choose>
<when test="auditStatus != null and auditStatus !=''">
and zr.audit_status = #{auditStatus}
@ -177,12 +208,17 @@
<!-- 数据范围过滤 -->
${params.dataScope}
<choose>
<when test="orderByDownloadCount != null and orderByDownloadCount">
order by (select sum(download_count) from sys_file_info sf where FIND_IN_SET(sf.file_id,zr.file_ids)) desc
</when>
<when test="orderByDownloadCount = null and orderByColumn = null and orderByColumn =''">
order by zr.sort DESC
<when test="resourceCategory != null and resourceCategory !=''">
ORDER BY
zr.like_count DESC,
(select sum(download_count) from sys_file_info sf where FIND_IN_SET(sf.file_id,zr.file_ids)) desc,
zr.view_count DESC,
zr.update_time DESC,
zr.create_time DESC
</when>
<otherwise>
ORDER BY zr.sort DESC
</otherwise>
</choose>
</where>
</sql>
@ -206,6 +242,7 @@
<include refid="selectZoneResourceVo"/>
where name = #{name}
and zone_id = #{zoneId}
and resource_category = #{resourceCategory}
</select>
<select id="selectZoneResourceCount" resultType="java.lang.Integer">
select count(0)
@ -241,6 +278,9 @@
<if test="remark != null">remark,</if>
<if test="keywords != null">keywords,</if>
<if test="extendData != null">extend_data,</if>
<if test="resourceCategory != null">resource_category,</if>
<if test="viewCount != null">view_count,</if>
<if test="likeCount != null">like_count,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null">#{name},</if>
@ -263,7 +303,10 @@
<if test="auditBy != null">#{auditBy},</if>
<if test="remark != null">#{remark},</if>
<if test="keywords != null">#{keywords},</if>
<if test="extendData != null">#{extendData},</if>
<if test="extendData != null">#{extendData, typeHandler=com.microservices.zone.resource.handler.JsonObjectTypeHandler},</if>
<if test="resourceCategory != null">#{resourceCategory},</if>
<if test="viewCount != null">#{viewCount},</if>
<if test="likeCount != null">#{likeCount},</if>
</trim>
</insert>
@ -291,6 +334,10 @@
<if test="keywords != null">keywords = #{keywords},</if>
<if test="extendData != null">extend_data = #{extendData},</if>
<if test="earnedPoints != null">earned_points = #{earnedPoints},</if>
<if test="extendData != null">extend_data = #{extendData, typeHandler=com.microservices.zone.resource.handler.JsonObjectTypeHandler},</if>
<if test="resourceCategory != null">resource_category = #{resourceCategory},</if>
<if test="viewCount != null">view_count = #{viewCount},</if>
<if test="likeCount != null">like_count = #{likeCount},</if>
</trim>
where id = #{id}
</update>
@ -382,4 +429,27 @@
set earned_points = IFNULL(earned_points, 0) + #{addPoints}
where id = #{id}
</update>
<select id="selectSkillResourceByName" resultType="com.microservices.zone.resource.domain.ZoneResource">
<include refid="selectZoneResourceVo"/>
where name = #{name} and resource_category = 'SKILL' and del_flag = '0'
</select>
<select id="selectSkillRecommendations" resultMap="ZoneResourceResult">
<include refid="selectZoneResourceAliasVo"/>
where zr.resource_category = #{resourceCategory}
and zr.audit_status = #{auditStatus}
and zr.del_flag = '0'
order by (select sum(download_count) from sys_file_info sf where FIND_IN_SET(sf.file_id,zr.file_ids)) desc,
zr.like_count desc, zr.view_count desc
limit #{limit}
</select>
<update id="incrementViewCount">
update zone_resource set view_count = IFNULL(view_count, 0) + 1 where id = #{id}
</update>
<update id="updateLikeCount">
update zone_resource set like_count = IFNULL(like_count, 0) + #{delta} where id = #{id}
</update>
</mapper>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.microservices.zone.resource.mapper.ZoneResourcesLikeMapper">
<resultMap type="com.microservices.zone.resource.domain.ZoneResourcesLike" id="ZoneResourcesLikeResult">
<result property="id" column="id"/>
<result property="resourceId" column="resource_id"/>
<result property="userId" column="user_id"/>
<result property="createTime" column="create_time"/>
</resultMap>
<select id="selectByResourceAndUser"
resultMap="ZoneResourcesLikeResult">
select id, resource_id, user_id, create_time
from zone_resources_like
where resource_id = #{resourceId} and user_id = #{userId}
</select>
<select id="selectByResourceIds"
resultMap="ZoneResourcesLikeResult">
select id, resource_id, user_id, create_time
from zone_resources_like
where resource_id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<insert id="insertLike" useGeneratedKeys="true" keyProperty="id">
insert into zone_resources_like (resource_id, user_id, create_time)
values (#{resourceId}, #{userId}, #{createTime})
</insert>
<delete id="deleteLike">
delete from zone_resources_like
where resource_id = #{resourceId} and user_id = #{userId}
</delete>
</mapper>

View File

@ -27,7 +27,7 @@
<pagehelper.boot.version>1.4.6</pagehelper.boot.version>
<druid.version>1.2.16</druid.version>
<dynamic-ds.version>3.5.2</dynamic-ds.version>
<commons.io.version>2.11.0</commons.io.version>
<commons.io.version>2.15.1</commons.io.version>
<velocity.version>2.3</velocity.version>
<fastjson.version>2.0.42</fastjson.version>
<jjwt.version>0.9.1</jjwt.version>