diff --git a/front/src/components/ModuleDocTree/index.vue b/front/src/components/ModuleDocTree/index.vue
new file mode 100644
index 0000000000000000000000000000000000000000..062939e3fc98d0db1c70effc732ae4c0f8e17bb2
--- /dev/null
+++ b/front/src/components/ModuleDocTree/index.vue
@@ -0,0 +1,228 @@
+
+
+
+
+
+
+
+
+ {{ node.label }}【{{ data.httpMethod }}】{{ data.url }}
+
+
+
+
+
+
diff --git a/front/src/components/ProjectMenu/index.vue b/front/src/components/ProjectMenu/index.vue
index f5fd0b4ef91bf528aefb71317f2cfa2f57fdfaab..42c1b182553085ea9321a5c8022375a8fea28184 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 3e4532496a465f00a9a3b26dfc12ea0d88207b27..f127f6d6fd4f1855835e05b426f1ac2752640522 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 1a41157a6b5de2bc7ff43d2f6fa37be92972124b..37128f2121f147bd5ef0e8f0972f42605b37b1de 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 d30127335f894babb587ed9baf1231be0f4ecc25..970039029289e746b87c7704bc7d511a3f38263f 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 99cced9650a74fb7c86a5042487ebd70c4c50873..5c59d983060b5a12e20e91bf64dfb0bfb6888ddd 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 fc6eda9f4f8b0524b7da79dcd9543f50016cffd1..8ba2b072a94737efd128e3ecaea837dd0ef0b676 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 0000000000000000000000000000000000000000..1914f172d6d979875d22f0127e37cd8d27e331c9
--- /dev/null
+++ b/front/src/views/project/ProjectRelease/index.vue
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+ {{ $t('search') }}
+
+
+
+ {{ $t('addRelease') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ scope.row.isSubscribe ? $t('cancelSubscribe') : $t('clickSubscribe') }}
+
+
+ {{ $t('viewAssociatedDocuments') }}
+ {{ $t('update') }}
+
+ {{ $t('remove') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ node.label }}
+
+
+
+
+
+
+
+
diff --git a/front/src/views/project/index_release.vue b/front/src/views/project/index_release.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4ddb7d58c33e7629d9a84ee899abf7119be13d0c
--- /dev/null
+++ b/front/src/views/project/index_release.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
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 0000000000000000000000000000000000000000..08d9cb7a81ed148b3264e9f25d4adc6edadf5259
--- /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 0cfeabcd3633e69f0542ed9b7931620a15e0e84e..f1ab2a88472f3864abe9232d24982ec47879298e 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 58af1a99c16913e35a55bbad3a12b0972e3f6383..df17a481d9a5b9b2b3ded47e8591ad72bef5657b 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 2b0bc55950415b2fba884cf31645937c72f347d3..5371c46af6289b58522121ab25ce3f49a20c32bc 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 0000000000000000000000000000000000000000..fe0187ebb369335f1551a0dc5036e64de5910fb0
--- /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 0000000000000000000000000000000000000000..7318c1c9a9493dd269c03529f4006e5746190e4a
--- /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 0000000000000000000000000000000000000000..8fbcc1fc5f1af24475fd150f963197ffab83dc5d
--- /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 0000000000000000000000000000000000000000..ab161899eadbbee2f1e8f8ea8e4636bfb65c0ab3
--- /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/DocInfoService.java b/server/server-service/src/main/java/cn/torna/service/DocInfoService.java
index 644867bdf2ca48a21424c3d787fc3de8cfec6b48..45f2d05e111c72963a00e7c67144586bee85475f 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/MessageService.java b/server/server-service/src/main/java/cn/torna/service/MessageService.java
index 161d05718015ec4642fbfd2626c70c70fc02b10d..766efd3f496692816a00fddcaf649f06344d4dff 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 0000000000000000000000000000000000000000..eac1a31456a243892dec7286edb827fb6f77809e
--- /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 55fef0c1180fa5df6f59d579daf3168ea119d0fd..eee999a68dde302cf880963129eef46909fcc020 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())
+ .in(!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 0000000000000000000000000000000000000000..addb9471f12b31b7b96643328c3aeed9f183444a
--- /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 0000000000000000000000000000000000000000..f0da62d5da2ee8445d9a0275c35a858272785088
--- /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 0000000000000000000000000000000000000000..85ea6ac3cd5ee5f967e5d2ed405a82d7aeb9b8b7
--- /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 0000000000000000000000000000000000000000..3f9a65a228db08afe2ecab3ecd89e1c83ac23cee
--- /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 0000000000000000000000000000000000000000..51a83147fbf82c112e117a240aa3bd8bbdeb40c6
--- /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 b564e8881a9359a25d0d0b5877eb2ec0a2e2495b..d3f7a25daf3abdea861e85a34db21828f49e4a72 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 0000000000000000000000000000000000000000..a68ea6e29326a023a86d426127aa795583775313
--- /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 0000000000000000000000000000000000000000..1cf9a8f8957d1587d33bf130c5e38f8d77f4e561
--- /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 0000000000000000000000000000000000000000..22c523ded4b9c70de089d86e0e41b342d0d1c2d6
--- /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 0000000000000000000000000000000000000000..2d6cf7ef3b6ec194a5eba44374a2df23c614e522
--- /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 0000000000000000000000000000000000000000..1467431f6d6fdc0542ba5cd144ad275d5f535068
--- /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;
+}