diff --git a/scripts/bkenv.properties b/scripts/bkenv.properties index 4be8b77e64d..99dc67c6e36 100644 --- a/scripts/bkenv.properties +++ b/scripts/bkenv.properties @@ -37,6 +37,10 @@ BK_SSM_PORT= BK_TURBO_PRIVATE_URL=http://bk-turbo.service.consul # BK_TURBO_PUBLIC_URL默认为http://turbo.$BK_DOMAIN. 按需修改. turbo网关 BK_TURBO_PUBLIC_URL=http://turbo.$BK_DOMAIN +# BK_CI_WEWORK_HOST 默认值为:https://qyapi.weixin.qq.com, 企业微信服务端host,用于调用企业微信api接口 +BK_CI_WEWORK_HOST=https://qyapi.weixin.qq.com +# BK_CI_WEWORK_API_URL 默认值为:https://qyapi.weixin.qq.com, 企业微信服务端host,用于调用企业微信api接口 +BK_CI_WEWORK_API_URL= ########## # 1-基础配置 @@ -199,7 +203,12 @@ BK_CI_REPOSITORY_SVN_API_URL= BK_CI_REPOSITORY_SVN_WEBHOOK_URL= # BK_CI_STORE_USER_AVATARS_URL默认为$BK_PAAS_PUBLIC_URL/console/static/img/getheadimg.jpg?. 无需修改. PaaS用户头像, 目前仅显示默认头像. BK_CI_STORE_USER_AVATARS_URL=$BK_PAAS_PUBLIC_URL/console/static/img/getheadimg.jpg? - +# BK_CI_FQDN_CERT BKCI站点的HTTPS证书存储位置, 默认值为空表示没有开启HTTPS,用于Agent与BKCI走HTTPS通信使用 +BK_CI_FQDN_CERT= +# BK_CI_NOTIFY_WEWORK_API_URL 默认值为:https://qyapi.weixin.qq.com, 企业微信服务端host,用于调用企业微信api接口. +BK_CI_NOTIFY_WEWORK_API_URL=https://qyapi.weixin.qq.com +# BK_CI_NOTIFY_WEWORK_SEND_CHANNEL 发送rtx的通道: weworkAgent 企业微信应用,blueking 蓝鲸消息 +BK_CI_NOTIFY_WEWORK_SEND_CHANNEL=weworkAgent ########## # 4-微服务依赖 ########## diff --git a/src/backend/ci/boot-assembly/build.gradle.kts b/src/backend/ci/boot-assembly/build.gradle.kts index c0a8488b790..5f6937a217e 100644 --- a/src/backend/ci/boot-assembly/build.gradle.kts +++ b/src/backend/ci/boot-assembly/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation(project(":core:log:biz-log-sample")) implementation(project(":core:misc:biz-misc-sample")) implementation(project(":core:notify:biz-notify-blueking")) + implementation(project(":core:notify:biz-notify-wework")) implementation(project(":core:openapi:biz-openapi")) implementation(project(":core:plugin:biz-plugin")) implementation(project(":core:process:plugin-load")) diff --git a/src/backend/ci/core/common/common-api/src/main/kotlin/com/tencent/devops/common/api/util/OkhttpUtils.kt b/src/backend/ci/core/common/common-api/src/main/kotlin/com/tencent/devops/common/api/util/OkhttpUtils.kt index a30799b255a..899200bc045 100644 --- a/src/backend/ci/core/common/common-api/src/main/kotlin/com/tencent/devops/common/api/util/OkhttpUtils.kt +++ b/src/backend/ci/core/common/common-api/src/main/kotlin/com/tencent/devops/common/api/util/OkhttpUtils.kt @@ -154,12 +154,13 @@ object OkhttpUtils { url: String, uploadFile: File, headers: Map? = null, - fileFieldName: String = "file" + fileFieldName: String = "file", + fileName: String = uploadFile.name ): Response { val fileBody = RequestBody.create(octetStream, uploadFile) val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart(fileFieldName, uploadFile.name, fileBody) + .addFormDataPart(fileFieldName, fileName, fileBody) .build() val requestBuilder = Request.Builder() .url(url) diff --git a/src/backend/ci/core/common/common-event/src/main/kotlin/com/tencent/devops/common/event/dispatcher/pipeline/mq/MQ.kt b/src/backend/ci/core/common/common-event/src/main/kotlin/com/tencent/devops/common/event/dispatcher/pipeline/mq/MQ.kt index 009d209618a..ab083633963 100644 --- a/src/backend/ci/core/common/common-event/src/main/kotlin/com/tencent/devops/common/event/dispatcher/pipeline/mq/MQ.kt +++ b/src/backend/ci/core/common/common-event/src/main/kotlin/com/tencent/devops/common/event/dispatcher/pipeline/mq/MQ.kt @@ -104,6 +104,7 @@ object MQ { const val QUEUE_PIPELINE_BUILD_FINISH_LOG = "q.engine.pipeline.build.log" const val QUEUE_PIPELINE_BUILD_FINISH_SUBPIPEINE = "q.engine.pipeline.build.subpipeline" const val QUEUE_PIPELINE_BUILD_FINISH_WEBHOOK_QUEUE = "q.engine.pipeline.build.finish.webhook.queue" + const val QUEUE_PIPELINE_BUILD_FINISH_NOTIFY_QUEUE = "q.engine.pipeline.build.finish.notify.queue" const val QUEUE_PIPELINE_BUILD_FINISH_EXT = "q.engine.pipeline.build.finish.ext" const val QUEUE_PIPELINE_BUILD_FINISH_DISPATCHER = "q.engine.pipeline.build.dispatcher" diff --git a/src/backend/ci/core/common/common-notify/src/main/kotlin/com/tencent/devops/common/notify/enums/NotifyType.kt b/src/backend/ci/core/common/common-notify/src/main/kotlin/com/tencent/devops/common/notify/enums/NotifyType.kt index b5363491ffd..c5448f3f841 100644 --- a/src/backend/ci/core/common/common-notify/src/main/kotlin/com/tencent/devops/common/notify/enums/NotifyType.kt +++ b/src/backend/ci/core/common/common-notify/src/main/kotlin/com/tencent/devops/common/notify/enums/NotifyType.kt @@ -31,5 +31,6 @@ enum class NotifyType { RTX, EMAIL, WECHAT, - SMS + SMS, + WEWORK } diff --git a/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/Constants.kt b/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/Constants.kt index 15aa626102f..893514aff6c 100644 --- a/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/Constants.kt +++ b/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/Constants.kt @@ -32,11 +32,13 @@ const val QUEUE_NOTIFY_RTX = "queue_notify_rtx" const val QUEUE_NOTIFY_WECHAT = "queue_notify_wechat" const val QUEUE_NOTIFY_EMAIL = "queue_notify_email" const val QUEUE_NOTIFY_SMS = "queue_notify_sms" +const val QUEUE_NOTIFY_WEWORK = "queue_notify_wework" const val EXCHANGE_NOTIFY = "exchange_notify" const val ROUTE_RTX = "rtx" const val ROUTE_WECHAT = "wechat" const val ROUTE_EMAIL = "email" const val ROUTE_SMS = "sms" +const val ROUTE_WEWORK = "wework" const val PIPELINE_QUALITY_AUDIT_NOTIFY_TEMPLATE = "QUALITY_AUDIT_NOTIFY_TEMPLATE" const val PIPELINE_QUALITY_END_NOTIFY_TEMPLATE = "QUALITY_END_NOTIFY_TEMPLATE" diff --git a/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyMediaMessage.kt b/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyMediaMessage.kt index d35d4920077..470e6fb52cf 100644 --- a/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyMediaMessage.kt +++ b/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyMediaMessage.kt @@ -35,7 +35,7 @@ import java.io.InputStream @ApiModel("企业微信文本消息") data class WeworkNotifyMediaMessage( @ApiModelProperty("接收人Id", required = true) - val receivers: List, + val receivers: Collection, @ApiModelProperty("接收人类型", required = true) val receiverType: WeworkReceiverType, @ApiModelProperty("媒体内容", required = true) diff --git a/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyTextMessage.kt b/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyTextMessage.kt index 0b6e571e309..fb35acb6625 100644 --- a/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyTextMessage.kt +++ b/src/backend/ci/core/notify/api-notify/src/main/kotlin/com/tencent/devops/notify/pojo/WeworkNotifyTextMessage.kt @@ -34,7 +34,7 @@ import io.swagger.annotations.ApiModelProperty @ApiModel("企业微信多媒体消息") data class WeworkNotifyTextMessage( @ApiModelProperty("接收人Id", required = true) - val receivers: List, + val receivers: Collection, @ApiModelProperty("接收人类型", required = true) val receiverType: WeworkReceiverType, @ApiModelProperty("文本内容类型", required = true) diff --git a/src/backend/ci/core/notify/biz-notify-blueking/src/main/kotlin/com/tencent/devops/notify/blueking/service/inner/WeworkServiceImpl.kt b/src/backend/ci/core/notify/biz-notify-blueking/src/main/kotlin/com/tencent/devops/notify/blueking/service/inner/BlueKingWeworkServiceImpl.kt similarity index 86% rename from src/backend/ci/core/notify/biz-notify-blueking/src/main/kotlin/com/tencent/devops/notify/blueking/service/inner/WeworkServiceImpl.kt rename to src/backend/ci/core/notify/biz-notify-blueking/src/main/kotlin/com/tencent/devops/notify/blueking/service/inner/BlueKingWeworkServiceImpl.kt index 4b11cee4ac6..f017b1a649c 100644 --- a/src/backend/ci/core/notify/biz-notify-blueking/src/main/kotlin/com/tencent/devops/notify/blueking/service/inner/WeworkServiceImpl.kt +++ b/src/backend/ci/core/notify/biz-notify-blueking/src/main/kotlin/com/tencent/devops/notify/blueking/service/inner/BlueKingWeworkServiceImpl.kt @@ -30,11 +30,12 @@ import com.tencent.devops.notify.pojo.WeworkNotifyMediaMessage import com.tencent.devops.notify.pojo.WeworkNotifyTextMessage import com.tencent.devops.notify.service.WeworkService import org.springframework.beans.factory.annotation.Autowired -import org.springframework.stereotype.Service +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.context.annotation.Configuration -@Service -@Suppress("ALL") -class WeworkServiceImpl @Autowired constructor() : WeworkService { +@Configuration +@ConditionalOnProperty(prefix = "notify", name = ["weworkChannel"], havingValue = "blueking") +class BlueKingWeworkServiceImpl @Autowired constructor() : WeworkService { override fun sendMediaMessage(weworkNotifyMediaMessage: WeworkNotifyMediaMessage) { TODO("Not yet implemented") diff --git a/src/backend/ci/core/notify/biz-notify-wework/build.gradle.kts b/src/backend/ci/core/notify/biz-notify-wework/build.gradle.kts new file mode 100644 index 00000000000..1d4f0ede935 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +dependencies { + implementation(project(":core:notify:api-notify")) + implementation(project(":core:notify:biz-notify")) + implementation(project(":core:notify:model-notify")) + implementation(project(":core:common:common-db")) + implementation(project(":core:common:common-notify")) +} diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/config/WeworkConfiguration.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/config/WeworkConfiguration.kt new file mode 100644 index 00000000000..c72e340511c --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/config/WeworkConfiguration.kt @@ -0,0 +1,47 @@ +package com.tencent.devops.notify.wework.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Configuration + +@Configuration +class WeworkConfiguration { + + @Value("\${wework.corpId:}") + lateinit var corpId: String + + @Value("\${wework.corpSecret:}") + lateinit var corpSecret: String + + @Value("\${wework.apiUrl:https://qyapi.weixin.qq.com}") + lateinit var apiUrl: String + + @Value("\${wework.agentId:}") + lateinit var agentId: String + + @Value("\${wework.tempDirectory:}") + lateinit var tempDirectory: String + + /** + * 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 + */ + @Value("\${wework.safe:#{null}}") + val safe: String? = null + + /** + * 表示是否开启重复消息检查,0表示否,1表示是,默认0 + */ + @Value("\${wework.enableDuplicateCheck:#{null}}") + val enableDuplicateCheck: String? = null + + /** + * 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时 + */ + @Value("\${wework.duplicateCheckInterval:#{null}}") + val duplicateCheckInterval: String? = null + + /** + * 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。 + */ + @Value("\${wework.enableIdTrans:#{null}}") + val enableIdTrans: String? = null +} diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/AbstractSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/AbstractSendMessageRequest.kt new file mode 100644 index 00000000000..2184d0a2223 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/AbstractSendMessageRequest.kt @@ -0,0 +1,75 @@ +package com.tencent.devops.notify.wework.pojo + +import com.fasterxml.jackson.annotation.JsonProperty + +@Suppress("UnnecessaryAbstractClass") +abstract class AbstractSendMessageRequest( + /** + * 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 + */ + @JsonProperty("agentid") + @get:JsonProperty("agentid") + open val agentId: Int, + @JsonProperty("duplicate_check_interval") + /** + * 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时 + */ + open val duplicateCheckInterval: Int? = null, + /** + * 表示是否开启重复消息检查,0表示否,1表示是,默认0 + */ + @JsonProperty("enable_duplicate_check") + @get:JsonProperty("enable_duplicate_check") + open val enableDuplicateCheck: Int? = null, + @JsonProperty("msgtype") + @get:JsonProperty("msgtype") + open val msgType: String, + /** + * 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 + */ + @JsonProperty("safe") + @get:JsonProperty("safe") + open val safe: Int? = null, + /** + * 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。 + * 当touser为”@all”时忽略本参数 + */ + @JsonProperty("toparty") + @get:JsonProperty("toparty") + open val toParty: String = "", + /** + * 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。 + * 当touser为”@all”时忽略本参数 + */ + @JsonProperty("totag") + @get:JsonProperty("totag") + open val toTag: String = "", + /** + * 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。 + * 特殊情况:指定为”@all”,则向该企业应用的全部成员发送 + */ + @JsonProperty("touser") + @get:JsonProperty("touser") + open val toUser: String = "" +) { + data class MediaMessageContent( + /** + * 媒体文件id,可以调用上传临时素材接口获取 + */ + @JsonProperty("media_id") + @get:JsonProperty("media_id") + val mediaId: String, + /** + * 视频消息的标题,不超过128个字节,超过会自动截断,视频才有 + */ + @JsonProperty("description") + @get:JsonProperty("description") + var description: String? = null, + /** + * 视频消息的描述,不超过512个字节,超过会自动截断,视频才有 + */ + @JsonProperty("title") + @get:JsonProperty("title") + var title: String? = null + ) +} diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/AccessTokenResp.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/AccessTokenResp.kt new file mode 100644 index 00000000000..122b2ace628 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/AccessTokenResp.kt @@ -0,0 +1,18 @@ +package com.tencent.devops.notify.wework.pojo + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AccessTokenResp( + @JsonProperty("access_token") + val accessToken: String?, + @JsonProperty("errcode") + val errCode: Int?, + @JsonProperty("errmsg") + val errMsg: String?, + @JsonProperty("expires_in") + val expiresIn: Int? +) { + fun isOk(): Boolean { + return errCode == 0 && accessToken != null + } +} diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/FileSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/FileSendMessageRequest.kt new file mode 100644 index 00000000000..e7ed75ed815 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/FileSendMessageRequest.kt @@ -0,0 +1,21 @@ +package com.tencent.devops.notify.wework.pojo + +data class FileSendMessageRequest( + override val agentId: Int, + override val duplicateCheckInterval: Int?, + override val enableDuplicateCheck: Int?, + override val safe: Int?, + override val toParty: String, + override val toTag: String, + override val toUser: String, + val file: MediaMessageContent +) : AbstractSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = duplicateCheckInterval, + enableDuplicateCheck = enableDuplicateCheck, + msgType = "file", + safe = safe, + toParty = toParty, + toTag = toTag, + toUser = toUser +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/ImageSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/ImageSendMessageRequest.kt new file mode 100644 index 00000000000..80a7d138bbf --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/ImageSendMessageRequest.kt @@ -0,0 +1,21 @@ +package com.tencent.devops.notify.wework.pojo + +data class ImageSendMessageRequest( + override val agentId: Int, + override val duplicateCheckInterval: Int?, + override val enableDuplicateCheck: Int?, + override val safe: Int?, + override val toParty: String, + override val toTag: String, + override val toUser: String, + val image: MediaMessageContent +) : AbstractSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = duplicateCheckInterval, + enableDuplicateCheck = enableDuplicateCheck, + msgType = "image", + safe = safe, + toParty = toParty, + toTag = toTag, + toUser = toUser +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/MarkdownSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/MarkdownSendMessageRequest.kt new file mode 100644 index 00000000000..266bcad9a6c --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/MarkdownSendMessageRequest.kt @@ -0,0 +1,21 @@ +package com.tencent.devops.notify.wework.pojo + +data class MarkdownSendMessageRequest( + override val agentId: Int, + override val duplicateCheckInterval: Int?, + override val enableDuplicateCheck: Int?, + override val safe: Int?, + override val toParty: String, + override val toTag: String, + override val toUser: String, + val markdown: TextMessageContent +) : AbstractSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = duplicateCheckInterval, + enableDuplicateCheck = enableDuplicateCheck, + msgType = "markdown", + safe = safe, + toParty = toParty, + toTag = toTag, + toUser = toUser +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/SendMessageResp.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/SendMessageResp.kt new file mode 100644 index 00000000000..cee064a126f --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/SendMessageResp.kt @@ -0,0 +1,16 @@ +package com.tencent.devops.notify.wework.pojo + +import com.fasterxml.jackson.annotation.JsonProperty + +data class SendMessageResp( + @JsonProperty("errcode") + val errCode: Int?, + @JsonProperty("errmsg") + val errMsg: String?, + @JsonProperty("invalidparty") + val invalidParty: String?, + @JsonProperty("invalidtag") + val invalidTag: String?, + @JsonProperty("invaliduser") + val invalidUser: String? +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/TextSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/TextSendMessageRequest.kt new file mode 100644 index 00000000000..6d8150f6143 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/TextSendMessageRequest.kt @@ -0,0 +1,35 @@ +package com.tencent.devops.notify.wework.pojo + +import com.fasterxml.jackson.annotation.JsonProperty + +data class TextSendMessageRequest( + override val agentId: Int, + override val duplicateCheckInterval: Int?, + override val enableDuplicateCheck: Int?, + /** + * 表示是否开启id转译,0表示否,1表示是,默认0。仅第三方应用需要用到,企业自建应用可以忽略。 + */ + @JsonProperty("enable_id_trans") + val enableIdTrans: Int? = null, + override val safe: Int?, + override val toParty: String, + override val toTag: String, + override val toUser: String, + val text: TextMessageContent +) : AbstractSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = duplicateCheckInterval, + enableDuplicateCheck = enableDuplicateCheck, + msgType = "text", + safe = safe, + toParty = toParty, + toTag = toTag, + toUser = toUser +) + +data class TextMessageContent( + /** + * 消息内容,最长不超过2048个字节,超过将截断(支持id转译) + */ + val content: String +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/UploadMediaResp.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/UploadMediaResp.kt new file mode 100644 index 00000000000..33df11e0255 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/UploadMediaResp.kt @@ -0,0 +1,16 @@ +package com.tencent.devops.notify.wework.pojo + +import com.fasterxml.jackson.annotation.JsonProperty + +data class UploadMediaResp( + @JsonProperty("created_at") + val createdAt: String?, + @JsonProperty("errcode") + val errCode: Int?, + @JsonProperty("errmsg") + val errMsg: String?, + @JsonProperty("media_id") + val mediaId: String?, + @JsonProperty("type") + val type: String? +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/VideoSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/VideoSendMessageRequest.kt new file mode 100644 index 00000000000..43b3d7c0557 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/VideoSendMessageRequest.kt @@ -0,0 +1,21 @@ +package com.tencent.devops.notify.wework.pojo + +class VideoSendMessageRequest( + override val agentId: Int, + override val duplicateCheckInterval: Int?, + override val enableDuplicateCheck: Int?, + override val safe: Int?, + override val toParty: String, + override val toTag: String, + override val toUser: String, + val video: MediaMessageContent +) : AbstractSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = duplicateCheckInterval, + enableDuplicateCheck = enableDuplicateCheck, + msgType = "video", + safe = safe, + toParty = toParty, + toTag = toTag, + toUser = toUser +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/VoiceSendMessageRequest.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/VoiceSendMessageRequest.kt new file mode 100644 index 00000000000..2fbcebc89d8 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/pojo/VoiceSendMessageRequest.kt @@ -0,0 +1,21 @@ +package com.tencent.devops.notify.wework.pojo + +data class VoiceSendMessageRequest( + override val agentId: Int, + override val duplicateCheckInterval: Int?, + override val enableDuplicateCheck: Int?, + override val safe: Int?, + override val toParty: String, + override val toTag: String, + override val toUser: String, + val voice: MediaMessageContent +) : AbstractSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = duplicateCheckInterval, + enableDuplicateCheck = enableDuplicateCheck, + msgType = "voice", + safe = safe, + toParty = toParty, + toTag = toTag, + toUser = toUser +) diff --git a/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/service/inner/WeworkServiceImpl.kt b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/service/inner/WeworkServiceImpl.kt new file mode 100644 index 00000000000..faed954bf29 --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify-wework/src/main/kotlin/com/tencent/devops/notify/wework/service/inner/WeworkServiceImpl.kt @@ -0,0 +1,391 @@ +/* + * Tencent is pleased to support the open source community by making BK-CI 蓝鲸持续集成平台 available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * BK-CI 蓝鲸持续集成平台 is licensed under the MIT license. + * + * A copy of the MIT License is included in this file. + * + * + * Terms of the MIT License: + * --------------------------------------------------- + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of + * the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN + * NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.tencent.devops.notify.wework.service.inner + +import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import com.tencent.devops.common.api.exception.OperationException +import com.tencent.devops.common.api.exception.RemoteServiceException +import com.tencent.devops.common.api.util.JsonUtil +import com.tencent.devops.common.api.util.OkhttpUtils +import com.tencent.devops.common.notify.enums.WeworkMediaType +import com.tencent.devops.common.notify.enums.WeworkReceiverType +import com.tencent.devops.common.notify.enums.WeworkTextType +import com.tencent.devops.common.redis.RedisOperation +import com.tencent.devops.notify.EXCHANGE_NOTIFY +import com.tencent.devops.notify.ROUTE_WEWORK +import com.tencent.devops.notify.dao.WeworkNotifyDao +import com.tencent.devops.notify.model.WeworkNotifyMessageWithOperation +import com.tencent.devops.notify.pojo.WeworkNotifyMediaMessage +import com.tencent.devops.notify.pojo.WeworkNotifyTextMessage +import com.tencent.devops.notify.service.WeworkService +import com.tencent.devops.notify.wework.config.WeworkConfiguration +import com.tencent.devops.notify.wework.pojo.AbstractSendMessageRequest +import com.tencent.devops.notify.wework.pojo.AccessTokenResp +import com.tencent.devops.notify.wework.pojo.FileSendMessageRequest +import com.tencent.devops.notify.wework.pojo.ImageSendMessageRequest +import com.tencent.devops.notify.wework.pojo.MarkdownSendMessageRequest +import com.tencent.devops.notify.wework.pojo.SendMessageResp +import com.tencent.devops.notify.wework.pojo.TextMessageContent +import com.tencent.devops.notify.wework.pojo.TextSendMessageRequest +import com.tencent.devops.notify.wework.pojo.UploadMediaResp +import com.tencent.devops.notify.wework.pojo.VideoSendMessageRequest +import com.tencent.devops.notify.wework.pojo.VoiceSendMessageRequest +import org.slf4j.LoggerFactory +import org.springframework.amqp.rabbit.core.RabbitTemplate +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.context.annotation.Configuration +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.Paths +import java.nio.file.StandardCopyOption +import java.util.Optional + +@Configuration +@ConditionalOnProperty(prefix = "notify", name = ["weworkChannel"], havingValue = "weworkAgent") +class WeworkServiceImpl( + private val weWorkConfiguration: WeworkConfiguration, + private val weworkNotifyDao: WeworkNotifyDao, + private val rabbitTemplate: RabbitTemplate, + private val redisOperation: RedisOperation +) : WeworkService { + + companion object { + private val LOG = LoggerFactory.getLogger(WeworkServiceImpl::class.java.name) + private const val WEWORK_ACCESS_TOKEN_KEY = "notify_wework_access_token_key" + } + + override fun sendMqMsg(message: WeworkNotifyMessageWithOperation) { + rabbitTemplate.convertAndSend(EXCHANGE_NOTIFY, ROUTE_WEWORK, message) + } + + override fun sendMediaMessage(weworkNotifyMediaMessage: WeworkNotifyMediaMessage) { + with(weworkNotifyMediaMessage) { + kotlin.runCatching { + val (toUser, toParty) = + receivers.chunkedReceivers(receiverType) + val mediaId = uploadMedia(mediaType, mediaInputStream, mediaName) + val requestBodies = + (toUser.map { getSendMessageRequest(mediaType = mediaType, mediaId = mediaId, toUser = it) } + + toParty.map { getSendMessageRequest(mediaType = mediaType, mediaId = mediaId, toParty = it) }) + .filter { it.isPresent } + doSendRequest(requestBodies) + }.fold({ + LOG.info("send message success, $weworkNotifyMediaMessage") + saveResult(receivers, "media:[type:$mediaType,name:$mediaName]", true, null) + }, { + LOG.warn("send message failed, $weworkNotifyMediaMessage", it) + saveResult(receivers, "media:[type:$mediaType,name:$mediaName]", false, it.message) + }) + } + } + + override fun sendTextMessage(weworkNotifyTextMessage: WeworkNotifyTextMessage) { + with(weworkNotifyTextMessage) { + kotlin.runCatching { + val (toUser, toParty) = + receivers.chunkedReceivers(receiverType) + val requestBodies = (toUser.map { + getSendMessageRequest( + content = message, + toUser = it, + textType = WeworkTextType.text + ) + } + + toParty.map { + getSendMessageRequest( + content = message, + toUser = it, + textType = WeworkTextType.text + ) + }).filter { it.isPresent } + doSendRequest(requestBodies) + }.fold({ + LOG.info("send message success, $weworkNotifyTextMessage") + saveResult(receivers, "type:$textType\n$message", true, null) + }, { + LOG.warn("send message failed, $weworkNotifyTextMessage", it) + saveResult(receivers, "type:$textType\n$message", false, it.message) + }) + } + } + + private fun doSendRequest(requestBodies: List>) { + if (requestBodies.isEmpty()) { + throw OperationException("no message to send") + } + val errMsg = requestBodies.asSequence().map { it.get() }.map { + send(it) + }.filter { it.isPresent }.joinToString(", ") + if (errMsg.isNotBlank()) + throw RemoteServiceException(errMsg) + } + + private fun saveResult(receivers: Collection, body: String, success: Boolean, errMsg: String?) { + weworkNotifyDao.insertOrUpdateWeworkNotifyRecord( + success = success, + lastErrorMessage = errMsg, + receivers = receivers.joinToString(","), + body = body + ) + } + + private fun send(abstractSendMessageRequest: AbstractSendMessageRequest): Optional { + val url = buildUrl("${weWorkConfiguration.apiUrl}/cgi-bin/message/send?access_token=${getAccessToken()}") + val requestBody = JsonUtil.toJson(abstractSendMessageRequest) + return OkhttpUtils.doPost(url, requestBody).use { + val responseBody = it.body()?.string() ?: "" + kotlin.runCatching { + val sendMessageResp = JsonUtil.to(responseBody, jacksonTypeRef()) + if (!it.isSuccessful || 0 != sendMessageResp.errCode) { + throw RemoteServiceException( + httpStatus = it.code(), + responseContent = responseBody, + errorMessage = "send wework message failed" + ) + } + }.fold({ Optional.empty() }, { e -> + LOG.warn("${it.request()}|send wework message failed, $responseBody") + Optional.of(e) + }) + } + } + + /** + * 非文本消息时,默认 mediaId 不为空 + * 文本消息时,默认 content 不为空 + */ + private fun getSendMessageRequest( + mediaType: WeworkMediaType? = null, + mediaId: String? = null, + textType: WeworkTextType? = null, + content: String? = null, + toUser: String = "", + toParty: String = "" + ): Optional { + val agentId = + weWorkConfiguration.agentId.toIntOrNull() ?: throw OperationException("Wework agent id is invalid") + return if (mediaType != null && mediaId != null) + when (mediaType) { + WeworkMediaType.file -> { + Optional.of( + FileSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = weWorkConfiguration.duplicateCheckInterval?.toIntOrNull(), + enableDuplicateCheck = weWorkConfiguration.enableDuplicateCheck?.toIntOrNull(), + safe = weWorkConfiguration.safe?.toIntOrNull(), + toParty = toParty, + toTag = "", + toUser = toUser, + file = AbstractSendMessageRequest.MediaMessageContent(mediaId) + ) + ) + } + WeworkMediaType.image -> { + Optional.of( + ImageSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = weWorkConfiguration.duplicateCheckInterval?.toIntOrNull(), + enableDuplicateCheck = weWorkConfiguration.enableDuplicateCheck?.toIntOrNull(), + safe = weWorkConfiguration.safe?.toIntOrNull(), + toParty = toParty, + toTag = "", + toUser = toUser, + image = AbstractSendMessageRequest.MediaMessageContent(mediaId) + ) + ) + } + WeworkMediaType.video -> { + Optional.of( + VideoSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = weWorkConfiguration.duplicateCheckInterval?.toIntOrNull(), + enableDuplicateCheck = weWorkConfiguration.enableDuplicateCheck?.toIntOrNull(), + safe = weWorkConfiguration.safe?.toIntOrNull(), + toParty = toParty, + toTag = "", + toUser = toUser, + video = AbstractSendMessageRequest.MediaMessageContent(mediaId) + ) + ) + } + WeworkMediaType.voice -> { + Optional.of( + VoiceSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = weWorkConfiguration.duplicateCheckInterval?.toIntOrNull(), + enableDuplicateCheck = weWorkConfiguration.enableDuplicateCheck?.toIntOrNull(), + safe = weWorkConfiguration.safe?.toIntOrNull(), + toParty = toParty, + toTag = "", + toUser = toUser, + voice = AbstractSendMessageRequest.MediaMessageContent(mediaId) + ) + ) + } + } else if (textType != null && content != null) { + when (textType) { + WeworkTextType.text -> Optional.of( + TextSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = weWorkConfiguration.duplicateCheckInterval?.toIntOrNull(), + enableDuplicateCheck = weWorkConfiguration.enableDuplicateCheck?.toIntOrNull(), + enableIdTrans = weWorkConfiguration.enableIdTrans?.toIntOrNull(), + safe = weWorkConfiguration.safe?.toIntOrNull(), + toParty = toParty, + toTag = "", + toUser = toUser, + text = TextMessageContent(content) + ) + ) + WeworkTextType.markdown -> Optional.of( + MarkdownSendMessageRequest( + agentId = agentId, + duplicateCheckInterval = weWorkConfiguration.duplicateCheckInterval?.toIntOrNull(), + enableDuplicateCheck = weWorkConfiguration.enableDuplicateCheck?.toIntOrNull(), + safe = weWorkConfiguration.safe?.toIntOrNull(), + toParty = toParty, + toTag = "", + toUser = toUser, + markdown = TextMessageContent(content) + ) + ) + } + } else Optional.empty() + } + + /** + * [发送应用消息](https://work.weixin.qq.com/api/doc/90000/90135/90236#%E6%96%87%E6%9C%AC%E6%B6%88%E6%81%AF) + * 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个) + * 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。 + * @return first 为 toUser, second 为 toParty + */ + private fun Collection.chunkedReceivers( + receiverType: WeworkReceiverType + ): Pair, List> { + return when (receiverType) { + WeworkReceiverType.single -> { + val toUser = HashSet(this).chunked(1000) + .map { it.joinToString(separator = "|") } + Pair(toUser, emptyList()) + } + WeworkReceiverType.group -> { + val toParty = HashSet(this).chunked(100).map { it.joinToString(separator = "|") } + Pair(emptyList(), toParty) + } + } + } + + private fun uploadMedia(mediaType: WeworkMediaType, inputStream: InputStream, mediaName: String): String { + val tempDirectory = weWorkConfiguration.tempDirectory + val tempFile = Files.createTempFile(Paths.get(tempDirectory), mediaName, "") + try { + Files.copy(inputStream, tempFile, StandardCopyOption.REPLACE_EXISTING) + val token = getAccessToken() + val url = + buildUrl("${weWorkConfiguration.apiUrl}/cgi-bin/media/upload?access_token=$token&type=$mediaType") + OkhttpUtils.uploadFile( + url = url, + uploadFile = tempFile.toFile(), + fileFieldName = "media", + fileName = mediaName + ).use { + val responseBody = it.body()?.string() ?: "{}" + return kotlin.runCatching { + val uploadMediaResp = JsonUtil.to(responseBody, jacksonTypeRef()) + val mediaId = uploadMediaResp.mediaId + if (!it.isSuccessful || mediaId.isNullOrBlank()) { + LOG.warn("${it.request()}|upload media($mediaName) to wework failed, $responseBody") + throw RemoteServiceException( + httpStatus = it.code(), + responseContent = responseBody, + errorMessage = "upload media($mediaName) to wework failed" + ) + } + mediaId + }.onFailure { _ -> + LOG.warn("${it.request()}|upload media($mediaName) to wework failed, $responseBody") + }.getOrThrow()!! + } + } finally { + Files.deleteIfExists(tempFile) + } + } + + fun getAccessToken(): String { + val now = System.currentTimeMillis() + // 获取缓存中有效的 token,有申请频率限制 + val accessTokenCache = redisOperation.get(WEWORK_ACCESS_TOKEN_KEY) + ?.let { json -> + JsonUtil.to(json, jacksonTypeRef()) + .takeIf { it.expiresIn * 1000L + now < it.timestamp } + } + if (accessTokenCache != null) return accessTokenCache.accessToken + else redisOperation.delete(WEWORK_ACCESS_TOKEN_KEY) + OkhttpUtils.doGet( + buildUrl( + "${weWorkConfiguration.apiUrl}/cgi-bin" + + "/gettoken?corpId=${weWorkConfiguration.corpId}&corpSecret=${weWorkConfiguration.corpSecret}" + ) + ).use { + val responseBody = it.body()?.string() ?: "{}" + return kotlin.runCatching { + val accessTokenResp = JsonUtil.to(responseBody, jacksonTypeRef()) + if (!it.isSuccessful && accessTokenResp.isOk()) { + LOG.warn("${it.request()}|failed to get wework access token: $responseBody") + throw RemoteServiceException( + httpStatus = it.code(), + responseContent = responseBody, + errorMessage = "failed to get wework access token: $responseBody" + ) + } + val accessToken = accessTokenResp.accessToken!! + val expiresIn = accessTokenResp.expiresIn + if (expiresIn != null && expiresIn > 600) { + // 提前 10 分钟过期,防止在操作过程中过期 + redisOperation.set( + key = WEWORK_ACCESS_TOKEN_KEY, + value = JsonUtil.toJson(AccessTokenCache(accessToken, expiresIn, now)), + expiredInSecond = expiresIn.toLong() - 600 + ) + } + accessToken + }.onFailure { _ -> + LOG.warn("${it.request()}|failed to get wework access token: $responseBody") + }.getOrThrow() + } + } + + private fun buildUrl(url: String): String { + return if (url.startsWith("http")) url else "https://$url" + } + + private data class AccessTokenCache( + val accessToken: String, + val expiresIn: Int, + val timestamp: Long + ) +} diff --git a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/consumer/NotifyMessageConsumer.kt b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/consumer/NotifyMessageConsumer.kt index fa8d49ae2fb..8c466c84c8f 100644 --- a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/consumer/NotifyMessageConsumer.kt +++ b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/consumer/NotifyMessageConsumer.kt @@ -26,24 +26,31 @@ */ package com.tencent.devops.notify.consumer +import com.tencent.devops.common.notify.enums.WeworkReceiverType +import com.tencent.devops.common.notify.enums.WeworkTextType import com.tencent.devops.notify.EXCHANGE_NOTIFY import com.tencent.devops.notify.QUEUE_NOTIFY_EMAIL import com.tencent.devops.notify.QUEUE_NOTIFY_RTX import com.tencent.devops.notify.QUEUE_NOTIFY_SMS import com.tencent.devops.notify.QUEUE_NOTIFY_WECHAT +import com.tencent.devops.notify.QUEUE_NOTIFY_WEWORK import com.tencent.devops.notify.ROUTE_EMAIL import com.tencent.devops.notify.ROUTE_RTX import com.tencent.devops.notify.ROUTE_SMS import com.tencent.devops.notify.ROUTE_WECHAT +import com.tencent.devops.notify.ROUTE_WEWORK import com.tencent.devops.notify.model.EmailNotifyMessageWithOperation import com.tencent.devops.notify.model.RtxNotifyMessageWithOperation import com.tencent.devops.notify.model.SmsNotifyMessageWithOperation import com.tencent.devops.notify.model.WechatNotifyMessageWithOperation +import com.tencent.devops.notify.model.WeworkNotifyMessageWithOperation +import com.tencent.devops.notify.pojo.WeworkNotifyTextMessage import com.tencent.devops.notify.service.EmailService import com.tencent.devops.notify.service.OrgService import com.tencent.devops.notify.service.RtxService import com.tencent.devops.notify.service.SmsService import com.tencent.devops.notify.service.WechatService +import com.tencent.devops.notify.service.WeworkService import org.slf4j.LoggerFactory import org.springframework.amqp.rabbit.annotation.Exchange import org.springframework.amqp.rabbit.annotation.Queue @@ -58,6 +65,7 @@ class NotifyMessageConsumer @Autowired constructor( private val emailService: EmailService, private val smsService: SmsService, private val wechatService: WechatService, + private val weworkService: WeworkService, private val orgService: OrgService ) { companion object { @@ -139,4 +147,30 @@ class NotifyMessageConsumer @Autowired constructor( logger.warn("Failed process received Wechat message", ignored) } } + + @RabbitListener( + containerFactory = "rabbitListenerContainerFactory", + bindings = [ + QueueBinding( + key = [ROUTE_WEWORK], + value = Queue(value = QUEUE_NOTIFY_WEWORK, durable = "true"), + exchange = Exchange(value = EXCHANGE_NOTIFY, durable = "true", delayed = "true", type = "topic") + )] + ) + fun onReceiveWeworkMessage(weworkNotifyMessageWithOperation: WeworkNotifyMessageWithOperation) { + try { + val parseStaff = orgService.parseStaff(weworkNotifyMessageWithOperation.getReceivers()) + weworkNotifyMessageWithOperation.clearReceivers() + weworkNotifyMessageWithOperation.addAllReceivers(parseStaff) + val weworkNotifyTextMessage = WeworkNotifyTextMessage( + receivers = parseStaff, + receiverType = WeworkReceiverType.single, + textType = WeworkTextType.text, + message = weworkNotifyMessageWithOperation.body + ) + weworkService.sendTextMessage(weworkNotifyTextMessage) + } catch (ignored: Exception) { + logger.warn("Failed process received Wework message", ignored) + } + } } diff --git a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/NotifyMessageTemplateDao.kt b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/NotifyMessageTemplateDao.kt index eff94f89691..1fa4c403743 100644 --- a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/NotifyMessageTemplateDao.kt +++ b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/NotifyMessageTemplateDao.kt @@ -31,10 +31,12 @@ import com.tencent.devops.model.notify.tables.TCommonNotifyMessageTemplate import com.tencent.devops.model.notify.tables.TEmailsNotifyMessageTemplate import com.tencent.devops.model.notify.tables.TRtxNotifyMessageTemplate import com.tencent.devops.model.notify.tables.TWechatNotifyMessageTemplate +import com.tencent.devops.model.notify.tables.TWeworkNotifyMessageTemplate import com.tencent.devops.model.notify.tables.records.TCommonNotifyMessageTemplateRecord import com.tencent.devops.model.notify.tables.records.TEmailsNotifyMessageTemplateRecord import com.tencent.devops.model.notify.tables.records.TRtxNotifyMessageTemplateRecord import com.tencent.devops.model.notify.tables.records.TWechatNotifyMessageTemplateRecord +import com.tencent.devops.model.notify.tables.records.TWeworkNotifyMessageTemplateRecord import com.tencent.devops.notify.pojo.NotifyTemplateMessage import com.tencent.devops.notify.pojo.NotifyTemplateMessageRequest import org.jooq.Condition @@ -139,6 +141,24 @@ class NotifyMessageTemplateDao { } } + /** + * 获取企业微信消息模板 新版 + * @param dslContext 数据库操作对象 + * @param commonTemplateId + */ + fun getWeworkNotifyMessageTemplate( + dslContext: DSLContext, + commonTemplateId: String + ): TWeworkNotifyMessageTemplateRecord? { + with(TWeworkNotifyMessageTemplate.T_WEWORK_NOTIFY_MESSAGE_TEMPLATE) { + val conditions = mutableListOf() + conditions.add(COMMON_TEMPLATE_ID.contains(commonTemplateId)) + return dslContext.selectFrom(this) + .where(conditions) + .fetchOne() + } + } + /** * 根据模板代码获取模板公共信息 */ diff --git a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/WeworkNotifyDao.kt b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/WeworkNotifyDao.kt new file mode 100644 index 00000000000..4224edb162b --- /dev/null +++ b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/dao/WeworkNotifyDao.kt @@ -0,0 +1,80 @@ +package com.tencent.devops.notify.dao + +import com.tencent.devops.common.api.util.PageUtil +import com.tencent.devops.model.notify.tables.TNotifyWework +import com.tencent.devops.model.notify.tables.records.TNotifyWeworkRecord +import org.jooq.Condition +import org.jooq.DSLContext +import org.jooq.Result +import org.springframework.stereotype.Repository +import java.time.LocalDateTime + +@Repository +class WeworkNotifyDao( + private val dslContext: DSLContext +) { + + fun insertOrUpdateWeworkNotifyRecord( + success: Boolean, + lastErrorMessage: String?, + receivers: String?, + body: String? + ) { + val now = LocalDateTime.now() + dslContext.insertInto( + TNotifyWework.T_NOTIFY_WEWORK, + TNotifyWework.T_NOTIFY_WEWORK.LAST_ERROR, + TNotifyWework.T_NOTIFY_WEWORK.BODY, + TNotifyWework.T_NOTIFY_WEWORK.UPDATED_TIME, + TNotifyWework.T_NOTIFY_WEWORK.CREATED_TIME, + TNotifyWework.T_NOTIFY_WEWORK.RECEIVERS, + TNotifyWework.T_NOTIFY_WEWORK.SUCCESS + ) + .values( + lastErrorMessage, + body, + now, + now, + receivers, + success + ) + .execute() + } + + fun count(success: Boolean?, fromSysId: String?): Int { + return dslContext.selectCount() + .from(TNotifyWework.T_NOTIFY_WEWORK) + .where(getListConditions(success)) + .fetchOne()!! + .value1() + } + + fun list( + page: Int, + pageSize: Int, + success: Boolean?, + fromSysId: String?, + createdTimeSortOrder: String? + ): Result { + val sqlLimit = PageUtil.convertPageSizeToSQLLimit(page, pageSize) + return dslContext.selectFrom(TNotifyWework.T_NOTIFY_WEWORK) + .where(getListConditions(success)) + .orderBy( + if (createdTimeSortOrder != null && createdTimeSortOrder == "descend") { + TNotifyWework.T_NOTIFY_WEWORK.CREATED_TIME.desc() + } else { + TNotifyWework.T_NOTIFY_WEWORK.CREATED_TIME.asc() + } + ) + .limit(sqlLimit.offset, sqlLimit.limit) + .fetch() + } + + private fun getListConditions(success: Boolean?): List { + val conditions = ArrayList() + if (success != null) { + conditions.add(TNotifyWework.T_NOTIFY_WEWORK.SUCCESS.eq(success)) + } + return conditions + } +} diff --git a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/NotifyMessageTemplateServiceImpl.kt b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/NotifyMessageTemplateServiceImpl.kt index 41a2bc7f6f9..844fdfb84ee 100644 --- a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/NotifyMessageTemplateServiceImpl.kt +++ b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/NotifyMessageTemplateServiceImpl.kt @@ -42,15 +42,16 @@ import com.tencent.devops.common.service.utils.MessageCodeUtil import com.tencent.devops.model.notify.tables.records.TCommonNotifyMessageTemplateRecord import com.tencent.devops.notify.dao.CommonNotifyMessageTemplateDao import com.tencent.devops.notify.dao.NotifyMessageTemplateDao +import com.tencent.devops.notify.model.WeworkNotifyMessageWithOperation +import com.tencent.devops.notify.pojo.EmailNotifyMessage +import com.tencent.devops.notify.pojo.NotifyContext import com.tencent.devops.notify.pojo.NotifyMessageCommonTemplate +import com.tencent.devops.notify.pojo.NotifyMessageContextRequest import com.tencent.devops.notify.pojo.NotifyTemplateMessageRequest -import com.tencent.devops.notify.pojo.EmailNotifyMessage import com.tencent.devops.notify.pojo.RtxNotifyMessage import com.tencent.devops.notify.pojo.SendNotifyMessageTemplateRequest -import com.tencent.devops.notify.pojo.NotifyMessageContextRequest import com.tencent.devops.notify.pojo.SubNotifyMessageTemplate import com.tencent.devops.notify.pojo.WechatNotifyMessage -import com.tencent.devops.notify.pojo.NotifyContext import org.jooq.DSLContext import org.jooq.impl.DSL import org.slf4j.LoggerFactory @@ -66,7 +67,8 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( private val commonNotifyMessageTemplateDao: CommonNotifyMessageTemplateDao, private val emailService: EmailService, private val rtxService: RtxService, - private val wechatService: WechatService + private val wechatService: WechatService, + private val weworkService: WeworkService ) : NotifyMessageTemplateService { private val logger = LoggerFactory.getLogger(NotifyMessageTemplateServiceImpl::class.java) @@ -301,14 +303,14 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( * 更新消息通知模板信息 * @param userId 用户ID * @param templateId 模板ID - * @param addNotifyMessageTemplateRequest 消息模板更新内容 + * @param notifyMessageTemplateRequest 消息模板更新内容 */ override fun updateNotifyMessageTemplate( userId: String, templateId: String, - addNotifyMessageTemplateRequest: NotifyTemplateMessageRequest + notifyMessageTemplateRequest: NotifyTemplateMessageRequest ): Result { - if (addNotifyMessageTemplateRequest.msg.size > 3) { + if (notifyMessageTemplateRequest.msg.size > 3) { return MessageCodeUtil.generateResponseDataObject( messageCode = CommonMessageCode.PARAMETER_IS_INVALID, params = arrayOf("TplNum"), @@ -320,7 +322,7 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( var hasWechat = false val notifyTypeScopeSet = mutableSetOf() // 判断提交的数据中是否存在同样类型的 - addNotifyMessageTemplateRequest.msg.forEach { + notifyMessageTemplateRequest.msg.forEach { if (it.notifyTypeScope.contains(NotifyType.EMAIL.name) && !hasEmail) { hasEmail = true notifyTypeScopeSet.add(NotifyType.EMAIL.name) @@ -361,12 +363,12 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( notifyMessageTemplateDao.updateCommonNotifyMessageTemplate( dslContext = context, templateId = templateId, - notifyMessageTemplateRequest = addNotifyMessageTemplateRequest, + notifyMessageTemplateRequest = notifyMessageTemplateRequest, notifyTypeScopeSet = notifyTypeScopeSet ) val uid = UUIDUtil.generate() // 根据模板类型向消息模板信息表中添加信息 - addNotifyMessageTemplateRequest.msg.forEach { + notifyMessageTemplateRequest.msg.forEach { if (it.notifyTypeScope.contains(NotifyType.WECHAT.name)) { val num = notifyMessageTemplateDao.countWechatMessageTemplate(dslContext, templateId) if (num > 0) { @@ -521,7 +523,7 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( // 企业微信消息 if (sendAllNotify || request.notifyType?.contains(NotifyType.RTX.name) == true) { if (!notifyTypeScope.contains(NotifyType.RTX.name)) { - logger.error("NotifyTemplate|NOT_FOUND|type=${NotifyType.EMAIL}|template=${request.templateCode}") + logger.error("NotifyTemplate|NOT_FOUND|type=${NotifyType.RTX}|template=${request.templateCode}") } else { val rtxTplRecord = notifyMessageTemplateDao.getRtxNotifyMessageTemplate( @@ -545,7 +547,7 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( // 微信消息 if (sendAllNotify || request.notifyType?.contains(NotifyType.WECHAT.name) == true) { if (!notifyTypeScope.contains(NotifyType.WECHAT.name)) { - logger.error("NotifyTemplate|NOT_FOUND|type=${NotifyType.EMAIL}|template=${request.templateCode}") + logger.error("NotifyTemplate|NOT_FOUND|type=${NotifyType.WECHAT}|template=${request.templateCode}") } else { val wechatTplRecord = notifyMessageTemplateDao.getWechatNotifyMessageTemplate( dslContext = dslContext, @@ -561,6 +563,26 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( ) } } + + // 新企业微信实现 + if (sendAllNotify || request.notifyType?.contains(NotifyType.WEWORK.name) == true) { + if (!notifyTypeScope.contains(NotifyType.WEWORK.name)) { + logger.error("NotifyTemplate|NOT_FOUND|type=${NotifyType.WEWORK}|template=${request.templateCode}") + } else { + val weworkTplRecord = notifyMessageTemplateDao.getWeworkNotifyMessageTemplate( + dslContext = dslContext, + commonTemplateId = commonNotifyMessageTemplateRecord.id + )!! + // 替换内容里的动态参数 + val body = replaceContentParams(request.bodyParams, weworkTplRecord.body) + sendWeworkNotifyMessage( + commonNotifyMessageTemplate = commonNotifyMessageTemplateRecord, + sendNotifyMessageTemplateRequest = request, + body = body, + sender = weworkTplRecord.sender + ) + } + } return Result(true) } @@ -678,6 +700,22 @@ class NotifyMessageTemplateServiceImpl @Autowired constructor( emailService.sendMqMsg(emailNotifyMessage) } + private fun sendWeworkNotifyMessage( + commonNotifyMessageTemplate: TCommonNotifyMessageTemplateRecord, + sendNotifyMessageTemplateRequest: SendNotifyMessageTemplateRequest, + body: String, + sender: String + ) { + val wechatNotifyMessage = WeworkNotifyMessageWithOperation() + wechatNotifyMessage.sender = sender + wechatNotifyMessage.addAllReceivers(sendNotifyMessageTemplateRequest.receivers) + wechatNotifyMessage.body = body + wechatNotifyMessage.priority = EnumNotifyPriority.parse(commonNotifyMessageTemplate.priority.toString()) + wechatNotifyMessage.source = EnumNotifySource.parse(commonNotifyMessageTemplate.source.toInt()) + ?: EnumNotifySource.BUSINESS_LOGIC + weworkService.sendMqMsg(wechatNotifyMessage) + } + private fun replaceContentParams(params: Map?, content: String): String { var content1 = content params?.forEach { paramName, paramValue -> diff --git a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/WeworkService.kt b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/WeworkService.kt index 8564cda7b80..0300dd5240c 100644 --- a/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/WeworkService.kt +++ b/src/backend/ci/core/notify/biz-notify/src/main/kotlin/com/tencent/devops/notify/service/WeworkService.kt @@ -26,11 +26,14 @@ */ package com.tencent.devops.notify.service +import com.tencent.devops.notify.model.WeworkNotifyMessageWithOperation import com.tencent.devops.notify.pojo.WeworkNotifyMediaMessage import com.tencent.devops.notify.pojo.WeworkNotifyTextMessage interface WeworkService { + fun sendMqMsg(message: WeworkNotifyMessageWithOperation) = Unit + fun sendMediaMessage(weworkNotifyMediaMessage: WeworkNotifyMediaMessage) fun sendTextMessage(weworkNotifyTextMessage: WeworkNotifyTextMessage) diff --git a/src/backend/ci/core/notify/boot-notify/build.gradle.kts b/src/backend/ci/core/notify/boot-notify/build.gradle.kts index 819752fd069..05726198727 100644 --- a/src/backend/ci/core/notify/boot-notify/build.gradle.kts +++ b/src/backend/ci/core/notify/boot-notify/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { api(project(":core:notify:biz-notify")) // 开源版实现 api(project(":core:notify:biz-notify-blueking")) // 对接蓝鲸实现 + implementation(project(":core:notify:biz-notify-wework")) // 对接企业微信实现 } plugins { diff --git a/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/pojo/pipeline/PipelineSubscriptionType.kt b/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/pojo/pipeline/PipelineSubscriptionType.kt index 88755bc7082..d26c1c68c12 100644 --- a/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/pojo/pipeline/PipelineSubscriptionType.kt +++ b/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/pojo/pipeline/PipelineSubscriptionType.kt @@ -34,5 +34,6 @@ enum class PipelineSubscriptionType { EMAIL, RTX, WECHAT, - SMS + SMS, + WEWORK } diff --git a/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineInfoExtService.kt b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineInfoExtService.kt new file mode 100644 index 00000000000..16f6e32065f --- /dev/null +++ b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineInfoExtService.kt @@ -0,0 +1,5 @@ +package com.tencent.devops.process.engine.service + +interface PipelineInfoExtService { + fun failNotifyChannel(): String +} diff --git a/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineRepositoryService.kt b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineRepositoryService.kt index 8f2a157d5d6..d6f9830a84b 100644 --- a/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineRepositoryService.kt +++ b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineRepositoryService.kt @@ -36,7 +36,6 @@ import com.tencent.devops.common.api.util.JsonUtil import com.tencent.devops.common.api.util.UUIDUtil import com.tencent.devops.common.event.dispatcher.pipeline.PipelineEventDispatcher import com.tencent.devops.common.event.pojo.pipeline.PipelineModelAnalysisEvent -import com.tencent.devops.common.notify.enums.NotifyType import com.tencent.devops.common.pipeline.Model import com.tencent.devops.common.pipeline.container.NormalContainer import com.tencent.devops.common.pipeline.container.Stage @@ -111,7 +110,8 @@ class PipelineRepositoryService constructor( private val templatePipelineDao: TemplatePipelineDao, private val pipelineResVersionDao: PipelineResVersionDao, private val pipelineSettingVersionDao: PipelineSettingVersionDao, - private val versionConfigure: VersionConfigure + private val versionConfigure: VersionConfigure, + private val pipelineInfoExtService: PipelineInfoExtService ) { fun deployPipeline( @@ -567,7 +567,7 @@ class PipelineRepositoryService constructor( // 蓝盾正常的BS渠道的默认没设置setting的,将发通知改成失败才发通知 // 而其他渠道的默认没设置则什么通知都设置为不发 val notifyTypes = if (channelCode == ChannelCode.BS) { - "${NotifyType.EMAIL.name},${NotifyType.RTX.name}" + pipelineInfoExtService.failNotifyChannel() } else { "" } diff --git a/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/init/PipelineServiceConfigure.kt b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/init/PipelineServiceConfigure.kt index 0d5e1d689f9..e0668adbf59 100644 --- a/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/init/PipelineServiceConfigure.kt +++ b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/init/PipelineServiceConfigure.kt @@ -28,9 +28,11 @@ package com.tencent.devops.process.init import com.tencent.devops.process.engine.service.PipelineBuildExtService +import com.tencent.devops.process.engine.service.PipelineInfoExtService import com.tencent.devops.process.engine.service.PipelinePauseExtService import com.tencent.devops.process.service.PipelineBuildExtServiceImpl import com.tencent.devops.process.service.PipelineContextService +import com.tencent.devops.process.service.PipelineInfoExtServiceImpl import com.tencent.devops.process.service.PipelinePauseExtServiceImpl import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.AutoConfigureOrder @@ -53,4 +55,8 @@ class PipelineServiceConfigure { @Bean @ConditionalOnMissingBean(PipelinePauseExtService::class) fun pipelinePauseExtService() = PipelinePauseExtServiceImpl() + + @Bean + @ConditionalOnMissingBean(PipelineInfoExtService::class) + fun pipelineInfoExtService() = PipelineInfoExtServiceImpl() } diff --git a/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/service/PipelineInfoExtServiceImpl.kt b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/service/PipelineInfoExtServiceImpl.kt new file mode 100644 index 00000000000..5a75758d1b3 --- /dev/null +++ b/src/backend/ci/core/process/biz-base/src/main/kotlin/com/tencent/devops/process/service/PipelineInfoExtServiceImpl.kt @@ -0,0 +1,10 @@ +package com.tencent.devops.process.service + +import com.tencent.devops.common.notify.enums.NotifyType +import com.tencent.devops.process.engine.service.PipelineInfoExtService + +class PipelineInfoExtServiceImpl : PipelineInfoExtService { + override fun failNotifyChannel(): String { + return "${NotifyType.WEWORK.name}" + } +} diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/init/PipelineNotifyQueueConfiguration.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/init/PipelineNotifyQueueConfiguration.kt new file mode 100644 index 00000000000..b7ce7b6e23f --- /dev/null +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/init/PipelineNotifyQueueConfiguration.kt @@ -0,0 +1,62 @@ +package com.tencent.devops.process.engine.init + +import com.tencent.devops.common.event.dispatcher.pipeline.mq.MQ +import com.tencent.devops.common.event.dispatcher.pipeline.mq.Tools +import com.tencent.devops.process.engine.listener.run.PipelineNotifyQueueListener +import org.springframework.amqp.core.Binding +import org.springframework.amqp.core.BindingBuilder +import org.springframework.amqp.core.FanoutExchange +import org.springframework.amqp.core.Queue +import org.springframework.amqp.rabbit.connection.ConnectionFactory +import org.springframework.amqp.rabbit.core.RabbitAdmin +import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +@Configuration +class PipelineNotifyQueueConfiguration { + + @Bean + @ConditionalOnMissingBean(name = ["pipelineBuildFanoutExchange"]) + fun pipelineBuildFanoutExchange(): FanoutExchange { + val fanoutExchange = FanoutExchange(MQ.EXCHANGE_PIPELINE_BUILD_FINISH_FANOUT, true, false) + fanoutExchange.isDelayed = true + return fanoutExchange + } + + @Bean + fun notifyQueueBuildFinishQueue(): Queue { + return Queue(MQ.QUEUE_PIPELINE_BUILD_FINISH_NOTIFY_QUEUE) + } + + @Bean + fun notifyQueueBuildFinishBind( + notifyQueueBuildFinishQueue: Queue, + pipelineBuildFanoutExchange: FanoutExchange + ): Binding { + return BindingBuilder.bind(notifyQueueBuildFinishQueue).to(pipelineBuildFanoutExchange) + } + + @Bean + fun notifyQueueBuildFinishListenerContainer( + connectionFactory: ConnectionFactory, + notifyQueueBuildFinishQueue: Queue, + rabbitAdmin: RabbitAdmin, + buildListener: PipelineNotifyQueueListener, + messageConverter: Jackson2JsonMessageConverter + ): SimpleMessageListenerContainer { + return Tools.createSimpleMessageListenerContainer( + connectionFactory = connectionFactory, + queue = notifyQueueBuildFinishQueue, + rabbitAdmin = rabbitAdmin, + buildListener = buildListener, + messageConverter = messageConverter, + startConsumerMinInterval = 10000, + consecutiveActiveTrigger = 5, + concurrency = 1, + maxConcurrency = 5 + ) + } +} diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/listener/run/PipelineNotifyQueueListener.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/listener/run/PipelineNotifyQueueListener.kt new file mode 100644 index 00000000000..600c72f7ce5 --- /dev/null +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/listener/run/PipelineNotifyQueueListener.kt @@ -0,0 +1,58 @@ +package com.tencent.devops.process.engine.listener.run + +import com.tencent.devops.common.event.dispatcher.pipeline.PipelineEventDispatcher +import com.tencent.devops.common.event.listener.pipeline.BaseListener +import com.tencent.devops.common.event.pojo.pipeline.PipelineBuildFinishBroadCastEvent +import com.tencent.devops.common.pipeline.enums.BuildStatus +import com.tencent.devops.common.service.trace.TraceTag +import com.tencent.devops.process.engine.service.PipelineSubscriptionService +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import org.springframework.stereotype.Component + +@Component +class PipelineNotifyQueueListener( + private val pipelineSubscriptionService: PipelineSubscriptionService, + pipelineEventDispatcher: PipelineEventDispatcher +) : BaseListener(pipelineEventDispatcher) { + + override fun execute(event: PipelineBuildFinishBroadCastEvent) { + try { + val traceId = MDC.get(TraceTag.BIZID) + if (traceId.isNullOrEmpty()) { + if (!event.traceId.isNullOrEmpty()) { + MDC.put(TraceTag.BIZID, event.traceId) + } else { + MDC.put(TraceTag.BIZID, TraceTag.buildBiz()) + } + } + this.onPipelineShutdown(event) + } finally { + MDC.remove(TraceTag.BIZID) + } + } + + override fun run(event: PipelineBuildFinishBroadCastEvent) { + this.execute(event) + } + + private fun onPipelineShutdown(event: PipelineBuildFinishBroadCastEvent, retryCount: Int = 0) { + if (retryCount < 3) { + try { + with(event) { + pipelineSubscriptionService.onPipelineShutdown(pipelineId = pipelineId, + buildId = buildId, + projectId = projectId, + buildStatus = BuildStatus.parse(status)) + } + } catch (e: Exception) { + LOG.warn("${event.buildId}|pipeline notify failed, retry count $retryCount", e) + this.onPipelineShutdown(event, retryCount + 1) + } + } + } + + companion object { + private val LOG = LoggerFactory.getLogger(PipelineNotifyQueueListener::class.java) + } +} diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineSubscriptionService.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineSubscriptionService.kt new file mode 100644 index 00000000000..879c59b31bc --- /dev/null +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/engine/service/PipelineSubscriptionService.kt @@ -0,0 +1,276 @@ +package com.tencent.devops.process.engine.service + +import com.tencent.devops.common.api.pojo.Result +import com.tencent.devops.common.api.util.DateTimeUtil +import com.tencent.devops.common.api.util.EnvUtils +import com.tencent.devops.common.client.Client +import com.tencent.devops.common.pipeline.enums.BuildStatus +import com.tencent.devops.common.pipeline.enums.ChannelCode +import com.tencent.devops.common.pipeline.enums.StartType +import com.tencent.devops.common.pipeline.pojo.element.agent.LinuxCodeCCScriptElement +import com.tencent.devops.common.pipeline.pojo.element.agent.LinuxPaasCodeCCScriptElement +import com.tencent.devops.common.service.utils.HomeHostUtil +import com.tencent.devops.notify.api.service.ServiceNotifyMessageTemplateResource +import com.tencent.devops.notify.pojo.SendNotifyMessageTemplateRequest +import com.tencent.devops.process.dao.PipelineSettingDao +import com.tencent.devops.process.pojo.PipelineNotifyTemplateEnum.PIPELINE_SHUTDOWN_FAILURE_NOTIFY_TEMPLATE +import com.tencent.devops.process.pojo.PipelineNotifyTemplateEnum.PIPELINE_SHUTDOWN_SUCCESS_NOTIFY_TEMPLATE +import com.tencent.devops.process.pojo.pipeline.ModelDetail +import com.tencent.devops.process.service.BuildVariableService +import com.tencent.devops.process.service.builds.PipelineBuildFacadeService +import com.tencent.devops.process.util.NotifyTemplateUtils +import com.tencent.devops.process.utils.PIPELINE_BUILD_NUM +import com.tencent.devops.process.utils.PIPELINE_START_CHANNEL +import com.tencent.devops.process.utils.PIPELINE_START_MOBILE +import com.tencent.devops.process.utils.PIPELINE_START_PIPELINE_USER_ID +import com.tencent.devops.process.utils.PIPELINE_START_TYPE +import com.tencent.devops.process.utils.PIPELINE_START_USER_ID +import com.tencent.devops.process.utils.PIPELINE_START_WEBHOOK_USER_ID +import com.tencent.devops.process.utils.PIPELINE_TIME_DURATION +import com.tencent.devops.process.utils.PIPELINE_VERSION +import com.tencent.devops.project.api.service.ServiceProjectResource +import org.jooq.DSLContext +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Service +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Date + +@Service +class PipelineSubscriptionService constructor( + private val dslContext: DSLContext, + private val pipelineSettingDao: PipelineSettingDao, + private val pipelineRuntimeService: PipelineRuntimeService, + private val pipelineRepositoryService: PipelineRepositoryService, + private val client: Client, + private val buildVariableService: BuildVariableService, + private val pipelineBuildFacadeService: PipelineBuildFacadeService +) { + + fun onPipelineShutdown( + pipelineId: String, + buildId: String, + projectId: String, + buildStatus: BuildStatus + ) { + logger.info("onPipelineShutdown $pipelineId|$buildId|$buildStatus") + val vars = buildVariableService.getAllVariable(buildId).toMutableMap() + vars[PIPELINE_TIME_DURATION]?.takeIf { it.isNotBlank() }?.toLongOrNull()?.let { + vars[PIPELINE_TIME_DURATION] = DateTimeUtil.formatMillSecond(it * 1000) + } + val executionVar = getExecutionVariables(pipelineId, vars) + val buildInfo = pipelineRuntimeService.getBuildInfo(buildId) ?: return + logger.info("buildInfo is $buildInfo") + val pipelineInfo = pipelineRepositoryService.getPipelineInfo(pipelineId) ?: return + var pipelineName = pipelineInfo.pipelineName + + // 判断codecc类型更改查看详情链接 + val detailUrl = if (pipelineInfo.channelCode == ChannelCode.CODECC) { + val detail = pipelineBuildFacadeService.getBuildDetail(userId = buildInfo.startUser, + projectId = projectId, + pipelineId = pipelineId, + buildId = buildId, + channelCode = ChannelCode.BS, + checkPermission = false) + val codeccModel = getCodeccTaskName(detail) + if (codeccModel != null) { + pipelineName = codeccModel.codeCCTaskName.toString() + } + val taskId = pipelineName + "${HomeHostUtil.innerServerHost()}/console/codecc/$projectId/task/$taskId/detail" + } else { + detailUrl(projectId, pipelineId, buildId) + } + + val trigger = executionVar.trigger + val buildNum = buildInfo.buildNum + val user = executionVar.user + + logger.info("onPipelineShutdown pipelineNameReal:$pipelineName") + val replaceWithEmpty = true + // 流水线设置订阅的用户 + val setting = pipelineSettingDao.getSetting(dslContext, pipelineId) ?: return + setting.successReceiver = EnvUtils.parseEnv(setting.successReceiver, vars, replaceWithEmpty) + setting.failReceiver = EnvUtils.parseEnv(setting.failReceiver, vars, replaceWithEmpty) + // 内容为null的时候处理为空字符串 + setting.successContent = setting.successContent ?: NotifyTemplateUtils.COMMON_SHUTDOWN_SUCCESS_CONTENT + setting.failContent = setting.failContent ?: NotifyTemplateUtils.COMMON_SHUTDOWN_FAILURE_CONTENT + // 内容 + var emailSuccessContent = setting.successContent + var emailFailContent = setting.failContent + val detail = pipelineBuildFacadeService.getBuildDetail(buildInfo.startUser, + projectId, + pipelineId, + buildId, + ChannelCode.BS, + false) + val failTask = getFailTaskName(detail) + vars["failTask"] = failTask + emailSuccessContent = EnvUtils.parseEnv(emailSuccessContent, vars, replaceWithEmpty) + emailFailContent = EnvUtils.parseEnv(emailFailContent, vars, replaceWithEmpty) + setting.successContent = EnvUtils.parseEnv(setting.successContent, vars, replaceWithEmpty) + + setting.failContent = EnvUtils.parseEnv(setting.failContent, vars, replaceWithEmpty) + + val projectName = + client.get(ServiceProjectResource::class).get(projectId).data?.projectName.toString() + val mapData = mutableMapOf( + "pipelineName" to pipelineName, + "buildNum" to buildNum.toString(), + "projectName" to projectName, + "detailUrl" to detailUrl, + "detailOuterUrl" to detailUrl, + "detailShortOuterUrl" to detailUrl, + "startTime" to getFormatTime(detail.startTime), + "trigger" to trigger, + "username" to user, + "detailUrl" to detailUrl, + "successContent" to setting.successContent, + "failContent" to setting.failContent, + "emailSuccessContent" to emailSuccessContent, + "emailFailContent" to emailFailContent, + "failTask" to failTask + ) + // 把流水线变量带上 + val params = vars + mapData + val result = when { + buildStatus.isFailure() -> { + client.get(ServiceNotifyMessageTemplateResource::class).sendNotifyMessageByTemplate( + SendNotifyMessageTemplateRequest( + templateCode = PIPELINE_SHUTDOWN_FAILURE_NOTIFY_TEMPLATE.templateCode, + receivers = setting.failReceiver.split(",").toMutableSet(), + notifyType = setting.failType.split(",").toMutableSet(), + titleParams = params, + bodyParams = params, + cc = null, + bcc = null + ) + ) + } + buildStatus.isCancel() -> { + // 取消暂时按失败的配置 + client.get(ServiceNotifyMessageTemplateResource::class).sendNotifyMessageByTemplate( + SendNotifyMessageTemplateRequest( + templateCode = PIPELINE_SHUTDOWN_SUCCESS_NOTIFY_TEMPLATE.templateCode, + receivers = setting.failReceiver.split(",").toMutableSet(), + notifyType = setting.failType.split(",").toMutableSet(), + titleParams = params, + bodyParams = params, + cc = null, + bcc = null + ) + ) + } + buildStatus.isSuccess() -> { + client.get(ServiceNotifyMessageTemplateResource::class).sendNotifyMessageByTemplate( + SendNotifyMessageTemplateRequest( + templateCode = PIPELINE_SHUTDOWN_SUCCESS_NOTIFY_TEMPLATE.templateCode, + receivers = setting.successReceiver.split(",").toMutableSet(), + notifyType = setting.successType.split(",").toMutableSet(), + titleParams = mapData, + bodyParams = mapData, + cc = null, + bcc = null + ) + ) + } + else -> Result(0) + } + if (result.isNotOk()) { + logger.warn("onPipelineShutdown notify failed: $result") + } + } + + private fun getFailTaskName(detail: ModelDetail): String { + var result = "unknown" + detail.model.stages.forEach { stage -> + stage.containers.forEach { container -> + container.elements.firstOrNull { "FAILED" == it.status }?.let { + result = it.name + } + } + } + return result + } + + private fun getCodeccTaskName(detail: ModelDetail): LinuxCodeCCScriptElement? { + for (stage in detail.model.stages) { + stage.containers.forEach { container -> + val codeccElemet = + container.elements.filter { it is LinuxCodeCCScriptElement || it is LinuxPaasCodeCCScriptElement } + if (codeccElemet.isNotEmpty()) return codeccElemet.first() as LinuxCodeCCScriptElement + } + } + return null + } + + private fun detailUrl(projectId: String, pipelineId: String, processInstanceId: String) = + "${HomeHostUtil.outerServerHost()}/console/pipeline/$projectId/$pipelineId/detail/$processInstanceId" + + fun getExecutionVariables(pipelineId: String, vars: Map): ExecutionVariables { + var buildUser = "" + var triggerType = "" + var buildNum: Int? = null + var pipelineVersion: Int? = null + var channelCode: ChannelCode? = null + var webhookTriggerUser: String? = null + var pipelineUserId: String? = null + var isMobileStart = false + + vars.forEach { (key, value) -> + when (key) { + PIPELINE_VERSION -> pipelineVersion = value.toInt() + PIPELINE_START_USER_ID -> buildUser = value + PIPELINE_START_TYPE -> triggerType = value + PIPELINE_BUILD_NUM -> buildNum = value.toInt() + PIPELINE_START_CHANNEL -> channelCode = ChannelCode.valueOf(value) + PIPELINE_START_WEBHOOK_USER_ID -> webhookTriggerUser = value + PIPELINE_START_PIPELINE_USER_ID -> pipelineUserId = value + PIPELINE_START_MOBILE -> isMobileStart = value.toBoolean() + } + } + + // 对于是web hook 触发的构建,用户显示触发人 + when (triggerType) { + StartType.WEB_HOOK.name -> { + webhookTriggerUser?.takeIf { it.isNotBlank() }?.let { + buildUser = it + } + } + StartType.PIPELINE.name -> { + pipelineUserId?.takeIf { it.isNotBlank() }?.let { + buildUser = it + } + } + } + + val trigger = StartType.toReadableString(triggerType, channelCode) + return ExecutionVariables(pipelineVersion = pipelineVersion, + buildNum = buildNum, + trigger = trigger, + originTriggerType = triggerType, + user = buildUser, + isMobileStart = isMobileStart) + } + + private fun getFormatTime(time: Long): String { + val current = LocalDateTime.ofInstant(Date(time).toInstant(), ZoneId.systemDefault()) + + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + return current.format(formatter) + } + + companion object { + private val logger = LoggerFactory.getLogger(PipelineSubscriptionService::class.java) + } + + data class ExecutionVariables( + val pipelineVersion: Int?, + val buildNum: Int?, + val trigger: String, + val originTriggerType: String, + val user: String, + val isMobileStart: Boolean + ) +} diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/template/TemplateFacadeService.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/template/TemplateFacadeService.kt index b6e8cbc80e7..ca396dd518d 100644 --- a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/template/TemplateFacadeService.kt +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/template/TemplateFacadeService.kt @@ -76,6 +76,7 @@ import com.tencent.devops.process.engine.dao.template.TemplateDao import com.tencent.devops.process.engine.dao.template.TemplateInstanceBaseDao import com.tencent.devops.process.engine.dao.template.TemplateInstanceItemDao import com.tencent.devops.process.engine.dao.template.TemplatePipelineDao +import com.tencent.devops.process.engine.service.PipelineInfoExtService import com.tencent.devops.process.engine.service.PipelineRepositoryService import com.tencent.devops.process.engine.utils.PipelineUtils import com.tencent.devops.process.permission.PipelinePermissionService @@ -153,7 +154,8 @@ class TemplateFacadeService @Autowired constructor( private val modelTaskIdGenerator: ModelTaskIdGenerator, private val paramService: ParamFacadeService, private val pipelineRepositoryService: PipelineRepositoryService, - private val modelCheckPlugin: ModelCheckPlugin + private val modelCheckPlugin: ModelCheckPlugin, + private val pipelineInfoExtService: PipelineInfoExtService ) { @Value("\${template.maxSyncInstanceNum:10}") @@ -182,7 +184,14 @@ class TemplateFacadeService @Autowired constructor( storeFlag = false ) - pipelineSettingDao.insertNewSetting(context, projectId, templateId, template.name, true) + pipelineSettingDao.insertNewSetting( + dslContext = context, + projectId = projectId, + pipelineId = templateId, + pipelineName = template.name, + isTemplate = true, + failNotifyTypes = pipelineInfoExtService.failNotifyChannel() + ) logger.info("Get the template version $version") } @@ -233,11 +242,12 @@ class TemplateFacadeService @Autowired constructor( saveTemplatePipelineSetting(userId, setting, true) } else { pipelineSettingDao.insertNewSetting( - context, - projectId, - newTemplateId, - copyTemplateReq.templateName, - true + dslContext = context, + projectId = projectId, + pipelineId = newTemplateId, + pipelineName = copyTemplateReq.templateName, + isTemplate = true, + failNotifyTypes = pipelineInfoExtService.failNotifyChannel() ) } @@ -297,7 +307,8 @@ class TemplateFacadeService @Autowired constructor( projectId = projectId, pipelineId = templateId, pipelineName = saveAsTemplateReq.templateName, - isTemplate = true + isTemplate = true, + failNotifyTypes = pipelineInfoExtService.failNotifyChannel() ) } @@ -1252,7 +1263,8 @@ class TemplateFacadeService @Autowired constructor( dslContext = context, projectId = projectId, pipelineId = pipelineId, - pipelineName = pipelineName + pipelineName = pipelineName, + failNotifyTypes = pipelineInfoExtService.failNotifyChannel() ) } addRemoteAuth(instanceModel, projectId, pipelineId, userId) @@ -2019,7 +2031,8 @@ class TemplateFacadeService @Autowired constructor( projectId = it, pipelineId = templateId, pipelineName = addMarketTemplateRequest.templateName, - isTemplate = true + isTemplate = true, + failNotifyTypes = pipelineInfoExtService.failNotifyChannel() ) projectTemplateMap[it] = templateId } diff --git a/src/backend/ci/settings.gradle.kts b/src/backend/ci/settings.gradle.kts index a7a615a0185..880c7c595fc 100644 --- a/src/backend/ci/settings.gradle.kts +++ b/src/backend/ci/settings.gradle.kts @@ -145,7 +145,6 @@ include(":core:store:biz-store-image-sample") include(":core:store:boot-store") include(":core:store:model-store") - include(":core:process") include(":core:process:api-process") include(":core:process:biz-base") @@ -193,6 +192,7 @@ include(":core:notify") include(":core:notify:api-notify") include(":core:notify:biz-notify") include(":core:notify:biz-notify-blueking") +include(":core:notify:biz-notify-wework") include(":core:notify:model-notify") include(":core:notify:boot-notify") diff --git a/src/frontend/devops-pipeline/src/components/PipelineEditTabs/NotifyTab.vue b/src/frontend/devops-pipeline/src/components/PipelineEditTabs/NotifyTab.vue new file mode 100644 index 00000000000..2cfb6aa6dae --- /dev/null +++ b/src/frontend/devops-pipeline/src/components/PipelineEditTabs/NotifyTab.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/frontend/devops-pipeline/src/components/PipelineEditTabs/VerticalTab.vue b/src/frontend/devops-pipeline/src/components/PipelineEditTabs/VerticalTab.vue index 55cca865a51..5b8f0038b2e 100755 --- a/src/frontend/devops-pipeline/src/components/PipelineEditTabs/VerticalTab.vue +++ b/src/frontend/devops-pipeline/src/components/PipelineEditTabs/VerticalTab.vue @@ -18,6 +18,7 @@ diff --git a/src/frontend/devops-pipeline/src/components/pipelineSetting/NotifySetting.vue b/src/frontend/devops-pipeline/src/components/pipelineSetting/NotifySetting.vue new file mode 100644 index 00000000000..e983deaa88a --- /dev/null +++ b/src/frontend/devops-pipeline/src/components/pipelineSetting/NotifySetting.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/frontend/devops-pipeline/src/components/pipelineSetting/settingBase/index.vue b/src/frontend/devops-pipeline/src/components/pipelineSetting/settingBase/index.vue index 5c0e37c635c..fa148e144f0 100755 --- a/src/frontend/devops-pipeline/src/components/pipelineSetting/settingBase/index.vue +++ b/src/frontend/devops-pipeline/src/components/pipelineSetting/settingBase/index.vue @@ -56,8 +56,71 @@ + + + +
+
+ +
+ + + {{ item.name }} + + +
+
+ + + + + + + -
+ + + + + + + + + + + + +
+ + + + +
{{ $t('save') }} {{ $t('cancel') }}
@@ -68,9 +131,15 @@