diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/AssetController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/AssetController.java index c9b34074d..a87650098 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/AssetController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/AssetController.java @@ -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(); } diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/AbstractNotice.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/AbstractNotice.java new file mode 100644 index 000000000..5b4db938c --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/AbstractNotice.java @@ -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 headers, Map 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 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(); + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/FsNotice.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/FsNotice.java new file mode 100644 index 000000000..2d5ca1461 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/FsNotice.java @@ -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 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 createFeiShuMsg(String title, String content, String[] chartIds, Object... params) { + List 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> 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 createDefaultHeader() { + return createHeaderWithAuth(getToken(defaultAppId, defaultAppSecret)); + } + + /** + * 创建带有认证信息的请求头 + * + * @param token 飞书token + * @return + */ + private Map createHeaderWithAuth(String token) { + Map 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; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuAtField.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuAtField.java new file mode 100644 index 000000000..9bfbd8309 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuAtField.java @@ -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; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuMsg.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuMsg.java new file mode 100644 index 000000000..d9603603d --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuMsg.java @@ -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> content; + + } + + @Data + public static abstract class BaseField { + private String tag; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTextField.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTextField.java new file mode 100644 index 000000000..b952013f0 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTextField.java @@ -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; + } +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTokenReq.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTokenReq.java new file mode 100644 index 000000000..bd7eb9813 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTokenReq.java @@ -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; +} diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTokenResp.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTokenResp.java new file mode 100644 index 000000000..7248ad425 --- /dev/null +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/notice/fs/entity/FeiShuTokenResp.java @@ -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; +} diff --git a/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java b/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java index 3aceed6ea..0284b151c 100644 --- a/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java +++ b/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java @@ -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("查询支付宝转账结果失败"); }