Compare commits

...

13 Commits

Author SHA1 Message Date
otto 57dad99399 Merge pull request '修复Skill无法下载的问题' (#1103) from otto/microservices:aiResource into aiResource 2026-05-08 13:59:36 +08:00
欧涛 965cedc275 feat(AI资源广场): Skill资源相关功能开发
修复Skill无法下载的问题
2026-05-08 13:59:16 +08:00
otto b26825084b Merge pull request 'feat(AI资源广场): Skill资源相关功能开发' (#1100) from otto/microservices:aiResource into aiResource 2026-05-07 10:37:19 +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
43 changed files with 2641 additions and 26 deletions

View File

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

View File

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

View File

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

View File

@ -271,9 +271,9 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
@Override @Override
public void fileDownloadById(Long fileId, HttpServletResponse response, HttpServletRequest request) { public void fileDownloadById(Long fileId, HttpServletResponse response, HttpServletRequest request) {
SysFileInfo sysFileInfo = selectSysFileInfoByFileId(fileId); SysFileInfo sysFileInfo = selectSysFileInfoByFileId(fileId);
if (StringUtils.isNotEmpty(sysFileInfo.getFileIdentifier())) { // if (StringUtils.isNotEmpty(sysFileInfo.getFileIdentifier())) {
throw new ServiceException("当前文件无法下载"); // throw new ServiceException("当前文件无法下载");
} // }
fileDownload(sysFileInfo, response); fileDownload(sysFileInfo, response);
} }
@ -397,6 +397,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
hierarchy = validPathSafety(hierarchy); hierarchy = validPathSafety(hierarchy);
} }
boolean isLocalUpload = true; boolean isLocalUpload = true;
boolean isHierarchyPath = false;
Long fileId = null; Long fileId = null;
switch (fileType) { switch (fileType) {
case "pms-gitlink": case "pms-gitlink":
@ -431,6 +432,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
sysFileInfo.setFileIdentifier(genFileIdentifier()); sysFileInfo.setFileIdentifier(genFileIdentifier());
baseFilePath += "/" + fileType + "/" + hierarchy + "/"; baseFilePath += "/" + fileType + "/" + hierarchy + "/";
isCommonFileType = false; isCommonFileType = false;
isHierarchyPath = true;
break; break;
} }
case "dms": { case "dms": {
@ -445,6 +447,13 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
isCommonFileType = false; isCommonFileType = false;
break; break;
} }
case "ai-resource": {
sysFileInfo.setFileIdentifier(genFileIdentifier());
baseFilePath += "/" + fileType + "/" + hierarchy + "/";
isCommonFileType = false;
isHierarchyPath = true;
break;
}
case "zone-identifier": { case "zone-identifier": {
// 生成唯一标识防止用户通过id递增访问 // 生成唯一标识防止用户通过id递增访问
sysFileInfo.setFileIdentifier(genFileIdentifier()); sysFileInfo.setFileIdentifier(genFileIdentifier());
@ -507,7 +516,7 @@ public class SysFileInfoServiceImpl implements ISysFileInfoService {
sysFileInfo.setFileOriginName(session.getFileName()); sysFileInfo.setFileOriginName(session.getFileName());
} }
if (!isCommonFileType) { if (!isCommonFileType) {
if ("pms".equals(fileType)) { if (isHierarchyPath) {
filePath = EscapeUtil.removeExtraSlashOfUrl("/" + fileType + "/" + hierarchy + "/" + filePath); filePath = EscapeUtil.removeExtraSlashOfUrl("/" + fileType + "/" + hierarchy + "/" + filePath);
} else { } else {
filePath = EscapeUtil.removeExtraSlashOfUrl("/" + fileType + "/" + filePath); filePath = EscapeUtil.removeExtraSlashOfUrl("/" + fileType + "/" + filePath);

View File

@ -11,15 +11,14 @@ import com.microservices.common.security.utils.SecurityUtils;
import com.microservices.zone.detail.domain.vo.ZoneFrontRoleVo; import com.microservices.zone.detail.domain.vo.ZoneFrontRoleVo;
import com.microservices.zone.detail.service.IZoneDetailService; import com.microservices.zone.detail.service.IZoneDetailService;
import com.microservices.zone.resource.domain.ZoneResource; import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo; import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.domain.vo.ZoneResourceInputVo; import com.microservices.zone.resource.service.IAiSkillParseService;
import com.microservices.zone.resource.domain.vo.ZoneResourceSearchVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceUpdateVo;
import com.microservices.zone.resource.service.IZoneResourceService; import com.microservices.zone.resource.service.IZoneResourceService;
import io.swagger.annotations.Api; import io.swagger.annotations.*;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/** /**
* @author otto * @author otto
@ -32,6 +31,9 @@ public class FrontController extends BaseController {
@Autowired @Autowired
private IZoneResourceService zoneResourceService; private IZoneResourceService zoneResourceService;
@Autowired
private IAiSkillParseService aiSkillParseService;
@Autowired @Autowired
private IZoneDetailService zoneDetailService; private IZoneDetailService zoneDetailService;
@ -103,4 +105,79 @@ public class FrontController extends BaseController {
public GenericsAjaxResult<ZoneFrontRoleVo> checkCurrentRole(@PathVariable Long zoneId) { public GenericsAjaxResult<ZoneFrontRoleVo> checkCurrentRole(@PathVariable Long zoneId) {
return genericsSuccess(zoneDetailService.checkCurrentRoleInZone(zoneId)); return genericsSuccess(zoneDetailService.checkCurrentRoleInZone(zoneId));
} }
// ==================== 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(aiSkillParseService.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));
}
/**
* 重新提交被驳回的Skill资源
*/
// @RequiresPermissions("zone:front:skill:add")
@Log(title = "重新提交Skill资源", businessType = BusinessType.UPDATE)
@PutMapping("/skillResource/{id}/resubmit")
@ApiOperation(value = "重新提交被驳回的资源")
public AjaxResult resubmitAiResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id) {
return toAjax(zoneResourceService.resubmitAiResource(id));
}
/**
* 撤回审核中的Skill资源
*/
// @RequiresPermissions("zone:front:skill:edit")
@Log(title = "撤回Skill资源", businessType = BusinessType.UPDATE)
@PutMapping("/skillResource/{id}/withdraw")
@ApiOperation(value = "撤回审核中的资源")
public AjaxResult withdrawAiResource(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id) {
return toAjax(zoneResourceService.withdrawAiResource(id));
}
/**
* 下架资源
*/
// @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));
}
/**
* 点赞/取消点赞
*/
// @RequiresPermissions("ai:resource:skill:query")
@PostMapping("/{id}/like")
@ApiOperation("点赞/取消点赞AI资源")
public AjaxResult toggleLike(
@ApiParam(value = "资源ID", required = true) @PathVariable("id") Long id) {
boolean liked = zoneResourceService.toggleAiResourceLike(id);
return success(liked ? "已点赞" : "已取消点赞");
}
} }

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.ZoneBaseDataVo;
import com.microservices.zone.detail.domain.vo.ZoneDetailDataVo; import com.microservices.zone.detail.domain.vo.ZoneDetailDataVo;
import com.microservices.zone.detail.service.IZoneDetailService; import com.microservices.zone.detail.service.IZoneDetailService;
import com.microservices.zone.journals.domain.ZoneJournals;
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.ZoneMemberDataVo;
import com.microservices.zone.member.domain.vo.ZoneMemberOpenSearchVo; import com.microservices.zone.member.domain.vo.ZoneMemberOpenSearchVo;
import com.microservices.zone.member.domain.vo.ZoneMemberOverviewVo; import com.microservices.zone.member.domain.vo.ZoneMemberOverviewVo;
@ -34,6 +37,7 @@ import com.microservices.zone.resource.domain.ZoneResourceDomain;
import com.microservices.zone.resource.domain.ZoneResourceType; import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo; import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo;
import com.microservices.zone.resource.domain.vo.ZoneResourceOpenSearchVo; import com.microservices.zone.resource.domain.vo.ZoneResourceOpenSearchVo;
import com.microservices.zone.resource.enums.AiResourceType;
import com.microservices.zone.resource.service.IZoneResourceDomainService; import com.microservices.zone.resource.service.IZoneResourceDomainService;
import com.microservices.zone.resource.service.IZoneResourceService; import com.microservices.zone.resource.service.IZoneResourceService;
import com.microservices.zone.resource.service.IZoneResourceTypeService; import com.microservices.zone.resource.service.IZoneResourceTypeService;
@ -241,7 +245,7 @@ public class OpenController extends BaseController {
@GetMapping("/{zoneId}/member/homePageList") @GetMapping("/{zoneId}/member/homePageList")
@ApiOperation("获取首页特色专区会员列表") @ApiOperation("获取首页特色专区会员列表")
public GenericsTableDataInfo<ZoneMemberDataVo> homePageMemberList(@ApiParam(name = "zoneId", value = "专区Id", required = true) @PathVariable("zoneId") Long zoneId) { public GenericsTableDataInfo<ZoneMemberDataVo> homePageMemberList(@ApiParam(name = "zoneId", value = "专区Id", required = true) @PathVariable("zoneId") Long zoneId) {
List<ZoneMemberDataVo> list = zoneMemberService.selectZoneMemberHomePageMemberList(zoneId,true); List<ZoneMemberDataVo> list = zoneMemberService.selectZoneMemberHomePageMemberList(zoneId, true);
return getGenericsDataTable(list); return getGenericsDataTable(list);
} }
@ -411,4 +415,67 @@ public class OpenController extends BaseController {
List<ZoneSpecialProjectReport> list = zoneSpecialProjectReportService.selectZoneSpecialProjectReportList(zoneSpecialProjectReportSearchVo.toZoneSpecialProjectReport()); List<ZoneSpecialProjectReport> list = zoneSpecialProjectReportService.selectZoneSpecialProjectReportList(zoneSpecialProjectReportSearchVo.toZoneSpecialProjectReport());
return getGenericsDataTable(list); return getGenericsDataTable(list);
} }
// ==================== AI资源广场 - 开放接口 ====================
/**
* 广场首页-Skill推荐列表前4名
*/
@GetMapping("/ai-square/skills")
@ApiOperation("广场首页-Skill推荐列表")
public GenericsAjaxResult<List<ZoneResourceDataVo>> skillRecommendations() {
return genericsSuccess(zoneResourceService.selectSkillRecommendations(4));
}
/**
* 公开-Skill详情
*/
@GetMapping("/ai-square/skill/{id}")
@ApiOperation("公开-Skill详情")
public GenericsAjaxResult<ZoneResourceDataVo> skillDetail(
@ApiParam(name = "id", value = "资源ID", required = true) @PathVariable("id") Long id) {
// 增加浏览次数
zoneResourceService.incrementViewCount(id);
return genericsSuccess(zoneResourceService.selectAiSkillResourceById(id));
}
/**
* 公开-Skill列表搜索
*/
@GetMapping("/ai-square/skill/list")
@ApiOperation("公开-Skill列表搜索")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageNum", value = "当前记录起始索引", paramType = "query", dataType = "Integer"),
@ApiImplicitParam(name = "pageSize", value = "每页显示记录数", paramType = "query", dataType = "Integer")
})
public GenericsTableDataInfo<ZoneResourceDataVo> skillList(ZoneResource zoneResource) {
// 公开接口只查询已发布的
zoneResource.setAuditStatus("1");
zoneResource.setResourceCategory(AiResourceType.SKILL.getCode());
startPage();
return zoneResourceService.selectAiSkillResourceList(zoneResource);
}
// ==================== 通用评论 - 开放接口 ====================
@Autowired
private com.microservices.zone.journals.service.IZoneJournalsService zoneJournalsService;
/**
* 公开-资源评论列表
*/
@GetMapping("/ai-square/{resourceId}/journals")
@ApiOperation("公开-资源评论列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "pageNum", value = "当前记录起始索引", paramType = "query", dataType = "Integer"),
@ApiImplicitParam(name = "pageSize", value = "每页显示记录数", paramType = "query", dataType = "Integer")
})
public GenericsTableDataInfo<ZoneJournalsParentDataVo> journalsList(
@ApiParam(name = "resourceId", value = "资源ID", required = true) @PathVariable("resourceId") Long resourceId,
ZoneJournalsSearchVo searchVo) {
searchVo.setJournalized_id(resourceId);
searchVo.setJournalized_type(ZoneJournals.JOURNAL_TYPE_AI_SKILL);
startPage();
return new GenericsTableDataInfo<>(zoneJournalsService.selectJournalsList(searchVo));
}
} }

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("/zone/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,91 @@
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_AI_SKILL = "aiSkill";
public static final String JOURNAL_TYPE_AI_MODEL = "aiModel";
public static final String JOURNAL_TYPE_AI_DATASET = "aiDataset";
public static final String JOURNAL_TYPE_ZONE_RESOURCE = "zoneResource";
/** 评论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 privateNotes;
/** 删除标志 */
@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,56 @@
package com.microservices.zone.journals.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
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<SysFileInfo> attachments;
/** 评论数量 */
@ApiModelProperty(value = "评论数量")
private Integer comments_count = 0;
/** 是否详情 */
@ApiModelProperty(value = "是否详情")
private Boolean is_journal_detail = false;
}

