package com.ruoyi.project.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.dingtalk.api.DefaultDingTalkClient;
import com.dingtalk.api.DingTalkClient;
import com.dingtalk.api.request.*;
import com.dingtalk.api.response.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.exception.base.BaseException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.project.domain.vo.DingDetpInfoVo;
import com.ruoyi.project.domain.vo.DingTalkReqVo;
import com.ruoyi.project.domain.vo.DingUserInfoVo;
import com.ruoyi.project.service.DingTalkService;
import com.ruoyi.system.mapper.SysUserMapper;
import com.taobao.api.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 智能中心
*
* @author ls
* @date 2023-05-23
*/
@Service
@Slf4j
public class DingTalkServiceImpl implements DingTalkService {
@Value("${dingAppid}")
private String dingAppid;
@Value("${dingAppSecret}")
private String dingAppSecret;
@Value("${agentId}")
private Long agentId;
/**
* access_token 缓存。
* 钉钉 token 有效期 7200 秒,这里按 6000 秒保守刷新;
* 原实现每次发消息都调一次 /gettoken,钉钉对取 token 有频次限制,审核高峰期容易整体失败。
*/
private volatile String cachedAccessToken;
/** access_token 过期时间(epoch millis) */
private volatile long accessTokenExpireAt;
private static final long ACCESS_TOKEN_TTL_MILLIS = 6000L * 1000L;
/**
* 钉钉接口超时(毫秒)。
* SDK 未显式设置超时时会无限等待,钉钉网络不通将占满容器线程、拖死审批等调用方,
* 因此这里对所有出站请求统一设置连接/读取超时。
*/
private static final int CONNECT_TIMEOUT_MS = 3000;
private static final int READ_TIMEOUT_MS = 5000;
@Autowired
private SysUserMapper sysUserMapper;
/**
* 创建带超时的钉钉客户端
*/
private DefaultDingTalkClient newClient(String url) {
DefaultDingTalkClient client = new DefaultDingTalkClient(url);
client.setConnectTimeout(CONNECT_TIMEOUT_MS);
client.setReadTimeout(READ_TIMEOUT_MS);
return client;
}
/**
* 发送钉钉通知(对外唯一入口)。
*
*
本方法保证不向外抛任何异常:钉钉属于旁路通知,任何失败都只记日志并返回 false,
* 绝不能影响审批、上报等业务主流程。
*/
@Override
public Boolean sendNotification(DingTalkReqVo dingTalkReqVo) {
try {
return doSendNotification(dingTalkReqVo);
} catch (Throwable t) {
log.error("钉钉通知发送发生未预期异常,已忽略(不影响业务主流程)", t);
return false;
}
}
private Boolean doSendNotification(DingTalkReqVo dingTalkReqVo) {
List userIdlist = new ArrayList();
if (dingTalkReqVo == null) {
log.warn("钉钉通知入参为空,跳过发送");
return false;
}
log.info("发送钉钉通知,title={}", dingTalkReqVo.getTitle());
String accessToken = null;
try {
accessToken = getAccessToken();
} catch (Exception e) {
log.error("钉钉获取 access_token 失败,无法发送通知", e);
}
if (StringUtils.isEmpty(accessToken)) {
log.error("钉钉 access_token 为空,无法发送通知");
return false;
}
// ---- 解析收件人:优先手机号,其次部门 ----
if (StringUtils.isNotEmpty(dingTalkReqVo.getNumber())) {
try {
DingTalkClient clientT = newClient("https://oapi.dingtalk.com/topapi/v2/user/getbymobile");
OapiV2UserGetbymobileRequest req = new OapiV2UserGetbymobileRequest();
req.setMobile(dingTalkReqVo.getNumber());
OapiV2UserGetbymobileResponse rsp = clientT.execute(req, accessToken);
String body = rsp == null ? null : rsp.getBody();
if (StringUtils.isEmpty(body)) {
log.error("钉钉按手机号查用户失败,mobile={},响应为空", dingTalkReqVo.getNumber());
return false;
}
JSONObject resultObj = JSONObject.parseObject(body).getJSONObject("result");
if (resultObj == null || resultObj.get("userid") == null) {
log.error("钉钉按手机号查用户失败,mobile={},响应={}", dingTalkReqVo.getNumber(), body);
return false;
}
userIdlist.add(resultObj.get("userid").toString());
} catch (Exception e) {
log.error("钉钉按手机号查用户异常,mobile={}", dingTalkReqVo.getNumber(), e);
return false;
}
} else if (dingTalkReqVo.getDeptId() != null) {
try {
DingTalkClient client3 = newClient("https://oapi.dingtalk.com/topapi/user/listid");
OapiUserListidRequest request2 = new OapiUserListidRequest();
request2.setDeptId(dingTalkReqVo.getDeptId()); // 设置要获取的部门 ID,这里以根部门为例675909059L
// 设置部门 ID,如果获取所有员工手机号,可以设置为根部门的 ID
OapiUserListidResponse response2 = client3.execute(request2, accessToken);
String body = response2 == null ? null : response2.getBody();
if (StringUtils.isNotEmpty(body)) {
// 解析为JSONObject
JSONObject jsonObject = JSONObject.parseObject(body);
// 提取出JSONArray
JSONArray jsonArray = new JSONArray(jsonObject.getJSONObject("result").getJSONArray("userid_list"));
// 将JSONArray转为List列表
String str = JSONObject.toJSONString(jsonArray);
userIdlist = JSONObject.parseObject(str, List.class);
}
} catch (Exception e) {
log.error("钉钉按部门查用户异常,deptId={}", dingTalkReqVo.getDeptId(), e);
return false;
}
} else {
// 既无手机号也无部门:原实现会走 listid(null) 被钉钉拒绝且异常被吞掉,表现为"静默发不出去"
log.warn("钉钉通知未提供收件人(手机号与部门均为空),跳过发送");
return false;
}
if (CollectionUtils.isEmpty(userIdlist)) {
log.warn("钉钉通知未解析到任何收件人,跳过发送");
return false;
}
// ---- 组装消息体(所有收件人共用) ----
// contents 中每行一个单键 map,用 List 顺序保证钉钉表单行序
List objects = new ArrayList<>();
List> contents = dingTalkReqVo.getContents();
if (contents != null) {
for (ConcurrentHashMap map : contents) {
if (map == null) {
continue;
}
for (Map.Entry entry : map.entrySet()) {
Object value = entry.getValue();
if (value == null) {
continue;
}
OapiMessageCorpconversationAsyncsendV2Request.Form formSeverityDesc = new OapiMessageCorpconversationAsyncsendV2Request.Form();
formSeverityDesc.setKey(entry.getKey());
if (entry.getKey().contains("审批时间") && value instanceof Date) {
// 24 小时制:原实现用 hh(12 小时制且无 AM/PM),下午的时间会显示成 01:xx
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
formSeverityDesc.setValue(simpleDateFormat.format((Date) value));
} else {
formSeverityDesc.setValue(value.toString());
}
objects.add(formSeverityDesc);
}
}
}
// ---- 逐人发送;全部成功才算成功(原实现 result 写在循环内,只反映最后一个收件人) ----
boolean allSuccess = true;
for (String urid : userIdlist) {
OapiMessageCorpconversationAsyncsendV2Response response = null;
try {
DingTalkClient client = newClient("https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2");
OapiMessageCorpconversationAsyncsendV2Request request = new OapiMessageCorpconversationAsyncsendV2Request();
request.setUseridList(urid);
request.setAgentId(agentId);
request.setToAllUser(false);
OapiMessageCorpconversationAsyncsendV2Request.Msg msg = new OapiMessageCorpconversationAsyncsendV2Request.Msg();
msg.setOa(new OapiMessageCorpconversationAsyncsendV2Request.OA());
msg.getOa().setMessageUrl(dingTalkReqVo.getUrl());
msg.getOa().setPcMessageUrl(dingTalkReqVo.getUrl());
msg.getOa().setHead(new OapiMessageCorpconversationAsyncsendV2Request.Head());
msg.getOa().getHead().setBgcolor("00409eff");
msg.getOa().setBody(new OapiMessageCorpconversationAsyncsendV2Request.Body());
msg.getOa().getBody().setTitle(dingTalkReqVo.getTitle());
msg.getOa().getBody().setForm(objects);
msg.setMsgtype("oa");
request.setMsg(msg);
response = client.execute(request, accessToken);
} catch (Exception e) {
log.error("钉钉消息发送异常,收件人={}", urid, e);
}
if (response == null || !response.isSuccess()) {
allSuccess = false;
log.error("钉钉消息发送失败,收件人={},响应={}", urid,
response == null ? "null" : response.getBody());
// 失败可能是 access_token 失效,清掉缓存让下次重新获取
clearAccessToken();
} else {
log.info("钉钉消息发送成功,收件人={},taskId={}", urid, response.getTaskId());
}
}
return allSuccess;
}
/**
* 免登陆接口
*
* @param authCode
* @return
*/
public Map noLogin(String authCode) {
try {
String urlString = "https://oapi.dingtalk.com/topapi/v2/user/getuserinfo";
String params = "access_token=" + getAccessToken() + "&code=" + authCode;
String result = HttpUtils.sendPost(urlString, params);
ObjectMapper objectMapper = new ObjectMapper();
Map map = objectMapper.readValue(result, Map.class);
return map;
} catch (Exception e) {
log.error(e.getMessage());
new BaseException("登录出异常了,请联系管理员处理");
}
return null;
}
@Override
public void deptidList(Long deptID, Integer begin) {
try {
//根据传过来的部门ID,去获取该部门下的用户信息
if (begin != 1) DingUserInfoList(deptID);
//获取子部门的dept_id
DingTalkClient client = newClient("https://oapi.dingtalk.com/topapi/v2/department/listsub");
OapiV2DepartmentListsubRequest req = new OapiV2DepartmentListsubRequest();
req.setDeptId(deptID);
req.setLanguage("zh_CN");
OapiV2DepartmentListsubResponse rsp = client.execute(req, getAccessToken());
ObjectMapper objectMapper = new ObjectMapper();
Map map = objectMapper.readValue(rsp.getBody(), Map.class);
if (ObjectUtils.isNotEmpty(map)) {
List