集成钉钉
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.rj;
|
||||
|
||||
import com.rj.config.AmapProperties;
|
||||
import com.rj.config.LbAssessmentDingTalkProperties;
|
||||
import com.rj.config.YihangyiVllmAsrProperties;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
@@ -40,7 +41,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
@MapperScan("com.rj.mapper")
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableConfigurationProperties({YihangyiVllmAsrProperties.class, AmapProperties.class})
|
||||
@EnableConfigurationProperties({YihangyiVllmAsrProperties.class, AmapProperties.class, LbAssessmentDingTalkProperties.class})
|
||||
public class AISmartCard20251230Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.rj.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 申请评估 - 钉钉群机器人 Webhook 配置。
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "lb.assessment-apply.dingtalk")
|
||||
public class LbAssessmentDingTalkProperties {
|
||||
|
||||
/**
|
||||
* 是否启用钉钉发送。
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 自定义机器人 Webhook 中的 access_token。
|
||||
*/
|
||||
private String accessToken = "";
|
||||
|
||||
/**
|
||||
* 加签密钥(SEC 开头);未配置则不加签。
|
||||
*/
|
||||
private String secret = "";
|
||||
}
|
||||
@@ -110,7 +110,7 @@ public class LbAssessmentApplyController {
|
||||
}
|
||||
|
||||
@GetMapping("/notificationText")
|
||||
@Operation(summary = "生成评估会议通知文案", description = "根据租户ID与记录ID查询申请评估记录,拼接并返回格式化通知文本")
|
||||
@Operation(summary = "生成评估会议通知文案", description = "根据租户ID与记录ID查询申请评估记录,拼接通知文本并写入 notification_text 字段后返回")
|
||||
public ResponseEntity<Map<String, Object>> getNotificationText(
|
||||
@Parameter(description = "租户ID", required = true) @RequestParam String tenantId,
|
||||
@Parameter(description = "申请评估记录ID(UUID)", required = true) @RequestParam String id) {
|
||||
@@ -122,6 +122,19 @@ public class LbAssessmentApplyController {
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
@PostMapping("/sendNotificationToDingTalk")
|
||||
@Operation(summary = "发送评估会议通知到钉钉群", description = "根据租户ID与记录ID查询申请评估记录,将 notification_text 发送至配置的钉钉群机器人")
|
||||
public ResponseEntity<Map<String, Object>> sendNotificationToDingTalk(
|
||||
@Parameter(description = "租户ID", required = true) @RequestParam String tenantId,
|
||||
@Parameter(description = "申请评估记录ID(UUID)", required = true) @RequestParam String id) {
|
||||
Map<String, Object> result = lbAssessmentApplyService.sendNotificationTextToDingTalk(tenantId, id);
|
||||
Boolean success = (Boolean) result.get("success");
|
||||
if (success != null && success) {
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
return ResponseEntity.badRequest().body(result);
|
||||
}
|
||||
|
||||
@GetMapping("/export")
|
||||
@Operation(summary = "导出申请评估Excel", description = "按租户ID与申请日期时间范围导出申请评估列表")
|
||||
public void export(
|
||||
|
||||
@@ -70,6 +70,10 @@ public class LbAssessmentApply implements Serializable {
|
||||
@Schema(description = "会议号")
|
||||
private String meetingNumber;
|
||||
|
||||
@TableField("notification_text")
|
||||
@Schema(description = "评估会议通知文案")
|
||||
private String notificationText;
|
||||
|
||||
@TableField("application_datetime")
|
||||
@Schema(description = "申请日期时间")
|
||||
private LocalDateTime applicationDatetime;
|
||||
|
||||
14
src/main/java/com/rj/service/DingTalkRobotService.java
Normal file
14
src/main/java/com/rj/service/DingTalkRobotService.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package com.rj.service;
|
||||
|
||||
/**
|
||||
* 钉钉自定义机器人(群消息)发送。
|
||||
*/
|
||||
public interface DingTalkRobotService {
|
||||
|
||||
/**
|
||||
* 向配置的钉钉群发送文本消息。
|
||||
*
|
||||
* @param content 消息正文
|
||||
*/
|
||||
void sendText(String content);
|
||||
}
|
||||
@@ -41,4 +41,9 @@ public interface ILbAssessmentApplyService extends IService<LbAssessmentApply> {
|
||||
*/
|
||||
Map<String, Object> buildAssessmentNotificationText(String tenantId, String id);
|
||||
|
||||
/**
|
||||
* 根据租户ID与记录ID查询申请评估记录,将 notification_text 发送至钉钉群。
|
||||
*/
|
||||
Map<String, Object> sendNotificationTextToDingTalk(String tenantId, String id);
|
||||
|
||||
}
|
||||
|
||||
110
src/main/java/com/rj/service/impl/DingTalkRobotServiceImpl.java
Normal file
110
src/main/java/com/rj/service/impl/DingTalkRobotServiceImpl.java
Normal file
@@ -0,0 +1,110 @@
|
||||
package com.rj.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.rj.config.LbAssessmentDingTalkProperties;
|
||||
import com.rj.service.DingTalkRobotService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DingTalkRobotServiceImpl implements DingTalkRobotService {
|
||||
|
||||
private static final String SEND_URL = "https://oapi.dingtalk.com/robot/send";
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final LbAssessmentDingTalkProperties properties;
|
||||
|
||||
public DingTalkRobotServiceImpl(RestTemplate restTemplate,
|
||||
ObjectMapper objectMapper,
|
||||
LbAssessmentDingTalkProperties properties) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.objectMapper = objectMapper;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendText(String content) {
|
||||
if (!properties.isEnabled()) {
|
||||
throw new IllegalStateException("钉钉机器人发送未启用,请在配置中设置 lb.assessment-apply.dingtalk.enabled=true");
|
||||
}
|
||||
if (!StringUtils.hasText(properties.getAccessToken())) {
|
||||
throw new IllegalStateException("未配置钉钉机器人 access-token(lb.assessment-apply.dingtalk.access-token)");
|
||||
}
|
||||
if (!StringUtils.hasText(content)) {
|
||||
throw new IllegalArgumentException("发送内容不能为空");
|
||||
}
|
||||
|
||||
String url = buildWebhookUrl();
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("msgtype", "text");
|
||||
Map<String, String> text = new HashMap<>();
|
||||
text.put("content", content);
|
||||
body.put("text", text);
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
|
||||
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
||||
parseResponse(response.getBody());
|
||||
}
|
||||
|
||||
private String buildWebhookUrl() {
|
||||
StringBuilder url = new StringBuilder(SEND_URL)
|
||||
.append("?access_token=")
|
||||
.append(properties.getAccessToken().trim());
|
||||
if (StringUtils.hasText(properties.getSecret())) {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
String sign = sign(timestamp, properties.getSecret().trim());
|
||||
url.append("×tamp=").append(timestamp).append("&sign=").append(sign);
|
||||
}
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
private static String sign(long timestamp, String secret) {
|
||||
try {
|
||||
String stringToSign = timestamp + "\n" + secret;
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
|
||||
return URLEncoder.encode(Base64.getEncoder().encodeToString(signData), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("钉钉加签失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseResponse(String body) {
|
||||
if (!StringUtils.hasText(body)) {
|
||||
throw new IllegalStateException("钉钉接口返回为空");
|
||||
}
|
||||
try {
|
||||
JsonNode root = objectMapper.readTree(body);
|
||||
int errcode = root.path("errcode").asInt(-1);
|
||||
if (errcode != 0) {
|
||||
String errmsg = root.path("errmsg").asText("未知错误");
|
||||
throw new IllegalStateException("钉钉发送失败:" + errmsg + "(errcode=" + errcode + ")");
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("解析钉钉响应失败:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.rj.entity.LbAssessmentApply;
|
||||
import com.rj.mapper.LbAssessmentApplyMapper;
|
||||
import com.rj.service.DingTalkRobotService;
|
||||
import com.rj.service.ILbAssessmentApplyService;
|
||||
import jakarta.servlet.ServletOutputStream;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -47,6 +48,9 @@ public class LbAssessmentApplyServiceImpl
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Autowired
|
||||
private DingTalkRobotService dingTalkRobotService;
|
||||
|
||||
@Value("${langchain4j.open-ai.chat-model.model-name:qwen-plus}")
|
||||
private String dashScopeChatModel;
|
||||
|
||||
@@ -67,7 +71,7 @@ public class LbAssessmentApplyServiceImpl
|
||||
moderatorName 主持人姓名;
|
||||
meetingNumber 会议号;
|
||||
applicationDatetime 申请日期时间,优先 ISO-8601(如 2026-05-15T14:30:00),也可 yyyy-MM-dd HH:mm:ss;若仅有日期可设为当天 00:00:00。
|
||||
不要输出 id、tenantId、createdAt、updatedAt。
|
||||
不要输出 id、tenantId、createdAt、updatedAt、notificationText。
|
||||
""";
|
||||
|
||||
@Override
|
||||
@@ -190,6 +194,7 @@ public class LbAssessmentApplyServiceImpl
|
||||
entity.setCreatedAt(now);
|
||||
}
|
||||
entity.setUpdatedAt(now);
|
||||
refreshNotificationText(entity);
|
||||
|
||||
boolean ok = this.save(entity);
|
||||
result.put("success", ok);
|
||||
@@ -216,6 +221,9 @@ public class LbAssessmentApplyServiceImpl
|
||||
}
|
||||
entity.setUpdatedAt(LocalDateTime.now());
|
||||
boolean ok = this.updateById(entity);
|
||||
if (ok) {
|
||||
persistNotificationText(entity.getId());
|
||||
}
|
||||
result.put("success", ok);
|
||||
result.put("message", ok ? "编辑成功" : "编辑失败");
|
||||
if (ok) {
|
||||
@@ -308,17 +316,14 @@ public class LbAssessmentApplyServiceImpl
|
||||
return result;
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<LbAssessmentApply> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(LbAssessmentApply::getTenantId, tenantId.trim())
|
||||
.eq(LbAssessmentApply::getId, id.trim());
|
||||
LbAssessmentApply record = this.getOne(queryWrapper);
|
||||
LbAssessmentApply record = getByTenantIdAndId(tenantId, id);
|
||||
if (record == null) {
|
||||
result.put("success", false);
|
||||
result.put("message", "未找到对应的申请评估记录");
|
||||
return result;
|
||||
}
|
||||
|
||||
String text = formatAssessmentNotificationText(record);
|
||||
String text = ensureAndSaveNotificationText(record);
|
||||
result.put("success", true);
|
||||
result.put("message", "生成成功");
|
||||
result.put("data", text);
|
||||
@@ -331,6 +336,91 @@ public class LbAssessmentApplyServiceImpl
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> sendNotificationTextToDingTalk(String tenantId, String id) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
try {
|
||||
if (tenantId == null || tenantId.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "tenantId不能为空");
|
||||
return result;
|
||||
}
|
||||
if (id == null || id.trim().isEmpty()) {
|
||||
result.put("success", false);
|
||||
result.put("message", "id不能为空");
|
||||
return result;
|
||||
}
|
||||
|
||||
LbAssessmentApply record = getByTenantIdAndId(tenantId, id);
|
||||
if (record == null) {
|
||||
result.put("success", false);
|
||||
result.put("message", "未找到对应的申请评估记录");
|
||||
return result;
|
||||
}
|
||||
|
||||
String text = ensureAndSaveNotificationText(record);
|
||||
dingTalkRobotService.sendText(text);
|
||||
|
||||
result.put("success", true);
|
||||
result.put("message", "发送成功");
|
||||
result.put("data", text);
|
||||
return result;
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
log.warn("发送评估会议通知到钉钉失败, tenantId={}, id={}, reason={}", tenantId, id, e.getMessage());
|
||||
result.put("success", false);
|
||||
result.put("message", e.getMessage());
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("发送评估会议通知到钉钉异常, tenantId={}, id={}", tenantId, id, e);
|
||||
result.put("success", false);
|
||||
result.put("message", "发送失败:" + e.getMessage());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private LbAssessmentApply getByTenantIdAndId(String tenantId, String id) {
|
||||
LambdaQueryWrapper<LbAssessmentApply> queryWrapper = new LambdaQueryWrapper<>();
|
||||
queryWrapper.eq(LbAssessmentApply::getTenantId, tenantId.trim())
|
||||
.eq(LbAssessmentApply::getId, id.trim());
|
||||
return this.getOne(queryWrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回可用的通知文案:库中已有则直接返回,否则生成并落库。
|
||||
*/
|
||||
private String ensureAndSaveNotificationText(LbAssessmentApply record) {
|
||||
String text = record.getNotificationText();
|
||||
if (text != null && !text.trim().isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
text = formatAssessmentNotificationText(record);
|
||||
LbAssessmentApply update = new LbAssessmentApply();
|
||||
update.setId(record.getId());
|
||||
update.setNotificationText(text);
|
||||
update.setUpdatedAt(LocalDateTime.now());
|
||||
if (!this.updateById(update)) {
|
||||
throw new IllegalStateException("通知文案保存失败");
|
||||
}
|
||||
record.setNotificationText(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
private void refreshNotificationText(LbAssessmentApply entity) {
|
||||
entity.setNotificationText(formatAssessmentNotificationText(entity));
|
||||
}
|
||||
|
||||
private void persistNotificationText(String id) {
|
||||
LbAssessmentApply record = this.getById(id);
|
||||
if (record == null) {
|
||||
return;
|
||||
}
|
||||
LbAssessmentApply update = new LbAssessmentApply();
|
||||
update.setId(id);
|
||||
update.setNotificationText(formatAssessmentNotificationText(record));
|
||||
update.setUpdatedAt(LocalDateTime.now());
|
||||
this.updateById(update);
|
||||
}
|
||||
|
||||
private static String formatAssessmentNotificationText(LbAssessmentApply record) {
|
||||
String teacherName = nullToEmpty(record.getAssessmentTeacherName());
|
||||
String applicantName = nullToEmpty(record.getApplicantName());
|
||||
|
||||
@@ -276,6 +276,14 @@ dify:
|
||||
amap:
|
||||
web-service-key: ${AMAP_WEB_SERVICE_KEY:34eaa47387aa9e8c1a8ca0d4c8abaa1d}
|
||||
|
||||
# 申请评估 - 钉钉群机器人(自定义机器人 Webhook)
|
||||
lb:
|
||||
assessment-apply:
|
||||
dingtalk:
|
||||
enabled: ${LB_ASSESSMENT_DINGTALK_ENABLED:true}
|
||||
access-token: ${LB_ASSESSMENT_DINGTALK_ACCESS_TOKEN:}
|
||||
secret: ${LB_ASSESSMENT_DINGTALK_SECRET:}
|
||||
|
||||
# MinIO 对象存储配置
|
||||
minio:
|
||||
# MinIO服务器地址
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- =============================================================================
|
||||
-- 升级脚本:为 lb_assessment_apply 表增加 notification_text 列
|
||||
-- =============================================================================
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `lb_assessment_apply`
|
||||
ADD COLUMN `notification_text` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '评估会议通知文案' AFTER `meeting_number`;
|
||||
Reference in New Issue
Block a user