From 65a5ab1ee15174024a21168b2bd634d7455df400 Mon Sep 17 00:00:00 2001 From: qiuyu <1163105536@qq.com> Date: Mon, 5 Aug 2024 19:43:55 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feature:=E3=80=90=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E3=80=91=E7=89=88=E6=9C=AC=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= =?UTF-8?q?-=E5=90=8E=E7=AB=AF=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/upgrade/releaseManage_ddl.sql | 28 ++ .../common/bean/DingdingWebHookBody.java | 11 + .../common/enums/UserSubscribeTypeEnum.java | 4 + .../util/DingTalkOrWeComWebHookUtil.java | 16 +- .../cn/torna/dao/entity/ProjectRelease.java | 51 +++ .../torna/dao/entity/ProjectReleaseDoc.java | 48 +++ .../dao/mapper/ProjectReleaseDocMapper.java | 11 + .../dao/mapper/ProjectReleaseMapper.java | 12 + .../java/cn/torna/service/MessageService.java | 6 + .../torna/service/ProjectReleaseService.java | 344 ++++++++++++++++++ .../torna/service/UserSubscribeService.java | 31 ++ .../service/dto/ProjectReleaseBindDocDTO.java | 38 ++ .../torna/service/dto/ProjectReleaseDTO.java | 48 +++ .../service/dto/ProjectReleaseDocDTO.java | 33 ++ .../service/event/ReleaseDocMessageEvent.java | 25 ++ .../listener/ReleaseDocMessageListener.java | 168 +++++++++ .../java/cn/torna/web/config/WebConfig.java | 19 + .../project/ProjectReleaseController.java | 160 ++++++++ .../project/param/ProjectReleaseAddParam.java | 40 ++ .../param/ProjectReleaseBindParam.java | 25 ++ .../param/ProjectReleaseRemoveParam.java | 21 ++ .../param/ProjectReleaseUpdateParam.java | 35 ++ 22 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 server/boot/src/main/resources/upgrade/releaseManage_ddl.sql create mode 100644 server/server-dao/src/main/java/cn/torna/dao/entity/ProjectRelease.java create mode 100644 server/server-dao/src/main/java/cn/torna/dao/entity/ProjectReleaseDoc.java create mode 100644 server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseDocMapper.java create mode 100644 server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseMapper.java create mode 100644 server/server-service/src/main/java/cn/torna/service/ProjectReleaseService.java create mode 100644 server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseBindDocDTO.java create mode 100644 server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDTO.java create mode 100644 server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDocDTO.java create mode 100644 server/server-service/src/main/java/cn/torna/service/event/ReleaseDocMessageEvent.java create mode 100644 server/server-service/src/main/java/cn/torna/service/listener/ReleaseDocMessageListener.java create mode 100644 server/server-web/src/main/java/cn/torna/web/controller/project/ProjectReleaseController.java create mode 100644 server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseAddParam.java create mode 100644 server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseBindParam.java create mode 100644 server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseRemoveParam.java create mode 100644 server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseUpdateParam.java diff --git a/server/boot/src/main/resources/upgrade/releaseManage_ddl.sql b/server/boot/src/main/resources/upgrade/releaseManage_ddl.sql new file mode 100644 index 00000000..08d9cb7a --- /dev/null +++ b/server/boot/src/main/resources/upgrade/releaseManage_ddl.sql @@ -0,0 +1,28 @@ +CREATE TABLE `project_release` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `project_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT 'project.id', + `release_no` varchar(20) NOT NULL COMMENT '版本号', + `release_desc` varchar(200) NULL DEFAULT '' COMMENT '版本描述', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态 1-有效 0-无效', + `dingding_webhook` varchar(200) NULL DEFAULT '' COMMENT '钉钉机器人webhook', + `is_deleted` tinyint NOT NULL DEFAULT 0, + `gmt_create` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_projectid_releaseno`(`project_id`, `release_no`) USING BTREE +) ENGINE = InnoDB COMMENT = '项目版本表'; + + +CREATE TABLE `project_release_doc` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `project_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT 'project.id', + `release_id` bigint NOT NULL COMMENT 'project_release.id', + `module_id` bigint NOT NULL COMMENT 'module.id', + `source_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT 'doc_info.id', + `is_deleted` tinyint NOT NULL DEFAULT 0, + `gmt_create` datetime NULL DEFAULT CURRENT_TIMESTAMP, + `gmt_modified` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_projectid_releaseid_sourceid`(`project_id`, `release_id`, `source_id`) USING BTREE +) ENGINE = InnoDB COMMENT = '项目版本关联文档表'; + diff --git a/server/server-common/src/main/java/cn/torna/common/bean/DingdingWebHookBody.java b/server/server-common/src/main/java/cn/torna/common/bean/DingdingWebHookBody.java index 0cfeabcd..f1ab2a88 100644 --- a/server/server-common/src/main/java/cn/torna/common/bean/DingdingWebHookBody.java +++ b/server/server-common/src/main/java/cn/torna/common/bean/DingdingWebHookBody.java @@ -1,7 +1,10 @@ package cn.torna.common.bean; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; +import java.util.Collection; import java.util.List; /** @@ -43,6 +46,8 @@ public class DingdingWebHookBody { } @Data + @AllArgsConstructor + @NoArgsConstructor public static class At { public At(List atUserIds) { @@ -56,5 +61,11 @@ public class DingdingWebHookBody { */ private List atUserIds; + /** + * 被@的群成员手机号。
+ * 注意
+ * + */ + private Collection atMobiles; } } diff --git a/server/server-common/src/main/java/cn/torna/common/enums/UserSubscribeTypeEnum.java b/server/server-common/src/main/java/cn/torna/common/enums/UserSubscribeTypeEnum.java index 58af1a99..df17a481 100644 --- a/server/server-common/src/main/java/cn/torna/common/enums/UserSubscribeTypeEnum.java +++ b/server/server-common/src/main/java/cn/torna/common/enums/UserSubscribeTypeEnum.java @@ -16,6 +16,10 @@ public enum UserSubscribeTypeEnum { * 推送文档 */ PUSH_DOC((byte) 3), + /** + * 订阅版本 + */ + RELEASE((byte) 4), ; private final byte type; diff --git a/server/server-common/src/main/java/cn/torna/common/util/DingTalkOrWeComWebHookUtil.java b/server/server-common/src/main/java/cn/torna/common/util/DingTalkOrWeComWebHookUtil.java index 2b0bc559..5371c46a 100644 --- a/server/server-common/src/main/java/cn/torna/common/util/DingTalkOrWeComWebHookUtil.java +++ b/server/server-common/src/main/java/cn/torna/common/util/DingTalkOrWeComWebHookUtil.java @@ -27,6 +27,20 @@ public class DingTalkOrWeComWebHookUtil { * @param userIds @用户的userId */ public static void pushRobotMessage(MessageNotifyTypeEnum notificationType, String url, String content, List userIds) { + pushRobotMessage(notificationType, url, content, userIds, null); + } + + /** + * 推送钉钉/企业微信机器人消息 + * + * @param url 推送完整url + * @param content 推送内容 + * @param userIds @用户的userId + * @param userMobiles @用户的手机号 + */ + public static void pushRobotMessage(MessageNotifyTypeEnum notificationType, String url, String content, List userIds, List userMobiles) { + log.info("pushRobotMessage notificationType:{}, url:{}, userIds:{}, userMobiles:{}, content:\n{} ", + notificationType.getDescription(), url, userIds, userMobiles, content); if (StringUtils.isEmpty(url) || StringUtils.isEmpty(content)) { return; } @@ -36,7 +50,7 @@ public class DingTalkOrWeComWebHookUtil { // 推送钉钉机器人 if (MessageNotifyTypeEnum.DING_TALK_WEB_HOOK.equals(notificationType)) { DingdingWebHookBody dingdingWebHookBody = DingdingWebHookBody.create(content); - dingdingWebHookBody.setAt(new DingdingWebHookBody.At(userIds)); + dingdingWebHookBody.setAt(new DingdingWebHookBody.At(userIds, userMobiles)); try { // 推送钉钉机器人 String result = HttpHelper.postJson(url, JSON.toJSONString(dingdingWebHookBody)) diff --git a/server/server-dao/src/main/java/cn/torna/dao/entity/ProjectRelease.java b/server/server-dao/src/main/java/cn/torna/dao/entity/ProjectRelease.java new file mode 100644 index 00000000..fe0187eb --- /dev/null +++ b/server/server-dao/src/main/java/cn/torna/dao/entity/ProjectRelease.java @@ -0,0 +1,51 @@ +package cn.torna.dao.entity; + +import com.gitee.fastmybatis.annotation.Column; +import com.gitee.fastmybatis.annotation.Pk; +import com.gitee.fastmybatis.annotation.PkStrategy; +import com.gitee.fastmybatis.annotation.Table; +import lombok.Data; + +import java.time.LocalDateTime; + + +/** + * 表名:project_release + * 备注:项目版本表 + * + * @author qiuyu + */ +@Table(name = "project_release", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT)) +@Data +public class ProjectRelease { + + /** 数据库字段:id */ + private Long id; + + /** project.id, 数据库字段:project_id */ + private Long projectId; + + /** 版本号 */ + private String releaseNo; + + /** 版本描述 */ + private String releaseDesc; + + /** 状态 1-有效 0-无效 */ + private Integer status; + + /** 钉钉机器人webhook */ + private String dingdingWebhook; + + /** 数据库字段:is_deleted */ + @Column(logicDelete = true) + private Byte isDeleted; + + /** 数据库字段:gmt_create */ + private LocalDateTime gmtCreate; + + /** 数据库字段:gmt_modified */ + private LocalDateTime gmtModified; + + +} \ No newline at end of file diff --git a/server/server-dao/src/main/java/cn/torna/dao/entity/ProjectReleaseDoc.java b/server/server-dao/src/main/java/cn/torna/dao/entity/ProjectReleaseDoc.java new file mode 100644 index 00000000..7318c1c9 --- /dev/null +++ b/server/server-dao/src/main/java/cn/torna/dao/entity/ProjectReleaseDoc.java @@ -0,0 +1,48 @@ +package cn.torna.dao.entity; + +import com.gitee.fastmybatis.annotation.Column; +import com.gitee.fastmybatis.annotation.Pk; +import com.gitee.fastmybatis.annotation.PkStrategy; +import com.gitee.fastmybatis.annotation.Table; +import lombok.Data; + +import java.time.LocalDateTime; + + +/** + * 表名:project_release_doc + * 备注:项目版本文档关联表 + * + * @author qiuyu + */ +@Table(name = "project_release_doc", pk = @Pk(name = "id", strategy = PkStrategy.INCREMENT)) +@Data +public class ProjectReleaseDoc { + + /** 数据库字段:id */ + private Long id; + + /** project.id, 数据库字段:project_id */ + private Long projectId; + + /** project_release.id */ + private Long releaseId; + + /** module.id */ + private Long moduleId; + + /** doc_info.id */ + private Long sourceId; + + /** 数据库字段:is_deleted */ + @Column(logicDelete = true) + private Byte isDeleted; + + /** 数据库字段:gmt_create */ + private LocalDateTime gmtCreate; + + /** 数据库字段:gmt_modified */ + private LocalDateTime gmtModified; + + +} \ No newline at end of file diff --git a/server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseDocMapper.java b/server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseDocMapper.java new file mode 100644 index 00000000..8fbcc1fc --- /dev/null +++ b/server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseDocMapper.java @@ -0,0 +1,11 @@ +package cn.torna.dao.mapper; + +import cn.torna.dao.entity.ProjectReleaseDoc; +import com.gitee.fastmybatis.core.mapper.BaseMapper; + +/** + * @author qiuyu + */ +public interface ProjectReleaseDocMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseMapper.java b/server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseMapper.java new file mode 100644 index 00000000..ab161899 --- /dev/null +++ b/server/server-dao/src/main/java/cn/torna/dao/mapper/ProjectReleaseMapper.java @@ -0,0 +1,12 @@ +package cn.torna.dao.mapper; + +import cn.torna.dao.entity.ProjectRelease; +import com.gitee.fastmybatis.core.mapper.BaseMapper; + + +/** + * @author qiuyu + */ +public interface ProjectReleaseMapper extends BaseMapper { + +} \ No newline at end of file diff --git a/server/server-service/src/main/java/cn/torna/service/MessageService.java b/server/server-service/src/main/java/cn/torna/service/MessageService.java index 161d0571..766efd3f 100644 --- a/server/server-service/src/main/java/cn/torna/service/MessageService.java +++ b/server/server-service/src/main/java/cn/torna/service/MessageService.java @@ -10,8 +10,10 @@ import cn.torna.dao.entity.Project; import cn.torna.dao.mapper.ModuleMapper; import cn.torna.dao.mapper.ProjectMapper; import cn.torna.service.dto.DocInfoDTO; +import cn.torna.service.event.ReleaseDocMessageEvent; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -46,6 +48,8 @@ public class MessageService { @Resource private ModuleMapper moduleMapper; + @Resource + private ApplicationContext applicationContext; /** * 发送第三方消息,钉钉、企业微信 @@ -70,6 +74,8 @@ public class MessageService { } } + // 这里推送关联版本的钉钉机器人webhook + applicationContext.publishEvent(new ReleaseDocMessageEvent(this, docInfoDTO, modifyType)); // 企业微信webhook url url = moduleConfigService.getWeComWebhookUrl(docInfoDTO.getModuleId()); if (StringUtils.hasText(url)) { diff --git a/server/server-service/src/main/java/cn/torna/service/ProjectReleaseService.java b/server/server-service/src/main/java/cn/torna/service/ProjectReleaseService.java new file mode 100644 index 00000000..eac1a314 --- /dev/null +++ b/server/server-service/src/main/java/cn/torna/service/ProjectReleaseService.java @@ -0,0 +1,344 @@ +package cn.torna.service; + +import cn.torna.common.bean.Booleans; +import cn.torna.common.enums.UserSubscribeTypeEnum; +import cn.torna.common.exception.BizException; +import cn.torna.common.util.CopyUtil; +import cn.torna.common.util.IdUtil; +import cn.torna.dao.entity.*; +import cn.torna.dao.mapper.ProjectReleaseDocMapper; +import cn.torna.dao.mapper.ProjectReleaseMapper; +import cn.torna.service.dto.ProjectReleaseBindDocDTO; +import cn.torna.service.dto.ProjectReleaseDTO; +import com.gitee.fastmybatis.core.query.LambdaQuery; +import com.gitee.fastmybatis.core.query.Query; +import com.gitee.fastmybatis.core.support.BaseLambdaService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author qiuyu + */ +@Service +public class ProjectReleaseService extends BaseLambdaService { + + @Resource + private ProjectReleaseMapper projectReleaseMapper; + @Resource + private ProjectReleaseDocMapper projectReleaseDocMapper; + @Resource + private UserSubscribeService userSubscribeService; + @Resource + private ModuleService moduleService; + @Resource + private DocInfoService docInfoService; + + /** + * 版本列表 + * + * @param userId 当前用户id + * @param projectId 项目id + * @param releaseNo 版本号(查询参数) + * @param status 版本状态(查询参数) + * @return List + * @author qiuyu + **/ + public List pageProjectRelease(long userId, long projectId, String releaseNo, Integer status) { + + Query projectReleaseQuery = LambdaQuery.create(ProjectRelease.class) + .eq(ProjectRelease::getProjectId, projectId) + .like(StringUtils.hasText(releaseNo), ProjectRelease::getReleaseNo, releaseNo) + .eq(Objects.nonNull(status), ProjectRelease::getStatus, status) + .orderByDesc(ProjectRelease::getId); + + List projectReleases = projectReleaseMapper.list(projectReleaseQuery); + List projectReleaseDTOS = CopyUtil.copyList(projectReleases, ProjectReleaseDTO::new); + if (CollectionUtils.isEmpty(projectReleaseDTOS)) { + return new ArrayList<>(0); + } + // 查询版本关联的文档 + List projectReleaseDocs = projectReleaseDocMapper.list( + LambdaQuery.create(ProjectReleaseDoc.class) + .eq(ProjectReleaseDoc::getProjectId, projectId)); + Map> releaseIdMap = new HashMap<>(0); + if (!CollectionUtils.isEmpty(projectReleaseDocs)) { + releaseIdMap = projectReleaseDocs.stream().collect(Collectors.groupingBy(ProjectReleaseDoc::getReleaseId)); + } + // 查询用户版本订阅列表 + List userSubscribes = userSubscribeService.listUserSubscribe(userId, UserSubscribeTypeEnum.RELEASE); + Map sourceIdMap = new HashMap<>(0); + if (!CollectionUtils.isEmpty(userSubscribes)) { + sourceIdMap = userSubscribes.stream().collect(Collectors.toMap( + UserSubscribe::getSourceId, + UserSubscribe::getUserId, + (v1, v2) -> v2)); + } + // 遍历封装版本信息 + Map finalSourceIdMap = sourceIdMap; + Map> finalReleaseIdMap = releaseIdMap; + projectReleaseDTOS.forEach(projectReleaseDTO -> { + // 设置添加 关联文档 + List listByReleaseId = finalReleaseIdMap.get(projectReleaseDTO.getId()); + if (!CollectionUtils.isEmpty(listByReleaseId)) { + Map> map = listByReleaseId.stream().collect( + Collectors.groupingBy(ProjectReleaseDoc::getModuleId, + Collectors.mapping(ProjectReleaseDoc::getSourceId, Collectors.toList()))); + projectReleaseDTO.setModuleSourceIdMap(toEncode(map)); + } else { + projectReleaseDTO.setModuleSourceIdMap(new HashMap<>(0)); + } + // 封装关注状态 + projectReleaseDTO.setIsSubscribe(finalSourceIdMap.get(projectReleaseDTO.getId()) != null); + + }); + return projectReleaseDTOS; + } + + /** + * 将Map中的Id转换为HashId + * + * @param map key和value均为Long类型的id + * @return Map> + * @author qiuyu + **/ + private Map> toEncode(Map> map) { + Map> encodedMap = new HashMap<>(); + if (CollectionUtils.isEmpty(map)) { + return encodedMap; + } + map.forEach((k, v) -> { + String encodedKey = IdUtil.encode(k); + List encodedValues = v.stream() + .map(IdUtil::encode) + .collect(Collectors.toList()); + encodedMap.put(encodedKey, encodedValues); + }); + return encodedMap; + } + + /** + * 新增项目版本 + * + * @param projectId 项目id + * @param releaseNo 版本号 + * @param releaseDesc 版本描述 + * @param status 版本状态 + * @param dingdingWebhook 钉钉机器人webhook + * @param moduleSourceIdMap 绑定的模块映射的文档集合 + * @author qiuyu + **/ + @Transactional(rollbackFor = Exception.class) + public void addProjectRelease(long projectId, String releaseNo, String releaseDesc, + int status, String dingdingWebhook, Map> moduleSourceIdMap) { + Query projectReleaseQuery = LambdaQuery.create(ProjectRelease.class) + .eq(ProjectRelease::getProjectId, projectId) + .eq(ProjectRelease::getReleaseNo, releaseNo); + ProjectRelease byQuery = projectReleaseMapper.get(projectReleaseQuery); + if (byQuery != null) { + throw new BizException("该版本号在此项目已存在"); + } + ProjectRelease projectRelease = new ProjectRelease(); + projectRelease.setProjectId(projectId); + projectRelease.setReleaseNo(releaseNo); + projectRelease.setReleaseDesc(releaseDesc); + projectRelease.setStatus(status == 1 ? 1 : 0); + projectRelease.setDingdingWebhook(dingdingWebhook); + projectRelease.setIsDeleted(Booleans.FALSE); + projectReleaseMapper.save(projectRelease); + // 保存关联 + saveBatchReleaseDocs(projectId, projectRelease.getId(), moduleSourceIdMap); + } + + /** + * 修改项目版本 + * + * @param id 版本id + * @param releaseDesc 版本描述 + * @param status 版本状态 + * @param dingdingWebhook 钉钉机器人webhook + * @param moduleSourceIdMap 绑定的模块映射的文档集合 + * @author qiuyu + **/ + @Transactional(rollbackFor = Exception.class) + public void updateProjectRelease(long id, String releaseDesc, int status, String dingdingWebhook, + Map> moduleSourceIdMap) { + ProjectRelease projectRelease = projectReleaseMapper.getById(id); + if (projectRelease == null) { + throw new BizException("该版本号在此项目不存在"); + } + projectRelease.setReleaseDesc(releaseDesc); + projectRelease.setStatus(status == 1 ? 1 : 0); + projectRelease.setDingdingWebhook(dingdingWebhook); + projectReleaseMapper.update(projectRelease); + + Query deleteQuery = LambdaQuery.create(ProjectReleaseDoc.class) + .eq(ProjectReleaseDoc::getProjectId, projectRelease.getProjectId()) + .eq(ProjectReleaseDoc::getReleaseId, projectRelease.getId()); + projectReleaseDocMapper.deleteByQuery(deleteQuery); + // 保存关联 + saveBatchReleaseDocs(projectRelease.getProjectId(), projectRelease.getId(), moduleSourceIdMap); + } + + /** + * 修改版本状态 + * + * @param id 版本id + * @param status 状态 + * @author qiuyu + **/ + @Transactional(rollbackFor = Exception.class) + public void updateStatus(long id, int status) { + ProjectRelease projectRelease = projectReleaseMapper.getById(id); + if (projectRelease == null) { + throw new BizException("该版本号在此项目不存在"); + } + projectRelease.setStatus(status == 1 ? 1 : 0); + projectReleaseMapper.update(projectRelease); + } + + /** + * 删除记录 + * + * @param id 版本id + * @author qiuyu + */ + @Transactional(rollbackFor = Exception.class) + public void deleteByReleaseId(long id) { + ProjectRelease projectRelease = projectReleaseMapper.getById(id); + if (projectRelease == null) { + throw new BizException("无效的版本ID"); + } + projectReleaseMapper.deleteById(id); + Query deleteQuery = LambdaQuery.create(ProjectReleaseDoc.class) + .eq(ProjectReleaseDoc::getProjectId, projectRelease.getProjectId()) + .eq(ProjectReleaseDoc::getReleaseId, projectRelease.getId()); + projectReleaseDocMapper.deleteByQuery(deleteQuery); + // 取消版本的关注记录 + userSubscribeService.cancelSubscribe(UserSubscribeTypeEnum.RELEASE, projectRelease.getId()); + } + + + /** + * 查看绑定文档 + * + * @param projectId 项目id + * @param releaseId 版本id + * @author qy + **/ + public List bindList(long projectId, long releaseId) { + List result = new ArrayList<>(); + Query query = LambdaQuery.create(ProjectReleaseDoc.class) + .eq(ProjectReleaseDoc::getProjectId, projectId) + .eq(ProjectReleaseDoc::getReleaseId, releaseId); + List list = projectReleaseDocMapper.list(query); + if (!CollectionUtils.isEmpty(list)) { + Map> muduleId2ListMap = list.stream().collect(Collectors.groupingBy(ProjectReleaseDoc::getModuleId)); + + // 获取模块名称 + Set moduleIds = muduleId2ListMap.keySet(); + List modules = moduleService.list(LambdaQuery.create(Module.class).in(Module::getId, moduleIds)); + Map moduleId2Name = modules.stream().collect(Collectors.toMap(Module::getId, Module::getName, (v1, v2) -> v2)); + + // 获取接口名称 + List docIds = list.stream().map(ProjectReleaseDoc::getSourceId).distinct().collect(Collectors.toList()); + List docInfos = docInfoService.list(LambdaQuery.create(DocInfo.class).in(DocInfo::getId, docIds)); + Map docInfoMap = docInfos.stream().collect(Collectors.toMap(DocInfo::getId, Function.identity(), (v1, v2) -> v2)); + + // 封装关联文档 + if (!CollectionUtils.isEmpty(moduleId2Name)) { + moduleId2Name.forEach((k, v) -> { + ProjectReleaseBindDocDTO module = new ProjectReleaseBindDocDTO(releaseId); + module.setIsFolder(1); + module.setId(k); + module.setLabel(v); + muduleId2ListMap.get(k).forEach(releaseDoc ->{ + ProjectReleaseBindDocDTO doc = new ProjectReleaseBindDocDTO(releaseId); + DocInfo docInfo = docInfoMap.get(releaseDoc.getSourceId()); + // 只展示接口 + if (docInfo.getIsFolder() == 0) { + doc.setIsFolder(0); + doc.setId(docInfo.getId()); + doc.setLabel(String.format("%s 【%s】 %s",docInfo.getName(), docInfo.getHttpMethod(), docInfo.getUrl())); + module.getChildren().add(doc); + } + }); + result.add(module); + }); + } + } + return result; + } + + /** + * 批量保存版本关联的文档 + * + * @param projectId 项目id + * @param releaseId 版本id + * @param moduleSourceIdMap 绑定的模块映射的文档集合 + * @author qiuyu + **/ + private void saveBatchReleaseDocs(long projectId, long releaseId, Map> moduleSourceIdMap) { + if (!CollectionUtils.isEmpty(moduleSourceIdMap)) { + List projectReleaseDocs = new ArrayList<>(); + moduleSourceIdMap.forEach((k, v) -> { + // 模块id + Long moduleId = IdUtil.decode(k); + // 文档ids + List sourceIdList = v.stream().distinct().map(IdUtil::decode).filter(Objects::nonNull).collect(Collectors.toList()); + if (!CollectionUtils.isEmpty(sourceIdList)) { + List collect = sourceIdList.stream().distinct().map(sourceId -> { + ProjectReleaseDoc projectReleaseDoc = new ProjectReleaseDoc(); + projectReleaseDoc.setProjectId(projectId); + projectReleaseDoc.setReleaseId(releaseId); + projectReleaseDoc.setModuleId(moduleId); + projectReleaseDoc.setSourceId(sourceId); + projectReleaseDoc.setIsDeleted(Booleans.FALSE); + return projectReleaseDoc; + }).collect(Collectors.toList()); + projectReleaseDocs.addAll(collect); + } + }); + + // 批量保存 + if (!CollectionUtils.isEmpty(projectReleaseDocs)) { + projectReleaseDocMapper.saveBatch(projectReleaseDocs); + } + } + } + + /** + * 获取关联的有效版本 + * + * @param projectId 项目id + * @param sourceId 文档id + * @return List + * @author qiuyu + **/ + public List relatedValidReleases(long projectId, long sourceId) { + Query projectReleaseDocQuery = LambdaQuery.create(ProjectReleaseDoc.class) + .eq(ProjectReleaseDoc::getProjectId, projectId) + .eq(ProjectReleaseDoc::getSourceId, sourceId); + List projectReleaseDocs = projectReleaseDocMapper.list(projectReleaseDocQuery); + if (CollectionUtils.isEmpty(projectReleaseDocs)) { + return new ArrayList<>(0); + } + List releaseIds = projectReleaseDocs.stream().map(ProjectReleaseDoc::getReleaseId).collect(Collectors.toList()); + Query projectReleasesQuery = LambdaQuery.create(ProjectRelease.class) + .eq(ProjectRelease::getProjectId, projectId) + .eq(ProjectRelease::getStatus, 1) + .in(ProjectRelease::getId, releaseIds); + List projectReleases = projectReleaseMapper.list(projectReleasesQuery); + if (CollectionUtils.isEmpty(projectReleases)) { + return new ArrayList<>(0); + } + return CopyUtil.copyList(projectReleases, ProjectReleaseDTO::new); + } + +} diff --git a/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java b/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java index 55fef0c1..21ac5d1e 100644 --- a/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java +++ b/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java @@ -4,11 +4,14 @@ import cn.torna.common.bean.Booleans; import cn.torna.common.enums.UserSubscribeTypeEnum; import cn.torna.dao.entity.UserSubscribe; import cn.torna.dao.mapper.UserSubscribeMapper; +import com.gitee.fastmybatis.core.query.LambdaQuery; import com.gitee.fastmybatis.core.query.Query; import com.gitee.fastmybatis.core.support.BaseLambdaService; import org.springframework.stereotype.Service; +import org.springframework.util.CollectionUtils; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -83,6 +86,23 @@ public class UserSubscribeService extends BaseLambdaService userIds = this.listUserIds(userSubscribeTypeEnum, sourceId); + if (!CollectionUtils.isEmpty(userIds)) { + this.deleteByQuery( + LambdaQuery.create(UserSubscribe.class) + .eq(UserSubscribe::getType, userSubscribeTypeEnum.getType()) + .eq(UserSubscribe::getSourceId, sourceId) + ); + } + } + public List listUserIds(UserSubscribeTypeEnum userSubscribeTypeEnum, long sourceId) { Query query = this.query() @@ -94,4 +114,15 @@ public class UserSubscribeService extends BaseLambdaService> listUserIdsGroupBySourceId(UserSubscribeTypeEnum userSubscribeTypeEnum, List sourceIds) { + Query query = this.query() + .eq(UserSubscribe::getType, userSubscribeTypeEnum.getType()) + .eq(!CollectionUtils.isEmpty(sourceIds), UserSubscribe::getSourceId, sourceIds); + List userSubscribes = this.list(query); + return userSubscribes.stream().collect(Collectors.groupingBy( + UserSubscribe::getSourceId, + Collectors.mapping(UserSubscribe::getUserId, Collectors.toList()) + )); + } } diff --git a/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseBindDocDTO.java b/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseBindDocDTO.java new file mode 100644 index 00000000..addb9471 --- /dev/null +++ b/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseBindDocDTO.java @@ -0,0 +1,38 @@ +package cn.torna.service.dto; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + + +/** + * + * @author qiuyu + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProjectReleaseBindDocDTO { + + /** project_release.id */ + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long releaseId; + + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long id; + + private String label; + + private Integer isFolder; + + private List children = new ArrayList<>(); + + public ProjectReleaseBindDocDTO(Long releaseId) { + this.releaseId = releaseId; + } +} \ No newline at end of file diff --git a/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDTO.java b/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDTO.java new file mode 100644 index 00000000..f0da62d5 --- /dev/null +++ b/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDTO.java @@ -0,0 +1,48 @@ +package cn.torna.service.dto; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + + +/** + * + * @author qiuyu + */ +@Data +public class ProjectReleaseDTO { + + /** 数据库字段:id */ + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long id; + + /** project.id, 数据库字段:project_id */ + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long projectId; + + /** 版本号 */ + private String releaseNo; + + /** 版本描述 */ + private String releaseDesc; + + /** 状态 1-有效 0-无效 */ + private Integer status; + + /** 钉钉机器人webhook */ + private String dingdingWebhook; + + /** 数据库字段:gmt_create */ + private LocalDateTime gmtCreate; + + /** 关联文档id */ + private Map> moduleSourceIdMap; + + /** 当前用户是否关注此版本 */ + private Boolean isSubscribe = false; + +} \ No newline at end of file diff --git a/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDocDTO.java b/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDocDTO.java new file mode 100644 index 00000000..85ea6ac3 --- /dev/null +++ b/server/server-service/src/main/java/cn/torna/service/dto/ProjectReleaseDocDTO.java @@ -0,0 +1,33 @@ +package cn.torna.service.dto; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import java.time.LocalDateTime; + + +/** + * + * @author qiuyu + */ +@Data +public class ProjectReleaseDocDTO { + + /** 数据库字段:id */ + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long id; + + /** project.id, 数据库字段:project_id */ + private Long projectId; + + /** project_release.id */ + private Long releaseId; + + /** doc_info.id */ + private Long sourceId; + + /** 数据库字段:gmt_create */ + private LocalDateTime gmtCreate; + +} \ No newline at end of file diff --git a/server/server-service/src/main/java/cn/torna/service/event/ReleaseDocMessageEvent.java b/server/server-service/src/main/java/cn/torna/service/event/ReleaseDocMessageEvent.java new file mode 100644 index 00000000..3f9a65a2 --- /dev/null +++ b/server/server-service/src/main/java/cn/torna/service/event/ReleaseDocMessageEvent.java @@ -0,0 +1,25 @@ +package cn.torna.service.event; + +import cn.torna.common.enums.ModifyType; +import cn.torna.service.dto.DocInfoDTO; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +/** + * @author qiuyu + */ +@Getter +public class ReleaseDocMessageEvent extends ApplicationEvent { + + private final DocInfoDTO docInfoDTO; + + private final ModifyType modifyType; + + + public ReleaseDocMessageEvent(Object source, DocInfoDTO docInfoDTO, ModifyType modifyType) { + super(source); + this.docInfoDTO = docInfoDTO; + this.modifyType = modifyType; + } + +} diff --git a/server/server-service/src/main/java/cn/torna/service/listener/ReleaseDocMessageListener.java b/server/server-service/src/main/java/cn/torna/service/listener/ReleaseDocMessageListener.java new file mode 100644 index 00000000..51a83147 --- /dev/null +++ b/server/server-service/src/main/java/cn/torna/service/listener/ReleaseDocMessageListener.java @@ -0,0 +1,168 @@ +package cn.torna.service.listener; + +import cn.torna.common.bean.EnvironmentKeys; +import cn.torna.common.enums.MessageNotifyTypeEnum; +import cn.torna.common.enums.ModifyType; +import cn.torna.common.enums.UserSubscribeTypeEnum; +import cn.torna.common.util.DingTalkOrWeComWebHookUtil; +import cn.torna.common.util.IdUtil; +import cn.torna.dao.entity.Module; +import cn.torna.dao.entity.Project; +import cn.torna.dao.entity.UserDingtalkInfo; +import cn.torna.dao.entity.UserInfo; +import cn.torna.dao.mapper.ModuleMapper; +import cn.torna.dao.mapper.ProjectMapper; +import cn.torna.service.ProjectReleaseService; +import cn.torna.service.UserDingtalkInfoService; +import cn.torna.service.UserInfoService; +import cn.torna.service.UserSubscribeService; +import cn.torna.service.dto.DocInfoDTO; +import cn.torna.service.dto.ProjectReleaseDTO; +import cn.torna.service.event.ReleaseDocMessageEvent; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.net.URL; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + + +/** + * 版本关联稿件变动消息提醒监听 + * @author qiuyu + */ +@Slf4j +@Component +public class ReleaseDocMessageListener { + + private static final DateTimeFormatter YMDHMS_PATTERN = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Resource + private ProjectReleaseService projectReleaseService; + + @Resource + private ProjectMapper projectMapper; + + @Resource + private ModuleMapper moduleMapper; + + @Resource + private UserSubscribeService userSubscribeService; + + @Resource + private UserDingtalkInfoService userDingtalkInfoService; + + @Resource + private Executor messageExecutor; + + @EventListener + public void onApplicationEvent(ReleaseDocMessageEvent event) { + final DocInfoDTO docInfoDTO = event.getDocInfoDTO(); + final ModifyType modifyType = event.getModifyType(); + messageExecutor.execute(() -> { + // 获取文档关联的有效版本 + List projectReleaseDTOS = projectReleaseService.relatedValidReleases(docInfoDTO.getProjectId(), docInfoDTO.getId()); + if (CollectionUtils.isEmpty(projectReleaseDTOS)) { + return; + } + // 获取分组后的版本的关注人 + Map> sourceId2UserIdsMap = userSubscribeService.listUserIdsGroupBySourceId( + UserSubscribeTypeEnum.RELEASE, + projectReleaseDTOS.stream().map(ProjectReleaseDTO::getId).collect(Collectors.toList()) + ); + + // 如果没有人关注 则跳过 + if (!CollectionUtils.isEmpty(sourceId2UserIdsMap)) { + for (ProjectReleaseDTO projectReleaseDTO : projectReleaseDTOS) { + String url = projectReleaseDTO.getDingdingWebhook(); + // 未配置钉钉机器人URL 则跳过 + if (StringUtils.hasText(url) && isUrl(url)) { + // 获取此版本的关注用户 + List userIds = sourceId2UserIdsMap.get(projectReleaseDTO.getId()); + if (CollectionUtils.isEmpty(userIds)) { + continue; + } + // 获取此版本的关注用户钉钉userid + List dingDingUserIds = userDingtalkInfoService.list(UserDingtalkInfo::getUserInfoId, userIds) + .stream() + .map(UserDingtalkInfo::getUserid) + .collect(Collectors.toList()); + // 获取此版本的关注用户钉钉手机号码 + List dingDingUserMobiles = null; + // 关注人未绑定钉钉,则跳过此版本文档消息提醒 + if (CollectionUtils.isEmpty(dingDingUserIds)) { + continue; + } + String content = buildDingDingMessage(MessageNotifyTypeEnum.DING_TALK_WEB_HOOK, docInfoDTO, projectReleaseDTO, + modifyType, dingDingUserIds, dingDingUserMobiles); + if (!StringUtils.hasText(content)) { + continue; + } + DingTalkOrWeComWebHookUtil.pushRobotMessage(MessageNotifyTypeEnum.DING_TALK_WEB_HOOK, url, content, dingDingUserIds, dingDingUserMobiles); + } + } + } + }); + } + + + private boolean isUrl(String url) { + try { + new URL(url).toExternalForm(); + return true; + } catch (Exception e) { + return false; + } + } + + + private String buildDingDingMessage(MessageNotifyTypeEnum notificationType, DocInfoDTO docInfoDTO, ProjectReleaseDTO projectReleaseDTO, + ModifyType modifyType, Collection userIds, Collection userMobiles) { + Project project = projectMapper.getById(docInfoDTO.getProjectId()); + Module module = moduleMapper.getById(docInfoDTO.getModuleId()); + Map replaceMap = new HashMap<>(16); + replaceMap.put("{projectName}", project.getName()); + replaceMap.put("{appName}", module.getName()); + replaceMap.put("{releaseNo}", projectReleaseDTO.getReleaseNo()); + replaceMap.put("{docName}", docInfoDTO.getName()); + replaceMap.put("{url}", docInfoDTO.getUrl()); + replaceMap.put("{modifier}", docInfoDTO.getModifierName()); + replaceMap.put("{modifyTime}", docInfoDTO.getGmtModified().format(YMDHMS_PATTERN)); + replaceMap.put("{modifyType}", modifyType.getDescription()); + String frontUrl = EnvironmentKeys.TORNA_FRONT_URL.getValue("http://localhost:7700"); + String docViewUrl = frontUrl + "/#/view/" + IdUtil.encode(docInfoDTO.getId()); + replaceMap.put("{docViewUrl}", docViewUrl); + String content = null; + // 钉钉 + if (notificationType.equals(MessageNotifyTypeEnum.DING_TALK_WEB_HOOK)) { + String atUser = ""; + if (!CollectionUtils.isEmpty(userIds)) { + atUser = userIds.stream() + .map(userId -> "@" + userId) + .collect(Collectors.joining(" ")); + } + // 有钉钉手机号添加@手机号 + if (!CollectionUtils.isEmpty(userMobiles)) { + atUser = atUser + userMobiles.stream() + .map(userMobile -> "@" + userMobile) + .collect(Collectors.joining(" ")); + } + replaceMap.put("{@user}", atUser); + content = EnvironmentKeys.PUSH_DINGDING_WEBHOOK_CONTENT.getValue(); + } + if (!StringUtils.hasText(content)) { + return content; + } + for (Map.Entry entry : replaceMap.entrySet()) { + content = content.replace(entry.getKey(), entry.getValue()); + } + return content; + } + +} diff --git a/server/server-web/src/main/java/cn/torna/web/config/WebConfig.java b/server/server-web/src/main/java/cn/torna/web/config/WebConfig.java index b564e888..d3f7a25d 100644 --- a/server/server-web/src/main/java/cn/torna/web/config/WebConfig.java +++ b/server/server-web/src/main/java/cn/torna/web/config/WebConfig.java @@ -25,6 +25,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.StringUtils; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; @@ -36,6 +37,8 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.Arrays; import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; /** * @author tanghc @@ -152,6 +155,22 @@ public class WebConfig implements WebMvcConfigurer, ApplicationContextAware, Ini return new TornaAsyncConfigurer("torna-sync", threadPoolSize); } + @Bean("messageExecutor") + public Executor messageExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); + executor.setMaxPoolSize(32); + executor.setQueueCapacity(1000); + executor.setKeepAliveSeconds(60); + executor.setThreadNamePrefix("messageTask-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + // 线程池对拒绝任务的处理策略 + // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 初始化 + executor.initialize(); + return executor; + } @Override public void afterPropertiesSet() throws Exception { diff --git a/server/server-web/src/main/java/cn/torna/web/controller/project/ProjectReleaseController.java b/server/server-web/src/main/java/cn/torna/web/controller/project/ProjectReleaseController.java new file mode 100644 index 00000000..a68ea6e2 --- /dev/null +++ b/server/server-web/src/main/java/cn/torna/web/controller/project/ProjectReleaseController.java @@ -0,0 +1,160 @@ +package cn.torna.web.controller.project; + +import cn.torna.common.annotation.HashId; +import cn.torna.common.bean.Result; +import cn.torna.common.bean.User; +import cn.torna.common.enums.UserSubscribeTypeEnum; +import cn.torna.service.ProjectReleaseService; +import cn.torna.service.UserSubscribeService; +import cn.torna.service.dto.ProjectReleaseDTO; +import cn.torna.web.config.UserContext; +import cn.torna.web.controller.project.param.ProjectReleaseAddParam; +import cn.torna.web.controller.project.param.ProjectReleaseBindParam; +import cn.torna.web.controller.project.param.ProjectReleaseRemoveParam; +import cn.torna.web.controller.project.param.ProjectReleaseUpdateParam; +import cn.torna.web.controller.user.param.UserSubscribeParam; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +/** + * 项目版本管理 + * + * @author qiuyu + */ +@RestController +@RequestMapping("project/release") +public class ProjectReleaseController { + + @Resource + private ProjectReleaseService projectReleaseService; + @Resource + private UserSubscribeService userSubscribeService; + + /** + * 分页查询项目版本 + * + * @author qiuyu + * @return + */ + @GetMapping("/list") + public Result> page( + @HashId Long projectId, + @RequestParam(required = false) String releaseNo, + @RequestParam(required = false) Integer status) { + User user = UserContext.getUser(); + List projectUserDTOList = projectReleaseService.pageProjectRelease( + user.getUserId(), + projectId, + releaseNo, + status + ); + return Result.ok(projectUserDTOList); + } + + /** + * 添加项目版本 + * + * @author qiuyu + * @param param 项目版本新增入参 + * @return + */ + @PostMapping("add") + public Result add(@RequestBody @Valid ProjectReleaseAddParam param) { + projectReleaseService.addProjectRelease( + param.getProjectId(), + param.getReleaseNo(), + param.getReleaseDesc(), + param.getStatus(), + param.getDingdingWebhook(), + param.getModuleSourceIdMap() + ); + return Result.ok(); + } + + /** + * 修改项目版本 + * + * @author qiuyu + * @param param 项目版本修改入参 + * @return + */ + @PostMapping("update") + public Result update(@RequestBody @Valid ProjectReleaseUpdateParam param) { + projectReleaseService.updateProjectRelease( + param.getId(), + param.getReleaseDesc(), + param.getStatus(), + param.getDingdingWebhook(), + param.getModuleSourceIdMap() + ); + return Result.ok(); + } + + /** + * 修改项目状态 + * + * @author qiuyu + * @param param 项目版本状态修改入参 + * @return + */ + @PostMapping("status") + public Result updateStatus(@RequestBody @Valid ProjectReleaseUpdateParam param) { + projectReleaseService.updateStatus(param.getId(), param.getStatus()); + return Result.ok(); + } + + /** + * 移除用户 + * + * @author qiuyu + * @param param 项目版本状删除入参 + */ + @PostMapping("remove") + public Result remove(@RequestBody @Valid ProjectReleaseRemoveParam param) { + projectReleaseService.deleteByReleaseId(param.getId()); + return Result.ok(); + } + + /** + * 查询项目版本绑定文档 + * + * @author qiuyu + * @deprecated 切换为新增、修改时绑定 + * @param param 项目版本绑定文档入参 + * @return + */ + @PostMapping("/bind/list") + public Result bindList(@RequestBody @Valid ProjectReleaseBindParam param) { + return Result.ok(projectReleaseService.bindList(param.getProjectId(), param.getReleaseId())); + } + + /** + * 订阅版本 + * + * @author qiuyu + * @param param param + */ + @PostMapping("doc/subscribe") + public Result subscribeDoc(@RequestBody UserSubscribeParam param) { + User user = UserContext.getUser(); + userSubscribeService.subscribe(user.getUserId(), UserSubscribeTypeEnum.RELEASE, param.getSourceId()); + return Result.ok(); + } + + /** + * 取消订阅版本 + * + * @author qiuyu + * @param param param + */ + @PostMapping("doc/cancelSubscribe") + public Result cancelSubscribe(@RequestBody UserSubscribeParam param) { + User user = UserContext.getUser(); + userSubscribeService.cancelSubscribe(user.getUserId(), UserSubscribeTypeEnum.RELEASE, param.getSourceId()); + return Result.ok(); + } + +} diff --git a/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseAddParam.java b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseAddParam.java new file mode 100644 index 00000000..1cf9a8f8 --- /dev/null +++ b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseAddParam.java @@ -0,0 +1,40 @@ +package cn.torna.web.controller.project.param; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +/** + * @author qiuyu + */ +@Data +public class ProjectReleaseAddParam { + + @NotNull(message = "项目ID不能为空") + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long projectId; + + /** 版本号 */ + @NotEmpty(message = "版本号不能为空") + private String releaseNo; + + /** 版本描述 */ + private String releaseDesc; + + /** 状态 1-有效 0-无效 */ + @NotNull(message = "版本状态不能为空") + private Integer status; + + /** 钉钉机器人webhook */ + @URL(message = "地址格式不正确") + private String dingdingWebhook; + + /** 关联文档Map (key:模块hashId value: 文档hashId集合) */ + private Map> moduleSourceIdMap; +} diff --git a/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseBindParam.java b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseBindParam.java new file mode 100644 index 00000000..22c523de --- /dev/null +++ b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseBindParam.java @@ -0,0 +1,25 @@ +package cn.torna.web.controller.project.param; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +/** + * @author qiuyu + */ +@Data +public class ProjectReleaseBindParam { + + @NotNull(message = "项目ID不能为空") + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long projectId; + + @NotNull(message = "项目版本ID不能为空") + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long releaseId; + +} diff --git a/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseRemoveParam.java b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseRemoveParam.java new file mode 100644 index 00000000..2d6cf7ef --- /dev/null +++ b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseRemoveParam.java @@ -0,0 +1,21 @@ +package cn.torna.web.controller.project.param; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * @author qiuyu + */ +@Data +public class ProjectReleaseRemoveParam { + + @NotNull(message = "项目版本ID不能为空") + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long id; + +} diff --git a/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseUpdateParam.java b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseUpdateParam.java new file mode 100644 index 00000000..1467431f --- /dev/null +++ b/server/server-web/src/main/java/cn/torna/web/controller/project/param/ProjectReleaseUpdateParam.java @@ -0,0 +1,35 @@ +package cn.torna.web.controller.project.param; + +import cn.torna.common.support.IdCodec; +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +/** + * @author qiuyu + */ +@Data +public class ProjectReleaseUpdateParam { + + @NotNull(message = "项目版本ID不能为空") + @JSONField(serializeUsing = IdCodec.class, deserializeUsing = IdCodec.class) + private Long id; + + /** 版本描述 */ + private String releaseDesc; + + /** 状态 1-有效 0-无效 */ + @NotNull(message = "版本状态不能为空") + private Integer status; + + /** 钉钉机器人webhook */ + @URL(message = "地址格式不正确") + private String dingdingWebhook; + + /** 关联文档Map (key:模块hashId value: 文档hashId集合) */ + private Map> moduleSourceIdMap; +} -- Gitee From e9ee1d6acbd64f514deb94f7d3e6ae7241fb88df Mon Sep 17 00:00:00 2001 From: qiuyu <1163105536@qq.com> Date: Mon, 5 Aug 2024 20:14:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feature:=20=E3=80=90=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E3=80=91=E5=89=8D=E7=AB=AF=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- front/src/components/ModuleDocTree/index.vue | 228 ++++++++++++ front/src/components/ProjectMenu/index.vue | 4 + front/src/router/index.js | 5 + front/src/utils/i18n/languages/en-us.js | 10 + front/src/utils/i18n/languages/zh-cn.js | 10 + front/src/utils/role-code.js | 6 + .../views/admin/setting/DingDing/index.vue | 1 + .../views/project/ProjectRelease/index.vue | 351 ++++++++++++++++++ front/src/views/project/index_release.vue | 19 + 9 files changed, 634 insertions(+) create mode 100644 front/src/components/ModuleDocTree/index.vue create mode 100644 front/src/views/project/ProjectRelease/index.vue create mode 100644 front/src/views/project/index_release.vue diff --git a/front/src/components/ModuleDocTree/index.vue b/front/src/components/ModuleDocTree/index.vue new file mode 100644 index 00000000..062939e3 --- /dev/null +++ b/front/src/components/ModuleDocTree/index.vue @@ -0,0 +1,228 @@ + + + diff --git a/front/src/components/ProjectMenu/index.vue b/front/src/components/ProjectMenu/index.vue index f5fd0b4e..42c1b182 100644 --- a/front/src/components/ProjectMenu/index.vue +++ b/front/src/components/ProjectMenu/index.vue @@ -22,6 +22,10 @@ {{ $t('constManager') }} + + + {{ $t('releaseManager') }} + diff --git a/front/src/router/index.js b/front/src/router/index.js index 3e453249..f127f6d6 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -165,6 +165,11 @@ export const constantRoutes = [ path: 'code/:projectId(\\w+)', name: 'ErrorCode', component: () => import('@/views/project/index_code') + }, + { + path: 'release/:projectId(\\w+)', + name: 'ProjectRelease', + component: () => import('@/views/project/index_release') } ] }, diff --git a/front/src/utils/i18n/languages/en-us.js b/front/src/utils/i18n/languages/en-us.js index 1a41157a..37128f21 100644 --- a/front/src/utils/i18n/languages/en-us.js +++ b/front/src/utils/i18n/languages/en-us.js @@ -94,6 +94,8 @@ export default { 'loginSubmit': 'Login', 'search': 'Search', 'addMember': 'Add Member', + 'addRelease': 'Add Release', + 'updateRelease': 'Update Release', 'member': 'Member', 'me': 'Me', 'role': 'Role', @@ -396,6 +398,10 @@ export default { 'language': 'Language', 'docTabView': 'Tabs View', 'nickEmail': 'Nickname/Email', + 'releaseNo': 'Version Number', + 'releaseDesc': 'Version Description', + 'viewAssociatedDocuments': 'View Associated Documents', + 'associatedDocument': 'Associated Document', 'visitStyle': 'Visit Style', 'updateName': 'Update Name', 'visitUrl': 'Visit Url', @@ -449,6 +455,7 @@ export default { 'grid': 'Grid', 'card': 'Card', 'constManager': 'Constant Management', + 'releaseManager': 'Release Management', 'projectConstant': 'Project Constant', 'applicationConstant': 'Application Constant', 'viewConst': 'View Constant', @@ -465,6 +472,9 @@ export default { 'comment': 'Comment', 'commentPlaceholder': 'Input comment here...', 'previousVersion': 'Previous Version', + 'valid': 'valid', + 'invalid': 'invalid', + 'bindingApiDoc': 'Binding interface document', // ---- common end ---- // ---- 组件特有的,key表示组件名称(文件夹名称) ---- RichTextEditor: { diff --git a/front/src/utils/i18n/languages/zh-cn.js b/front/src/utils/i18n/languages/zh-cn.js index d3012733..97003902 100644 --- a/front/src/utils/i18n/languages/zh-cn.js +++ b/front/src/utils/i18n/languages/zh-cn.js @@ -90,6 +90,8 @@ export default { 'loginSubmit': '登 录', 'search': '查询', 'addMember': '添加成员', + 'addRelease': '添加版本', + 'updateRelease': '修改版本', 'member': '成员', 'me': '我', 'role': '角色', @@ -393,6 +395,10 @@ export default { 'language': '语言', 'docTabView': '标签导航', 'nickEmail': '昵称/邮箱', + 'releaseNo': '版本号', + 'releaseDesc': '版本描述', + 'viewAssociatedDocuments': '查看关联版本', + 'associatedDocument': '关联文档', 'visitStyle': '访问方式', 'updateName': '修改名称', 'visitUrl': '访问链接', @@ -450,6 +456,7 @@ export default { 'grid': '列表', 'card': '卡片', 'constManager': '常量管理', + 'releaseManager': '版本管理', 'projectConstant': '项目常量', 'applicationConstant': '应用常量', 'viewConst': '查看常量', @@ -466,6 +473,9 @@ export default { 'comment': '评论', 'commentPlaceholder': '在此输入评论内容...', 'previousVersion': '上一版', + 'valid': '有效', + 'invalid': '无效', + 'bindingApiDoc': '绑定接口文档', // ---- common end ---- // ---- 组件特有的,key表示组件名称(文件夹名称) ---- RichTextEditor: { diff --git a/front/src/utils/role-code.js b/front/src/utils/role-code.js index 99cced96..5c59d983 100644 --- a/front/src/utils/role-code.js +++ b/front/src/utils/role-code.js @@ -40,5 +40,11 @@ Object.assign(Vue.prototype, { } } return '' + }, + getStatusCodeConfig() { + return [ + { label: this.$t('invalid'), code: '0' }, + { label: this.$t('valid'), code: '1' } + ] } }) diff --git a/front/src/views/admin/setting/DingDing/index.vue b/front/src/views/admin/setting/DingDing/index.vue index fc6eda9f..8ba2b072 100644 --- a/front/src/views/admin/setting/DingDing/index.vue +++ b/front/src/views/admin/setting/DingDing/index.vue @@ -27,6 +27,7 @@ {modifyType}修改类型,枚举值:修改,删除 {projectName}项目名称 {appName}应用名称 + {releaseNo}版本号 {docName}文档名称 {modifier}修改人 {modifyTime}修改时间 diff --git a/front/src/views/project/ProjectRelease/index.vue b/front/src/views/project/ProjectRelease/index.vue new file mode 100644 index 00000000..1914f172 --- /dev/null +++ b/front/src/views/project/ProjectRelease/index.vue @@ -0,0 +1,351 @@ + + + diff --git a/front/src/views/project/index_release.vue b/front/src/views/project/index_release.vue new file mode 100644 index 00000000..4ddb7d58 --- /dev/null +++ b/front/src/views/project/index_release.vue @@ -0,0 +1,19 @@ + + + -- Gitee From 0b787763709ce4db66eae54f05666e75e832b4ff Mon Sep 17 00:00:00 2001 From: qiuyu <1163105536@qq.com> Date: Tue, 6 Aug 2024 16:22:04 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=B7=B2=E9=85=8D=E7=BD=AE=E9=92=89=E9=92=89?= =?UTF-8?q?=E6=9C=BA=E5=99=A8=E4=BA=BAwebhook=EF=BC=8C=E5=BD=93=E6=97=A0?= =?UTF-8?q?=E5=85=B3=E6=B3=A8=E4=BA=BA=E7=9A=84=E6=A8=A1=E5=9D=97=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=8F=98=E6=9B=B4=E6=97=B6=EF=BC=8C=E5=AF=BC=E8=87=B4?= =?UTF-8?q?=E7=9A=84=E6=9F=A5=E8=AF=A2=E5=BC=82=E5=B8=B8=E3=80=82=20?= =?UTF-8?q?=E3=80=90=E4=BF=AE=E5=A4=8D=E3=80=91=E7=89=88=E6=9C=AC=E5=8F=98?= =?UTF-8?q?=E6=9B=B4=E4=BF=A1=E6=81=AF=E6=8E=A8=E9=80=81=E6=97=B6,?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E4=B8=8D=E5=88=B0=E5=85=B3=E6=B3=A8=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=9A=84=E9=97=AE=E9=A2=98=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/cn/torna/service/DocInfoService.java | 6 ++++++ .../main/java/cn/torna/service/UserSubscribeService.java | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/server-service/src/main/java/cn/torna/service/DocInfoService.java b/server/server-service/src/main/java/cn/torna/service/DocInfoService.java index 644867bd..45f2d05e 100755 --- a/server/server-service/src/main/java/cn/torna/service/DocInfoService.java +++ b/server/server-service/src/main/java/cn/torna/service/DocInfoService.java @@ -829,6 +829,9 @@ public class DocInfoService extends BaseLambdaService { */ public List listSubscribeDocDingDingUserIds(long docId) { List userIds = userSubscribeService.listUserIds(UserSubscribeTypeEnum.DOC, docId); + if (CollectionUtils.isEmpty(userIds)) { + return new ArrayList<>(0); + } List dingtalkInfoList = userDingtalkInfoMapper.list(UserDingtalkInfo::getUserInfoId, userIds); return dingtalkInfoList.stream() .map(UserDingtalkInfo::getUserid) @@ -844,6 +847,9 @@ public class DocInfoService extends BaseLambdaService { */ public List listSubscribeDocWeComUserMobiles(long docId) { List userIds = userSubscribeService.listUserIds(UserSubscribeTypeEnum.DOC, docId); + if (CollectionUtils.isEmpty(userIds)) { + return new ArrayList<>(0); + } List userInfoId = userWeComInfoWrapper.list(UserWeComInfo::getUserInfoId, userIds); return userInfoId.stream() .map(UserWeComInfo::getMobile) diff --git a/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java b/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java index 21ac5d1e..eee999a6 100644 --- a/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java +++ b/server/server-service/src/main/java/cn/torna/service/UserSubscribeService.java @@ -118,7 +118,7 @@ public class UserSubscribeService extends BaseLambdaService> listUserIdsGroupBySourceId(UserSubscribeTypeEnum userSubscribeTypeEnum, List sourceIds) { Query query = this.query() .eq(UserSubscribe::getType, userSubscribeTypeEnum.getType()) - .eq(!CollectionUtils.isEmpty(sourceIds), UserSubscribe::getSourceId, sourceIds); + .in(!CollectionUtils.isEmpty(sourceIds), UserSubscribe::getSourceId, sourceIds); List userSubscribes = this.list(query); return userSubscribes.stream().collect(Collectors.groupingBy( UserSubscribe::getSourceId, -- Gitee