View File

@ -0,0 +1,56 @@
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 = "评论对象类型不能为空")
@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;
/** 是否隐私评论 */
@ApiModelProperty(value = "是否隐私评论")
private String private_notes;
}

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,32 @@
package com.microservices.zone.journals.domain.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* 通用评论查询VO
*
* @author otto
* @date 2025-04-30
*/
@Data
@ApiModel("评论查询对象")
public class ZoneJournalsSearchVo {
/** 评论对象ID */
@ApiModelProperty(value = "评论对象ID")
private Long journalized_id;
/** 评论对象类型 */
@ApiModelProperty(value = "评论对象类型")
private String journalized_type;
/** 评论用户ID */
@ApiModelProperty(value = "评论用户ID")
private Long user_id;
/** 是否隐私评论 */
@ApiModelProperty(value = "是否隐私评论")
private String private_notes;
}

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,414 @@
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.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());
query.setUserId(searchVo.getUser_id());
query.setPrivateNotes(searchVo.getPrivate_notes());
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.setPrivateNotes(StringUtils.isEmpty(inputVo.getPrivate_notes()) ? "0" : inputVo.getPrivate_notes());
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) {
switch (zoneJournals.getJournalizedType()) {
case ZoneJournals.JOURNAL_TYPE_AI_SKILL:
case ZoneJournals.JOURNAL_TYPE_AI_MODEL:
case ZoneJournals.JOURNAL_TYPE_AI_DATASET:
case ZoneJournals.JOURNAL_TYPE_ZONE_RESOURCE:
// 校验资源是否存在
zoneResourceService.selectZoneResourceById(zoneJournals.getJournalizedId());
break;
default:
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<SysFileInfo> 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;
} catch (Exception e) {
return new ArrayList<>();
}
}
}

