diff --git a/src/main/java/com/rj/AISmartCard20251230Application.java b/src/main/java/com/rj/AISmartCard20251230Application.java index b341a2c..5f9c89c 100644 --- a/src/main/java/com/rj/AISmartCard20251230Application.java +++ b/src/main/java/com/rj/AISmartCard20251230Application.java @@ -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) { diff --git a/src/main/java/com/rj/config/LbAssessmentDingTalkProperties.java b/src/main/java/com/rj/config/LbAssessmentDingTalkProperties.java new file mode 100644 index 0000000..1fcca37 --- /dev/null +++ b/src/main/java/com/rj/config/LbAssessmentDingTalkProperties.java @@ -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 = ""; +} diff --git a/src/main/java/com/rj/controller/LbAssessmentApplyController.java b/src/main/java/com/rj/controller/LbAssessmentApplyController.java index 460a46f..b5055c0 100644 --- a/src/main/java/com/rj/controller/LbAssessmentApplyController.java +++ b/src/main/java/com/rj/controller/LbAssessmentApplyController.java @@ -110,7 +110,7 @@ public class LbAssessmentApplyController { } @GetMapping("/notificationText") - @Operation(summary = "生成评估会议通知文案", description = "根据租户ID与记录ID查询申请评估记录,拼接并返回格式化通知文本") + @Operation(summary = "生成评估会议通知文案", description = "根据租户ID与记录ID查询申请评估记录,拼接通知文本并写入 notification_text 字段后返回") public ResponseEntity> 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> sendNotificationToDingTalk( + @Parameter(description = "租户ID", required = true) @RequestParam String tenantId, + @Parameter(description = "申请评估记录ID(UUID)", required = true) @RequestParam String id) { + Map 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( diff --git a/src/main/java/com/rj/entity/LbAssessmentApply.java b/src/main/java/com/rj/entity/LbAssessmentApply.java index f16c54a..5fa6803 100644 --- a/src/main/java/com/rj/entity/LbAssessmentApply.java +++ b/src/main/java/com/rj/entity/LbAssessmentApply.java @@ -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; diff --git a/src/main/java/com/rj/service/DingTalkRobotService.java b/src/main/java/com/rj/service/DingTalkRobotService.java new file mode 100644 index 0000000..b862617 --- /dev/null +++ b/src/main/java/com/rj/service/DingTalkRobotService.java @@ -0,0 +1,14 @@ +package com.rj.service; + +/** + * 钉钉自定义机器人(群消息)发送。 + */ +public interface DingTalkRobotService { + + /** + * 向配置的钉钉群发送文本消息。 + * + * @param content 消息正文 + */ + void sendText(String content); +} diff --git a/src/main/java/com/rj/service/ILbAssessmentApplyService.java b/src/main/java/com/rj/service/ILbAssessmentApplyService.java index 8a5e716..3ab7d6b 100644 --- a/src/main/java/com/rj/service/ILbAssessmentApplyService.java +++ b/src/main/java/com/rj/service/ILbAssessmentApplyService.java @@ -41,4 +41,9 @@ public interface ILbAssessmentApplyService extends IService { */ Map buildAssessmentNotificationText(String tenantId, String id); + /** + * 根据租户ID与记录ID查询申请评估记录,将 notification_text 发送至钉钉群。 + */ + Map sendNotificationTextToDingTalk(String tenantId, String id); + } diff --git a/src/main/java/com/rj/service/impl/DingTalkRobotServiceImpl.java b/src/main/java/com/rj/service/impl/DingTalkRobotServiceImpl.java new file mode 100644 index 0000000..b08ba50 --- /dev/null +++ b/src/main/java/com/rj/service/impl/DingTalkRobotServiceImpl.java @@ -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 body = new HashMap<>(); + body.put("msgtype", "text"); + Map text = new HashMap<>(); + text.put("content", content); + body.put("text", text); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + HttpEntity> entity = new HttpEntity<>(body, headers); + + ResponseEntity 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); + } + } +} diff --git a/src/main/java/com/rj/service/impl/LbAssessmentApplyServiceImpl.java b/src/main/java/com/rj/service/impl/LbAssessmentApplyServiceImpl.java index 8f59dbf..b1fe24c 100644 --- a/src/main/java/com/rj/service/impl/LbAssessmentApplyServiceImpl.java +++ b/src/main/java/com/rj/service/impl/LbAssessmentApplyServiceImpl.java @@ -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 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 sendNotificationTextToDingTalk(String tenantId, String id) { + Map 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 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()); diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 33cec35..98d45f3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -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服务器地址 diff --git a/src/main/sql/lb_assessment_apply_alter_add_notification_text.sql b/src/main/sql/lb_assessment_apply_alter_add_notification_text.sql new file mode 100644 index 0000000..aacfdca --- /dev/null +++ b/src/main/sql/lb_assessment_apply_alter_add_notification_text.sql @@ -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`;