飞书提醒

pull/1121/head
梁宇奇 2025-05-18 18:07:35 +08:00
parent 179bc4b0dd
commit 7fcdedfd41
9 changed files with 398 additions and 4 deletions

View File

@ -6,6 +6,7 @@ import com.ruoyi.common.core.controller.XktBaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.PageVO;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.notice.fs.FsNotice;
import com.ruoyi.web.controller.xkt.vo.BasePageVO;
import com.ruoyi.web.controller.xkt.vo.account.*;
import com.ruoyi.xkt.dto.account.*;
@ -35,6 +36,8 @@ public class AssetController extends XktBaseController {
private IAssetService assetService;
@Autowired
private AliPaymentMangerImpl aliPaymentManger;
@Autowired
private FsNotice fsNotice;
@ApiOperation(value = "档口资产")
@GetMapping(value = "store/current")
@ -89,8 +92,8 @@ public class AssetController extends XktBaseController {
//付款单到账
if (success) {
assetService.withdrawSuccess(prepareResult.getFinanceBillId());
}else {
//TODO 告警-人工介入
} else {
fsNotice.sendMsg2DefaultChat("档口提现异常", "付款单: " + prepareResult.getFinanceBillId());
}
return success();
}

View File

@ -0,0 +1,44 @@
package com.ruoyi.framework.notice;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpUtil;
import com.ruoyi.common.core.redis.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Map;
/**
* @author liangyq
* @date 2025-05-18 17:15
*/
@Slf4j
public abstract class AbstractNotice {
@Autowired
protected RedisCache redisCache;
protected String httpGet(String url, Map<String, String> headers, Map<String, Object> paramMap) {
HttpRequest httpRequest = HttpUtil.createGet(url);
if (MapUtil.isNotEmpty(headers)) {
httpRequest.addHeaders(headers);
}
if (MapUtil.isNotEmpty(paramMap)) {
httpRequest.form(paramMap);
}
return httpRequest.execute().body();
}
protected String httpPost(String url, Map<String, String> headers, String body) {
HttpRequest httpRequest = HttpUtil.createPost(url);
if (MapUtil.isNotEmpty(headers)) {
httpRequest.addHeaders(headers);
}
if (StrUtil.isNotBlank(body)) {
httpRequest.body(body);
}
return httpRequest.execute().body();
}
}

View File

@ -0,0 +1,167 @@
package com.ruoyi.framework.notice.fs;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.notice.AbstractNotice;
import com.ruoyi.framework.notice.fs.entity.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @author liangyq
* @date 2025-05-18 17:14
*/
@Slf4j
@Component
public class FsNotice extends AbstractNotice {
public static final String FEI_SHU_ROBOT_TOKEN_CACHE_PREFIX = "FS_ROBOT_TOKEN_";
public static final String FEI_SHU_ROBOT_TOKEN_REQUEST_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/";
public static final String FEI_SHU_ROBOT_SEND_MSG_URL = "https://open.feishu.cn/open-apis/message/v4/send/";
@Value("${fs.robot.appId:cli_a8a3175e84f8500b}")
private String defaultAppId;
@Value("${fs.robot.appSecret:AZTpyNiW4hpLRXcDhrdjJcr8Z404gVqf}")
private String defaultAppSecret;
@Value("${fs.robot.defaultChatId:oc_4f66ce3cf471f50aac6c3fdae7f0aad9}")
private String defaultChatId;
/**
*
*
* @param title
* @param content
*/
public void sendMsg2DefaultChat(String title, String content) {
ThreadUtil.execAsync(() -> sendMsg(title, content, new String[]{defaultChatId}));
}
/**
*
*
* @param title
* @param content
* @param chartIds
* @param params
*/
private void sendMsg(String title, String content, String[] chartIds, Object... params) {
try {
List<FeiShuMsg> msgs = createFeiShuMsg(title, content, chartIds, params);
for (FeiShuMsg msg : msgs) {
String response = httpPost(FEI_SHU_ROBOT_SEND_MSG_URL, createDefaultHeader(), JSON.toJSONString(msg));
log.info("[FS]发送消息: msg={}, response={}", msg, response);
}
} catch (Exception e) {
log.error("[FS]发送消息异常", e);
}
}
/**
*
*
* @param title
* @param content
* @param chartIds
* @param params
* @return
*/
private List<FeiShuMsg> createFeiShuMsg(String title, String content, String[] chartIds, Object... params) {
List<FeiShuMsg> msgs = new ArrayList<>(chartIds.length);
for (String chartId : chartIds) {
//标题和内容
FeiShuMsg.ZhCn zhCn = new FeiShuMsg.ZhCn();
zhCn.setTitle(title);
String textContent = ArrayUtil.isNotEmpty(params) ? StrUtil.indexedFormat(content, params) : content;
List<List<FeiShuMsg.BaseField>> contentField = Collections.singletonList(Arrays
.asList(FeiShuTextField.createText(textContent), FeiShuAtField.createAtAll()));
zhCn.setContent(contentField);
//消息体
FeiShuMsg feiShuMsg = new FeiShuMsg();
feiShuMsg.setChatId(chartId);
feiShuMsg.setMsgType("post");
feiShuMsg.setContent(FeiShuMsg.Content.builder().post(FeiShuMsg.Post.builder().zhCn(zhCn).build()).build());
msgs.add(feiShuMsg);
}
return msgs;
}
/**
*
*
* @return
*/
private Map<String, String> createDefaultHeader() {
return createHeaderWithAuth(getToken(defaultAppId, defaultAppSecret));
}
/**
*
*
* @param token token
* @return
*/
private Map<String, String> createHeaderWithAuth(String token) {
Map<String, String> headers = new HashMap<>(2);
headers.put("Content-Type", "application/json; charset=utf-8");
if (StrUtil.isNotEmpty(token)) {
//值格式:"Bearer access_token"
headers.put("Authorization", "Bearer ".concat(token));
}
return headers;
}
/**
* token
*
* @param appId id
* @param appSecret
* @return token
*/
private String getToken(String appId, String appSecret) {
String token = redisCache.getCacheObject(FEI_SHU_ROBOT_TOKEN_CACHE_PREFIX.concat(appId));
if (StrUtil.isEmpty(token)) {
synchronized (this) {
token = redisCache.getCacheObject(FEI_SHU_ROBOT_TOKEN_CACHE_PREFIX.concat(appId));
if (StrUtil.isEmpty(token)) {
token = refreshToken(appId, appSecret);
}
}
}
return token;
}
/**
* token
*
* @param appId id
* @param appSecret
* @return
*/
private String refreshToken(String appId, String appSecret) {
String response = httpPost(FEI_SHU_ROBOT_TOKEN_REQUEST_URL, null,
JSON.toJSONString(FeiShuTokenReq.builder().appId(appId).appSecret(appSecret).build()));
FeiShuTokenResp respToken = StrUtil.isEmpty(response) ?
null : JSON.parseObject(response, FeiShuTokenResp.class);
if (Objects.isNull(respToken) || StrUtil.isEmpty(respToken.getTenantAccessToken())) {
throw new ServiceException("[FS]TOKEN获取失败");
}
String token = respToken.getTenantAccessToken();
//Token 有效期为 2 小时,在此期间调用该接口 token 不会改变。
//当 token 有效期小于 30 分的时候,再次请求获取 token 的时候,会生成一个新的 token与此同时老的 token 依然有效。
int expire = Integer.parseInt(respToken.getExpire()) - 30 * 60;
redisCache.setCacheObject(FEI_SHU_ROBOT_TOKEN_CACHE_PREFIX.concat(appId), token, expire > 0 ? expire : 1,
TimeUnit.SECONDS);
log.info("[FS]TOKEN缓存刷新成功appId={}expire={}", appId, expire);
return token;
}
}

View File

@ -0,0 +1,25 @@
package com.ruoyi.framework.notice.fs.entity;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* @author liangyuqi
* @date 2021/7/14 15:20
*/
@Data
public class FeiShuAtField extends FeiShuMsg.BaseField {
private String tag = "at";
@JSONField(name = "user_id")
private String userId;
public static FeiShuAtField createAtAll() {
return createAt("all");
}
public static FeiShuAtField createAt(String userId) {
FeiShuAtField feiShuAtField = new FeiShuAtField();
feiShuAtField.setUserId(userId);
return feiShuAtField;
}
}

View File

@ -0,0 +1,76 @@
package com.ruoyi.framework.notice.fs.entity;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
*
*
* {
* "open_id":"ou_5ad573a6411d72b8305fda3a9c15c70e",
* "root_id":"om_40eb06e7b84dc71c03e009ad3c754195",
* "chat_id":"oc_5ad11d72b830411d72b836c20",
* "user_id": "92e39a99",
* "email":"fanlv@gmail.com",
* "msg_type":"post",
* "content":{
* "post":{
* "zh_cn":{}, // option
* "ja_jp":{}, // option
* "en_us":{} // option
* }
* }
* }
*
* @author liangyuqi
* @date 2021/7/14 10:52
*/
@Data
public class FeiShuMsg {
@JSONField(name = "open_id")
private String openId;
@JSONField(name = "root_id")
private String rootId;
@JSONField(name = "chat_id")
private String chatId;
@JSONField(name = "user_id")
private String userId;
private String email;
@JSONField(name = "msg_type")
private String msgType;
private Content content;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Content {
private Post post;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class Post {
@JSONField(name = "zh_cn")
private ZhCn zhCn;
}
@Data
public static class ZhCn {
private String title;
private List<List<BaseField>> content;
}
@Data
public static abstract class BaseField {
private String tag;
}
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.framework.notice.fs.entity;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
/**
* @author liangyuqi
* @date 2021/7/14 15:20
*/
@Data
public class FeiShuTextField extends FeiShuMsg.BaseField {
@JSONField(name = "un_escape")
private boolean unEscape = true;
private String tag = "text";
private String text;
public static FeiShuTextField createText(String text) {
FeiShuTextField feiShuTextField = new FeiShuTextField();
feiShuTextField.setText(text);
return feiShuTextField;
}
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.framework.notice.fs.entity;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author liangyuqi
* @date 2021/7/14 14:12
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FeiShuTokenReq {
@JSONField(name = "app_id")
private String appId;
@JSONField(name = "app_secret")
private String appSecret;
}

View File

@ -0,0 +1,22 @@
package com.ruoyi.framework.notice.fs.entity;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author liangyuqi
* @date 2021/7/14 14:12
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class FeiShuTokenResp {
@JSONField(name = "tenant_access_token")
private String tenantAccessToken;
@JSONField(name = "expire")
private String expire;
}

View File

@ -1,6 +1,5 @@
package com.ruoyi.xkt.manager.impl;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.NumberUtil;
@ -12,8 +11,8 @@ import com.alipay.api.diagnosis.DiagnosisUtils;
import com.alipay.api.domain.*;
import com.alipay.api.request.*;
import com.alipay.api.response.*;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.notice.fs.FsNotice;
import com.ruoyi.xkt.domain.StoreOrderDetail;
import com.ruoyi.xkt.dto.finance.AlipayReqDTO;
import com.ruoyi.xkt.dto.order.StoreOrderExt;
@ -25,6 +24,7 @@ import com.ruoyi.xkt.enums.EPayStatus;
import com.ruoyi.xkt.manager.PaymentManager;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@ -85,6 +85,9 @@ public class AliPaymentMangerImpl implements PaymentManager {
@Value("${alipay.gatewayUrl:https://openapi-sandbox.dl.alipaydev.com/gateway.do}")
private String gatewayUrl;
@Autowired
private FsNotice fsNotice;
@Override
public EPayChannel channel() {
return EPayChannel.ALI_PAY;
@ -165,6 +168,8 @@ public class AliPaymentMangerImpl implements PaymentManager {
default:
throw new ServiceException("未知的支付来源");
}
//告警
fsNotice.sendMsg2DefaultChat("支付发起异常", JSON.toJSONString(reqDTO));
throw new ServiceException("支付发起失败");
}
@ -216,6 +221,8 @@ public class AliPaymentMangerImpl implements PaymentManager {
} catch (Exception e) {
log.error("退款异常", e);
}
//告警
fsNotice.sendMsg2DefaultChat("退款发起异常", JSON.toJSONString(orderRefund));
throw new ServiceException("退款失败");
}
@ -262,6 +269,8 @@ public class AliPaymentMangerImpl implements PaymentManager {
} catch (Exception e) {
log.error("查询订单支付结果异常", e);
}
//告警
fsNotice.sendMsg2DefaultChat("查询订单支付结果异常", JSON.toJSONString(model));
throw new ServiceException("查询订单支付结果失败");
}
@ -314,6 +323,8 @@ public class AliPaymentMangerImpl implements PaymentManager {
} catch (Exception e) {
log.error("支付宝转账异常", e);
}
//告警
fsNotice.sendMsg2DefaultChat("支付宝转账异常", JSON.toJSONString(model));
throw new ServiceException("支付宝转账失败");
}
@ -362,6 +373,8 @@ public class AliPaymentMangerImpl implements PaymentManager {
} catch (Exception e) {
log.error("查询支付宝转账结果异常", e);
}
//告警
fsNotice.sendMsg2DefaultChat("查询支付宝转账结果异常", JSON.toJSONString(model));
throw new ServiceException("查询支付宝转账结果失败");
}