View File

@ -0,0 +1,69 @@
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.IAiSkillParseService;
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;
import java.util.List;
/**
* AI资源管理Controller
*/
@RestController
@RequestMapping("/ai-resource")
@Api(tags = "AI资源管理接口")
public class AiResourceController extends BaseController {
@Autowired
private IZoneResourceService zoneResourceService;
@Autowired
private IAiSkillParseService aiSkillParseService;
/**
* 上传ZIP并解析
*/
// @RequiresPermissions("ai:resource: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(aiSkillParseService.parseSkillZip(file));
}
/**
* 创建Skill资源
*/
// @RequiresPermissions("ai:resource: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("ai:resource:skill:edit")
@Log(title = "修改Skill资源", businessType = BusinessType.UPDATE)
@PutMapping("/skill")
@ApiOperation("修改Skill资源")
public AjaxResult editSkill(@RequestBody @Validated AiSkillUpdateVo updateVo) {
return success(zoneResourceService.updateAiSkillResource(updateVo));
}
}

View File

@ -12,6 +12,7 @@ import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.service.IZoneResourceService; import com.microservices.zone.resource.service.IZoneResourceService;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -106,4 +107,17 @@ public class ZoneResourceController extends BaseController {
public AjaxResult remove(@PathVariable Long[] ids) { public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(zoneResourceService.deleteZoneResourceByIds(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

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

View File

@ -1,9 +1,11 @@
package com.microservices.zone.resource.domain; package com.microservices.zone.resource.domain;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.annotation.Excel; import com.microservices.common.core.annotation.Excel;
import com.microservices.common.core.utils.bean.BeanUtils; import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.detail.domain.ZoneSort; import com.microservices.zone.detail.domain.ZoneSort;
import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo; import com.microservices.zone.resource.domain.vo.ZoneResourceDataVo;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
@ -93,8 +95,26 @@ public class ZoneResource extends ZoneSort {
@ApiModelProperty(value = "资源标签") @ApiModelProperty(value = "资源标签")
private String keywords; private String keywords;
@ApiModelProperty(value = "拓展数据") @ApiModelProperty(value = "拓展数据JSON格式")
private String extendData; private JSONObject extendData;
/**
* 资源分类SKILL/MODEL/DATASETNULL表示原有专区资源
*/
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASET")
private String resourceCategory;
/**
* 浏览次数
*/
@ApiModelProperty(value = "浏览次数")
private Integer viewCount;
/**
* 点赞次数
*/
@ApiModelProperty(value = "点赞次数")
private Integer likeCount;
public ZoneResourceDataVo toZoneResourceDataVo() { public ZoneResourceDataVo toZoneResourceDataVo() {
ZoneResourceDataVo target = new ZoneResourceDataVo(); ZoneResourceDataVo target = new ZoneResourceDataVo();
@ -105,4 +125,34 @@ public class ZoneResource extends ZoneSort {
return target; 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

@ -0,0 +1,68 @@
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.enums.AiResourceType;
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简介", required = true)
@NotBlank(message = "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(AiResourceType.SKILL.getCode());
return target;
}
}

View File

@ -0,0 +1,26 @@
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;
}

View File

@ -0,0 +1,78 @@
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;
/**
* 转换为 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

@ -1,8 +1,10 @@
package com.microservices.zone.resource.domain.vo; package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import com.microservices.system.api.domain.SimpleSysUser; import com.microservices.system.api.domain.SimpleSysUser;
import com.microservices.zone.resource.domain.ZoneResourceType; import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.utils.ResourceExtendDataUtils;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty; import io.swagger.annotations.ApiModelProperty;
import lombok.Data; import lombok.Data;
@ -73,6 +75,18 @@ public class ZoneResourceDataVo {
private Date auditTime; private Date auditTime;
@ApiModelProperty(value = "审核人") @ApiModelProperty(value = "审核人")
private String auditBy; private String auditBy;
@ApiModelProperty(value = "拓展数据") @ApiModelProperty(value = "拓展数据JSON格式")
private String extendData; private JSONObject extendData;
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASET")
private String resourceCategory;
@ApiModelProperty(value = "审核意见/驳回原因")
private String auditReason;
@ApiModelProperty(value = "浏览次数")
private Integer viewCount;
@ApiModelProperty(value = "点赞次数")
private Integer likeCount;
} }

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.domain.vo; package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.bean.BeanUtils; import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.resource.domain.ZoneResource; import com.microservices.zone.resource.domain.ZoneResource;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
@ -33,6 +34,11 @@ public class ZoneResourceInputVo {
@ApiModelProperty(value = "所属专区", required = true) @ApiModelProperty(value = "所属专区", required = true)
@NotNull @NotNull
private Long zoneId; private Long zoneId;
/**
* 资源分类SKILL/MODEL/DATASETNULL表示原有专区资源
*/
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASET", hidden = true)
private String resourceCategory = null;
@ApiModelProperty(value = "拓展数据") @ApiModelProperty(value = "拓展数据")
private String extendData; private String extendData;
@ -40,6 +46,7 @@ public class ZoneResourceInputVo {
public ZoneResource toZoneResource() { public ZoneResource toZoneResource() {
ZoneResource target = new ZoneResource(); ZoneResource target = new ZoneResource();
BeanUtils.copyProperties(this, target); BeanUtils.copyProperties(this, target);
target.setExtendData(JSONObject.parseObject(extendData));
return target; return target;
} }
} }

View File

@ -53,7 +53,7 @@ public class ZoneResourceOpenSearchVo extends BaseEntity {
@ApiModelProperty(value = "是否根据下载次数降序排序与orderByColumn互斥该属性优先级更高") @ApiModelProperty(value = "是否根据下载次数降序排序与orderByColumn互斥该属性优先级更高")
private Boolean orderByDownloadCount = false; private Boolean orderByDownloadCount = false;
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过") @ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过 3已下架")
private String auditStatus; private String auditStatus;
public ZoneResource toZoneResource() { public ZoneResource toZoneResource() {

View File

@ -17,6 +17,11 @@ public class ZoneResourceSearchVo extends ZoneResourceOpenSearchVo {
@ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过") @ApiModelProperty(value = "审核状态0待审核1审核通过 2审核不通过")
private String auditStatus; private String auditStatus;
/**
* 资源分类SKILL/MODEL/DATASETNULL表示原有专区资源
*/
@ApiModelProperty(value = "资源分类SKILL/MODEL/DATASET")
private String resourceCategory;
public ZoneResource toZoneResource() { public ZoneResource toZoneResource() {
ZoneResource zoneResource = super.toZoneResource(); ZoneResource zoneResource = super.toZoneResource();

View File

@ -1,5 +1,6 @@
package com.microservices.zone.resource.domain.vo; package com.microservices.zone.resource.domain.vo;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.bean.BeanUtils; import com.microservices.common.core.utils.bean.BeanUtils;
import com.microservices.zone.resource.domain.ZoneResource; import com.microservices.zone.resource.domain.ZoneResource;
import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModel;
@ -44,6 +45,7 @@ public class ZoneResourceUpdateVo {
public ZoneResource toZoneResource() { public ZoneResource toZoneResource() {
ZoneResource target = new ZoneResource(); ZoneResource target = new ZoneResource();
BeanUtils.copyProperties(this, target); BeanUtils.copyProperties(this, target);
target.setExtendData(JSONObject.parseObject(extendData));
return target; return target;
} }
} }

View File

@ -0,0 +1,25 @@
package com.microservices.zone.resource.enums;
/**
* AI资源类型枚举
*/
public enum AiResourceType {
SKILL("SKILL", "Skill技能");
private final String code;
private final String desc;
AiResourceType(String code, String desc) {
this.code = code;
this.desc = desc;
}
public String getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

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

@ -0,0 +1,38 @@
package com.microservices.zone.resource.mapper;
import com.microservices.zone.resource.domain.AiResourceLike;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* AI资源点赞Mapper接口
*/
@Mapper
public interface AiResourceLikeMapper {
/**
* 查询用户对指定资源的点赞记录
*
* @param resourceId 资源ID
* @param userId 用户ID
* @return 点赞记录
*/
AiResourceLike selectByResourceAndUser(@Param("resourceId") Long resourceId, @Param("userId") Long userId);
/**
* 新增点赞记录
*
* @param aiResourceLike 点赞记录
* @return 影响行数
*/
int insertLike(AiResourceLike aiResourceLike);
/**
* 删除点赞记录
*
* @param resourceId 资源ID
* @param userId 用户ID
* @return 影响行数
*/
int deleteLike(@Param("resourceId") Long resourceId, @Param("userId") Long userId);
}

View File

@ -95,4 +95,39 @@ public interface ZoneResourceMapper {
* @param zoneId 专区Id * @param zoneId 专区Id
*/ */
void deleteZoneResourceByZoneId(Long zoneId); void deleteZoneResourceByZoneId(Long zoneId);
/**
* 通过名称查询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);
} }

View File

@ -0,0 +1,21 @@
package com.microservices.zone.resource.service;
import com.microservices.zone.resource.domain.vo.AiSkillParseResultVo;
import org.springframework.web.multipart.MultipartFile;
/**
* Skill ZIP解析服务接口
*/
public interface IAiSkillParseService {
/**
* 解析Skill ZIP包
* 1. 校验ZIP内必须包含 SKILL.md
* 2. 提取 SKILL.md 内容
* 3. 生成目录树结构 JSON
*
* @param zipFile 上传的ZIP文件
* @return 解析结果
*/
AiSkillParseResultVo parseSkillZip(MultipartFile zipFile);
}

View File

@ -138,4 +138,84 @@ public interface IZoneResourceService {
* @return * @return
*/ */
Integer selectMyZoneResourceCount(ZoneResource zoneResourceSearch); 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 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);
/**
* AI资源点赞/取消点赞
*
* @param resourceId 资源ID
* @return true-已点赞, false-已取消
*/
boolean toggleAiResourceLike(Long resourceId);
/**
* 增加AI资源浏览次数
*
* @param id 资源ID
*/
void incrementViewCount(Long id);
} }

View File

@ -0,0 +1,241 @@
package com.microservices.zone.resource.service.impl;
import com.alibaba.fastjson2.JSON;
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.utils.FeignUtils;
import com.microservices.zone.resource.domain.vo.AiSkillParseResultVo;
import com.microservices.zone.resource.service.IAiSkillParseService;
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 java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Skill ZIP解析服务实现
*/
@Service
public class AiSkillParseServiceImpl implements IAiSkillParseService {
private static final Logger logger = LoggerFactory.getLogger(AiSkillParseServiceImpl.class);
private static final String SKILL_MD_FILE = "SKILL.md";
private final RemoteFileService remoteFileService;
public AiSkillParseServiceImpl(RemoteFileService remoteFileService) {
this.remoteFileService = remoteFileService;
}
@Override
public AiSkillParseResultVo parseSkillZip(MultipartFile zipFile) {
// 1. 校验文件大小
if (zipFile.getSize() > ZoneConstants.SKILL_ZIP_MAX_SIZE) {
throw new ServiceException("ZIP包大小不能超过10MB");
}
// 2. 校验文件格式
String originalFilename = zipFile.getOriginalFilename();
if (originalFilename == null || !originalFilename.toLowerCase().endsWith(".zip")) {
throw new ServiceException("仅支持ZIP格式文件");
}
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream(), StandardCharsets.UTF_8)) {
ZipEntry entry;
String skillMdContent = null;
List<FileTreeNode> fileTreeNodes = new ArrayList<>();
while ((entry = zis.getNextEntry()) != null) {
String entryName = entry.getName();
if (entry.isDirectory()) {
continue;
}
// 防止路径遍历攻击
if (entryName.contains("..")) {
throw new ServiceException("ZIP包中包含非法路径");
}
// 提取 SKILL.md在任意层级下查找
String fileName = new File(entryName).getName();
if (SKILL_MD_FILE.equalsIgnoreCase(fileName)) {
skillMdContent = readEntryContent(zis);
}
// 收集文件信息构建目录树
fileTreeNodes.add(new FileTreeNode(entryName, entry.getSize()));
}
// 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();
result.setSkillMdContent(skillMdContent);
result.setFileTreeJson(fileTreeJson.toJSONString());
// YAML front matter 中提取 description 字段
String description = extractDescription(skillMdContent);
result.setSummary(description);
// 解析成功后自动上传ZIP文件到File微服务上传失败则阻断整个接口
SysFile sysFile = FeignUtils.getReturnData(
remoteFileService.upload(zipFile, "ai-resource", "skill", SecurityConstants.INNER)
);
if (sysFile == null || StringUtils.isEmpty(sysFile.getFileIdentifier())) {
throw new ServiceException("文件上传返回为空,请重试");
}
result.setFileId(sysFile.getFileId());
return result;
} catch (ServiceException e) {
throw e;
} catch (IOException e) {
logger.error("ZIP包解析失败", e);
throw new ServiceException("ZIP包解析失败" + e.getMessage());
}
}
private String readEntryContent(ZipInputStream zis) throws IOException {
StringBuilder sb = new StringBuilder();
byte[] buffer = new byte[4096];
int len;
while ((len = zis.read(buffer)) > 0) {
sb.append(new String(buffer, 0, len, StandardCharsets.UTF_8));
}
return sb.toString();
}
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 ArrayList<JSONObject>());
currentChildren.add(existingFolder);
}
currentChildren = existingFolder.getJSONArray("children");
}
}
}
// 如果根目录只有一个子文件夹直接返回该子文件夹
JSONArray children = root.getJSONArray("children");
if (children.size() == 1 && "folder".equals(children.getJSONObject(0).get("type"))) {
return children.getJSONObject(0);
}
return root;
}
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;
}
// 查找 YAML front matter 边界
String[] lines = skillMdContent.split("\\n");
boolean inFrontMatter = false;
boolean foundStart = false;
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 desc = trimmed.substring("description:".length()).trim();
// 处理引号包裹的情况
if (desc.startsWith("\"") && desc.endsWith("\"")) {
return desc.substring(1, desc.length() - 1);
}
if (desc.startsWith("'") && desc.endsWith("'")) {
return desc.substring(1, desc.length() - 1);
}
return desc;
}
}
return null;
}
}

