增加钉钉通知

This commit is contained in:
2025-11-04 17:19:28 +08:00
parent 1de86a2f46
commit 4845094577
11 changed files with 398 additions and 42 deletions

View File

@@ -4,6 +4,7 @@ import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import java.text.SimpleDateFormat
import java.util.*
@@ -15,6 +16,7 @@ import java.util.*
* 启动入口
**/
@SpringBootApplication
@ConfigurationPropertiesScan
open class DakaApplication
val <T : Any> T.logger: Logger

View File

@@ -8,12 +8,15 @@ import com.pomelotea.hoperun.sign.config.HoperunUserConfig.deviceMap
import com.pomelotea.hoperun.sign.config.HoperunUserConfig.getUserConfig
import com.pomelotea.hoperun.sign.config.HoperunUserConfig.userConfigMap
import com.pomelotea.hoperun.sign.config.UserConfig
import com.pomelotea.hoperun.sign.notify.ServerChan3NotifyHelper
import com.pomelotea.hoperun.sign.service.NotificationService
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler.Companion.dakaQueue
import okhttp3.FormBody
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.*
import java.text.SimpleDateFormat
import java.util.*
@@ -29,12 +32,14 @@ import java.util.*
@RestController
@RequestMapping("/api/daka")
class HoperunSignController {
class HoperunSignController(
@field:Autowired private val notificationService: NotificationService
) {
init {
AutoDakaScheduler()
AutoDakaScheduler(notificationService)
// AutoRenewSessionScheduler()
val yxl = UserConfig(
project_id = "U2103S000112",

View File

@@ -0,0 +1,25 @@
package com.pomelotea.hoperun.sign.api.model
data class DingTalkNotifyRequest(
val msgtype: String = "text",
val text: TextContent
)
data class TextContent(
val content: String
)
data class DingTalkNotifyResponse(
val errcode: Int,
val errmsg: String
)
data class MarkdownContent(
val title: String,
val text: String
)
data class DingTalkMarkdownRequest(
val msgtype: String = "markdown",
val markdown: MarkdownContent
)

View File

@@ -0,0 +1,12 @@
package com.pomelotea.hoperun.sign.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
@Component
@ConfigurationProperties(prefix = "dingtalk")
class DingTalkConfig {
lateinit var webhook: String
lateinit var secret: String
var enabled: Boolean = false
}

View File

@@ -0,0 +1,11 @@
package com.pomelotea.hoperun.sign.config
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.stereotype.Component
@Component
@ConfigurationProperties(prefix = "sc3")
class ServerChan3Config {
lateinit var uid: String
lateinit var sendKey: String
}

View File

@@ -0,0 +1,100 @@
package com.pomelotea.hoperun.sign.notify
import com.alibaba.fastjson.JSON
import com.alibaba.fastjson.JSONObject
import com.pomelotea.hoperun.sign.api.model.DingTalkMarkdownRequest
import com.pomelotea.hoperun.sign.api.model.DingTalkNotifyRequest
import com.pomelotea.hoperun.sign.api.model.DingTalkNotifyResponse
import com.pomelotea.hoperun.sign.api.model.MarkdownContent
import com.pomelotea.hoperun.sign.common.client
import com.pomelotea.hoperun.sign.config.DingTalkConfig
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler.Companion.logger
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.springframework.stereotype.Service
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import kotlin.experimental.and
@Service
open class DingTalkNotifyHelper(
private val dingTalkConfig: DingTalkConfig
) {
fun sendText(content: String): DingTalkNotifyResponse {
if (!dingTalkConfig.enabled) {
logger.info("钉钉通知未启用,跳过发送消息")
return DingTalkNotifyResponse(0, "钉钉通知未启用")
}
val request = DingTalkNotifyRequest(
text = com.pomelotea.hoperun.sign.api.model.TextContent(content)
)
return sendRequest(request)
}
fun sendMarkdown(title: String, content: String): DingTalkNotifyResponse {
if (!dingTalkConfig.enabled) {
logger.info("钉钉通知未启用,跳过发送消息")
return DingTalkNotifyResponse(0, "钉钉通知未启用")
}
val request = DingTalkMarkdownRequest(
markdown = MarkdownContent(title, content)
)
return sendRequest(request)
}
private fun sendRequest(request: Any): DingTalkNotifyResponse {
try {
val url = generateSignedUrl()
val jsonBody = JSON.toJSONString(request)
logger.info("发送钉钉消息: $jsonBody")
val httpRequest = Request.Builder()
.url(url)
.post(jsonBody.toRequestBody("application/json;charset=utf-8".toMediaTypeOrNull()))
.build()
val response = client.newCall(httpRequest).execute()
val responseBody = response.body?.string()
logger.info("钉钉响应: $responseBody")
return JSONObject.parseObject(responseBody, DingTalkNotifyResponse::class.java)
} catch (e: Exception) {
logger.error("发送钉钉消息失败", e)
return DingTalkNotifyResponse(-1, "发送钉钉消息失败: ${e.message}")
}
}
private fun generateSignedUrl(): String {
val webhook = dingTalkConfig.webhook
val secret = dingTalkConfig.secret
if (secret.isEmpty()) {
return webhook
}
val timestamp = System.currentTimeMillis()
val sign = generateSign(timestamp.toString(), secret)
return "$webhook&timestamp=$timestamp&sign=$sign"
}
private fun generateSign(timestamp: String, secret: String): String {
val stringToSign = "$timestamp\n$secret"
val mac = Mac.getInstance("HmacSHA256")
mac.init(SecretKeySpec(secret.toByteArray(StandardCharsets.UTF_8), "HmacSHA256"))
val signData = mac.doFinal(stringToSign.toByteArray(StandardCharsets.UTF_8))
val sign = Base64.getEncoder().encodeToString(signData)
return URLEncoder.encode(sign, StandardCharsets.UTF_8.name())
}
}

View File

@@ -5,22 +5,21 @@ import com.alibaba.fastjson.JSONObject
import com.pomelotea.hoperun.sign.api.model.ScNotifyRequest
import com.pomelotea.hoperun.sign.api.model.ScNotifyResponse
import com.pomelotea.hoperun.sign.common.client
import com.pomelotea.hoperun.sign.config.ServerChan3Config
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler.Companion.logger
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
@Service
open class ServerChan3NotifyHelper(
@field:Value("\${sc3.uid}") private val uid: String,
@field:Value("\${sc3.sendKey}") private val sendKey: String,
private val serverChan3Config: ServerChan3Config
) {
fun push(req: ScNotifyRequest): ScNotifyResponse {
val notifyRequest = Request.Builder()
.url("https://${uid}.push.ft07.com/send/${sendKey}.send")
.url("https://${serverChan3Config.uid}.push.ft07.com/send/${serverChan3Config.sendKey}.send")
.post(
JSON.toJSONString(req)
.toRequestBody("application/json;charset=utf-8".toMediaTypeOrNull())

View File

@@ -9,6 +9,7 @@ import com.pomelotea.hoperun.sign.common.*
import com.pomelotea.hoperun.sign.config.HoperunUserConfig
import com.pomelotea.hoperun.sign.config.UserConfig
import com.pomelotea.hoperun.sign.notify.ServerChan3NotifyHelper
import com.pomelotea.hoperun.sign.service.NotificationService
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
@@ -30,9 +31,8 @@ import java.util.concurrent.TimeUnit
* date 2023-03-22 14:50
* 自动打卡定时任务
**/
@Service
open class AutoDakaScheduler(
private val serverChan3NotifyHelper: ServerChan3NotifyHelper
private val notificationService: NotificationService
) {
val timeThreadPool: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
@@ -57,21 +57,19 @@ open class AutoDakaScheduler(
if (daka == null) {
daka = generateDakaInfo(v, dakaDate)
dakaQueue.add(daka)
val scNotifyResponse = serverChan3NotifyHelper.push(
ScNotifyRequest(
title = "添加打卡任务:${v.username}, $daka",
desp = "添加打卡任务:${v.username}, $daka"
)
notificationService.sendDingTalkNotification(
"#### 【 $dakaDate 】添加打卡任务!",
"""
#### 【 $dakaDate 】添加打卡任务!
**************************************************
##### 工号: ${daka.employeeNo}
##### 上班卡: ${daka.beginTime}
##### 下班卡: ${daka.endTime}
**************************************************
"""
)
if (scNotifyResponse.code != 0) {
serverChan3NotifyHelper.push(
ScNotifyRequest(
title = "添加打卡任务失败:${v.username}, $daka",
desp = "添加打卡任务失败:${v.username}, $daka"
)
)
}
logger.info("添加打卡任务: ${v.username}, $daka")
}
}
@@ -123,18 +121,27 @@ open class AutoDakaScheduler(
beginTime(employeeNo = daka.employeeNo, date = daka.dakaDate, time = daka.beginTime)
if (resp.result != "success") {
logger.error("打上班卡失败")
serverChan3NotifyHelper.push(
ScNotifyRequest(
title = "打上班卡失败:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.beginTime}",
desp = "打上班卡失败:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.beginTime}"
)
notificationService.sendDingTalkNotification(
title = "${daka.dakaDate} 】打上班卡失败",
"""
#### 【 ${daka.dakaDate} 】打上班卡失败!
**************************************************
##### 工号: ${daka.employeeNo}
##### 上班卡: ${daka.beginTime}
##### 下班卡: ${daka.endTime}
**************************************************
"""
)
} else {
serverChan3NotifyHelper.push(
ScNotifyRequest(
title = "打上班卡成功:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.beginTime}",
desp = "打上班卡成功:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.beginTime}"
)
notificationService.sendDingTalkNotification(
title = "打上班卡成功:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.beginTime}",
"""
#### 【 ${daka.dakaDate} 】打上班卡成功!
**************************************************
##### 工号: ${daka.employeeNo}
##### 时间: ${daka.beginTime}
**************************************************
"""
)
logger.info("打上班卡成功")
}
@@ -155,19 +162,26 @@ open class AutoDakaScheduler(
val resp: DakaResponse = endTime(employeeNo = daka.employeeNo, date = daka.dakaDate, time = daka.endTime)
if (resp.result != "success") {
logger.error("打下班卡失败")
serverChan3NotifyHelper.push(
ScNotifyRequest(
notificationService.sendDingTalkNotification(
title = "打下班卡失败:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.endTime}",
desp = "打下班卡失败:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.endTime}"
)
)
"""
#### 【 ${daka.dakaDate} 】打下班卡失败!
**************************************************
##### 工号: ${daka.employeeNo}
##### 上班卡: ${daka.beginTime}
##### 下班卡: ${daka.endTime}
**************************************************
""" )
} else {
serverChan3NotifyHelper.push(
ScNotifyRequest(
title = "打下班卡成功:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.endTime}",
desp = "打下班卡成功:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.endTime}"
)
)
notificationService.sendDingTalkNotification(
title = "打下班卡成功:${daka.employeeNo}:DATE:${daka.dakaDate}:TIME:${daka.endTime}",
"""
#### 【 ${daka.dakaDate} 】打下班卡成功!
**************************************************
##### 工号: ${daka.employeeNo}
##### 时间: ${daka.endTime}
**************************************************
""" )
logger.info("打下班卡成功")
}
} catch (e: Exception) {

View File

@@ -0,0 +1,70 @@
package com.pomelotea.hoperun.sign.service
import com.pomelotea.hoperun.sign.api.model.ScNotifyRequest
import com.pomelotea.hoperun.sign.api.model.ScNotifyResponse
import com.pomelotea.hoperun.sign.api.model.DingTalkNotifyResponse
import com.pomelotea.hoperun.sign.config.DingTalkConfig
import com.pomelotea.hoperun.sign.config.ServerChan3Config
import com.pomelotea.hoperun.sign.notify.DingTalkNotifyHelper
import com.pomelotea.hoperun.sign.notify.ServerChan3NotifyHelper
import com.pomelotea.hoperun.sign.scheduler.AutoDakaScheduler.Companion.logger
import org.springframework.stereotype.Service
@Service
open class NotificationService(
private val serverChan3NotifyHelper: ServerChan3NotifyHelper,
private val dingTalkNotifyHelper: DingTalkNotifyHelper,
private val serverChan3Config: ServerChan3Config,
private val dingTalkConfig: DingTalkConfig
) {
fun sendAllNotifications(title: String, content: String) {
// 发送 ServerChan3 通知
try {
val scResponse = sendServerChanNotification(title, content)
logger.info("ServerChan3 通知发送结果: ${scResponse.message}")
} catch (e: Exception) {
logger.error("ServerChan3 通知发送失败", e)
}
// 发送钉钉通知
try {
val dtResponse = sendDingTalkNotification(title, content)
logger.info("钉钉通知发送结果: ${dtResponse.errmsg}")
} catch (e: Exception) {
logger.error("钉钉通知发送失败", e)
}
}
fun sendServerChanNotification(title: String, content: String): ScNotifyResponse {
val request = ScNotifyRequest(title = title, desp = content)
return serverChan3NotifyHelper.push(request)
}
fun sendDingTalkNotification(title: String, content: String): DingTalkNotifyResponse {
// 如果内容包含markdown格式使用markdown类型消息
return if (content.contains("**") || content.contains("#") || content.contains("*")) {
dingTalkNotifyHelper.sendMarkdown(title, content)
} else {
dingTalkNotifyHelper.sendText("$title\n\n$content")
}
}
fun sendDingTalkText(text: String): DingTalkNotifyResponse {
return dingTalkNotifyHelper.sendText(text)
}
fun sendDingTalkMarkdown(title: String, content: String): DingTalkNotifyResponse {
return dingTalkNotifyHelper.sendMarkdown(title, content)
}
fun isServerChanEnabled(): Boolean {
return serverChan3Config.uid.isNotEmpty() && serverChan3Config.sendKey.isNotEmpty()
}
fun isDingTalkEnabled(): Boolean {
return dingTalkConfig.enabled &&
dingTalkConfig.webhook.isNotEmpty() &&
!dingTalkConfig.webhook.contains("YOUR_ACCESS_TOKEN")
}
}