View File

@ -24,9 +24,12 @@ import com.microservices.zone.detail.service.IZoneDetailService;
import com.microservices.zone.resource.domain.ZoneResource; import com.microservices.zone.resource.domain.ZoneResource;
import com.microservices.zone.resource.domain.ZoneResourceToType; import com.microservices.zone.resource.domain.ZoneResourceToType;
import com.microservices.zone.resource.domain.ZoneResourceType; import com.microservices.zone.resource.domain.ZoneResourceType;
import com.microservices.zone.resource.domain.AiResourceLike;
import com.microservices.zone.resource.domain.vo.*; import com.microservices.zone.resource.domain.vo.*;
import com.microservices.zone.resource.enums.AiResourceType;
import com.microservices.zone.resource.mapper.ZoneResourceMapper; import com.microservices.zone.resource.mapper.ZoneResourceMapper;
import com.microservices.zone.resource.mapper.ZoneResourceToTypeMapper; import com.microservices.zone.resource.mapper.ZoneResourceToTypeMapper;
import com.microservices.zone.resource.mapper.AiResourceLikeMapper;
import com.microservices.zone.resource.service.IZoneResourceDomainService; import com.microservices.zone.resource.service.IZoneResourceDomainService;
import com.microservices.zone.resource.service.IZoneResourceService; import com.microservices.zone.resource.service.IZoneResourceService;
import com.microservices.zone.resource.service.IZoneResourceTypeService; import com.microservices.zone.resource.service.IZoneResourceTypeService;
@ -37,6 +40,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.List; import java.util.List;
@ -72,6 +76,8 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
@Autowired @Autowired
@Lazy @Lazy
private IZoneSpecialProjectRelevancyService zoneSpecialProjectRelevancyService; private IZoneSpecialProjectRelevancyService zoneSpecialProjectRelevancyService;
@Autowired
private AiResourceLikeMapper aiResourceLikeMapper;
/** /**
* 查询特色专区资源聚合 * 查询特色专区资源聚合
@ -155,9 +161,7 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
if (1 == zoneDetail.getResourceNeedAudit()) { if (1 == zoneDetail.getResourceNeedAudit()) {
boolean isOperator = AuthUtil.hasUserIdentity(zoneDetail.getDeptId(), SystemRole.ZONE_OPERATOR.getRoleKey()); boolean isOperator = AuthUtil.hasUserIdentity(zoneDetail.getDeptId(), SystemRole.ZONE_OPERATOR.getRoleKey());
boolean isMember = AuthUtil.hasUserIdentity(zoneDetail.getDeptId(), SystemRole.ZONE_MEMBER.getRoleKey()); boolean isMember = AuthUtil.hasUserIdentity(zoneDetail.getDeptId(), SystemRole.ZONE_MEMBER.getRoleKey());
if (isOperator || isMember) { return isOperator || isMember;
return true;
}
} }
return false; return false;
} }
@ -224,7 +228,7 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
); );
} catch (Exception e) { } catch (Exception e) {
logger.error("从文件微服务获取资源附件时失败:{}", e.getMessage()); logger.error("从文件微服务获取资源附件时失败:{}", e.getMessage());
throw new ServiceException("资源附件列表异常:"+e.getMessage()); throw new ServiceException("资源附件列表异常:" + e.getMessage());
} }
return sysFileInfoList; return sysFileInfoList;
} }
@ -410,4 +414,172 @@ public class ZoneResourceServiceImpl implements IZoneResourceService {
ZoneResource zoneResource = selectZoneResourceById(id); ZoneResource zoneResource = selectZoneResourceById(id);
return zoneResourceToData(zoneResource); return zoneResourceToData(zoneResource);
} }
// ==================== AI资源广场 Skill 功能 ====================
@Override
public ZoneResourceDataVo insertAiSkillResource(AiSkillInputVo inputVo) {
ZoneResourceInputVo zoneResourceInputVo = inputVo.toZoneResourceInputVo();
return insertZoneResource(zoneResourceInputVo);
}
@Override
public ZoneResourceDataVo updateAiSkillResource(AiSkillUpdateVo updateVo) {
ZoneResource oldResource = getAiResourceById(updateVo.getId());
if (!AiResourceType.SKILL.getCode().equals(oldResource.getResourceCategory())) {
throw new ServiceException("当前资源非Skill资源");
}
// 已发布状态不允许直接修改
if (ZoneConstants.RESOURCE_AUDIT_PASS.equals(oldResource.getAuditStatus())) {
throw new ServiceException("已发布的资源不允许直接修改,请先下架后再修改");
}
ZoneResourceUpdateVo zoneResourceUpdateVo = updateVo.toZoneResourceUpdateVo(oldResource);
return updateZoneResource(zoneResourceUpdateVo);
}
@Override
public ZoneResourceDataVo selectAiSkillResourceById(Long id) {
ZoneResource resource = getAiResourceById(id);
return zoneResourceToData(resource);
}
@Override
public GenericsTableDataInfo<ZoneResourceDataVo> selectAiSkillResourceList(ZoneResource zoneResource) {
zoneResource.setResourceCategory(AiResourceType.SKILL.getCode());
List<ZoneResource> list = zoneResourceMapper.selectZoneResourceList(zoneResource);
return PageUtils.toPage(list, this::zoneResourceToData);
}
@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);
resource.setRemark(reason);
resource.setUpdateTime(DateUtils.getNowDate());
resource.setUpdateBy(SecurityUtils.getUsername());
return zoneResourceMapper.updateZoneResource(resource);
}
@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())) {
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_AUDIT_FAILED);
resource.setUpdateTime(DateUtils.getNowDate());
return zoneResourceMapper.updateZoneResource(resource);
}
@Override
public List<ZoneResourceDataVo> selectSkillRecommendations(int limit) {
List<ZoneResource> list = zoneResourceMapper.selectSkillRecommendations(
AiResourceType.SKILL.getCode(), ZoneConstants.RESOURCE_AUDIT_PASS, limit);
return list.stream().map(this::zoneResourceToData).collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean toggleAiResourceLike(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("当前用户未绑定平台账号,无法点赞");
}
AiResourceLike existingLike = aiResourceLikeMapper.selectByResourceAndUser(resourceId, userId);
if (existingLike != null) {
// 取消点赞
aiResourceLikeMapper.deleteLike(resourceId, userId);
zoneResourceMapper.updateLikeCount(resourceId, -1);
return false;
} else {
// 新增点赞
AiResourceLike like = new AiResourceLike();
like.setResourceId(resourceId);
like.setUserId(userId);
like.setCreateTime(DateUtils.getNowDate());
aiResourceLikeMapper.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 (!AiResourceType.SKILL.getCode().equals(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 + "个字符");
}
}
}
} }

View File

@ -0,0 +1,99 @@
package com.microservices.zone.resource.utils;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.utils.StringUtils;
/**
* 资源 extendData 工具类
* 用于处理不同资源类型的扩展数据
*/
public class ResourceExtendDataUtils {
private static final String SKILL_ROOT = "skill";
private static final String SKILL_CONTENT = "content";
private static final String SKILL_FILE_TREE = "fileTree";
/**
* 构建 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(SKILL_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(SKILL_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(SKILL_FILE_TREE, fileTreeJson);
return extendData;
}
}

View File

@ -66,6 +66,10 @@ public class ZoneConstants {
* 资源审核状态-待审核 * 资源审核状态-待审核
*/ */
public final static String RESOURCE_NOT_AUDIT = "0"; public final static String RESOURCE_NOT_AUDIT = "0";
/**
* 资源审核状态-已下架
*/
public final static String RESOURCE_TAKEN_DOWN = "3";
/** /**
* 前台权限-管理者 * 前台权限-管理者
*/ */
@ -82,4 +86,24 @@ public class ZoneConstants {
public static String getFileSuffixOfResourceTypeRedisKey(Long zoneId, String fileSuffix) { public static String getFileSuffixOfResourceTypeRedisKey(Long zoneId, String fileSuffix) {
return REDIS_KEY_FILE_SUFFIX_OF_RESOURCE_TYPE_PREFIX + zoneId + "_" + fileSuffix; return REDIS_KEY_FILE_SUFFIX_OF_RESOURCE_TYPE_PREFIX + zoneId + "_" + fileSuffix;
} }
/**
* Skill ZIP文件最大大小10MB
*/
public final static long SKILL_ZIP_MAX_SIZE = 10 * 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,23 @@
<?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.AiResourceLikeMapper">
<select id="selectByResourceAndUser" resultType="com.microservices.zone.resource.domain.AiResourceLike">
select id, resource_id, user_id, create_time
from ai_resource_like
where resource_id = #{resourceId} and user_id = #{userId}
</select>
<insert id="insertLike" useGeneratedKeys="true" keyProperty="id">
insert into ai_resource_like (resource_id, user_id, create_time)
values (#{resourceId}, #{userId}, #{createTime})
</insert>
<delete id="deleteLike">
delete from ai_resource_like
where resource_id = #{resourceId} and user_id = #{userId}
</delete>
</mapper>

View File

@ -0,0 +1,167 @@
<?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="privateNotes" column="private_notes" />
<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,
private_notes,
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>
<if test="privateNotes != null and privateNotes != ''"> and private_notes = #{privateNotes}</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="privateNotes != null">private_notes,</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="privateNotes != null">#{privateNotes},</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 zone_journals
where parent_id = #{id}
and del_flag = '0'
)
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

@ -26,7 +26,11 @@
<result property="summary" column="summary"/> <result property="summary" column="summary"/>
<result property="remark" column="remark"/> <result property="remark" column="remark"/>
<result property="keywords" column="keywords"/> <result property="keywords" column="keywords"/>
<result property="extendData" column="extend_data"/> <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" <association property="zoneResourceDomain" column="domain_id"
javaType="com.microservices.zone.resource.domain.ZoneResourceDomain" javaType="com.microservices.zone.resource.domain.ZoneResourceDomain"
resultMap="domainResult"/> resultMap="domainResult"/>
@ -59,7 +63,10 @@
remark, remark,
update_time, update_time,
keywords, keywords,
extend_data extend_data,
resource_category,
view_count,
like_count
from zone_resource from zone_resource
</sql> </sql>
@ -85,6 +92,9 @@
zr.remark, zr.remark,
zr.keywords, zr.keywords,
zr.extend_data, zr.extend_data,
zr.resource_category,
zr.view_count,
zr.like_count,
zrd.name as domain_name zrd.name as domain_name
from zone_resource zr from zone_resource zr
left join zone_resource_domain zrd on zr.domain_id = zrd.id left join zone_resource_domain zrd on zr.domain_id = zrd.id
@ -113,6 +123,9 @@
zr.remark, zr.remark,
zr.keywords, zr.keywords,
zr.extend_data, zr.extend_data,
zr.resource_category,
zr.view_count,
zr.like_count,
zrd.name as domain_name zrd.name as domain_name
from zone_resource zr from zone_resource zr
left join zone_resource_domain zrd on zr.domain_id = zrd.id left join zone_resource_domain zrd on zr.domain_id = zrd.id
@ -127,6 +140,7 @@
<if test="deptId != null ">and zr.dept_id = #{deptId}</if> <if test="deptId != null ">and zr.dept_id = #{deptId}</if>
<if test="createBy != null and createBy != ''">and zr.create_by = #{createBy}</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="keywords != null and keywords != ''">and zr.keywords like concat('%', #{keywords}, '%')</if>
<if test="resourceCategory != null and resourceCategory != ''">and zr.resource_category = #{resourceCategory}</if>
<choose> <choose>
<when test="auditStatus != null and auditStatus !=''"> <when test="auditStatus != null and auditStatus !=''">
and zr.audit_status = #{auditStatus} and zr.audit_status = #{auditStatus}
@ -231,6 +245,9 @@
<if test="remark != null">remark,</if> <if test="remark != null">remark,</if>
<if test="keywords != null">keywords,</if> <if test="keywords != null">keywords,</if>
<if test="extendData != null">extend_data,</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>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="name != null">#{name},</if> <if test="name != null">#{name},</if>
@ -253,7 +270,10 @@
<if test="auditBy != null">#{auditBy},</if> <if test="auditBy != null">#{auditBy},</if>
<if test="remark != null">#{remark},</if> <if test="remark != null">#{remark},</if>
<if test="keywords != null">#{keywords},</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> </trim>
</insert> </insert>
@ -279,7 +299,10 @@
<if test="auditTime != null">audit_time = #{auditTime},</if> <if test="auditTime != null">audit_time = #{auditTime},</if>
<if test="auditBy != null">audit_by = #{auditBy},</if> <if test="auditBy != null">audit_by = #{auditBy},</if>
<if test="keywords != null">keywords = #{keywords},</if> <if test="keywords != null">keywords = #{keywords},</if>
<if test="extendData != null">extend_data = #{extendData},</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> </trim>
where id = #{id} where id = #{id}
</update> </update>
@ -301,4 +324,27 @@
from zone_resource from zone_resource
where zone_id = #{zoneId} where zone_id = #{zoneId}
</delete> </delete>
<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> </mapper>