This commit is contained in:
van
2026-04-01 01:57:19 +08:00
parent 9f3fb23a91
commit f2f6d02b2f
13 changed files with 347 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
package com.ruoyi.jarvis.domain.dto;
/**
* 企微消息经 wxSend 转发至 Jarvis 的请求体
*/
public class WeComInboundRequest {
private String fromUserName;
private String content;
/** 企微 CorpId */
private String toUserName;
private String agentId;
private String msgId;
public String getFromUserName() {
return fromUserName;
}
public void setFromUserName(String fromUserName) {
this.fromUserName = fromUserName;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getToUserName() {
return toUserName;
}
public void setToUserName(String toUserName) {
this.toUserName = toUserName;
}
public String getAgentId() {
return agentId;
}
public void setAgentId(String agentId) {
this.agentId = agentId;
}
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
}

View File

@@ -65,4 +65,9 @@ public interface SuperAdminMapper
* @return 结果
*/
public int deleteSuperAdminByIds(Long[] ids);
/**
* 企微成员 UserID 对应 super_admin.wxid
*/
SuperAdmin selectSuperAdminByWecomUserId(String wxid);
}

View File

@@ -12,6 +12,11 @@ public interface ILogisticsService {
* @return 是否成功获取并推送
*/
boolean fetchLogisticsAndPush(JDOrder order);
/**
* 分享类京东物流短链:查运单并通过 wxts 推送到指定 touser不依赖订单表
*/
boolean fetchLogisticsByShareLinkAndPush(String trackingUrl, String remark, String touser);
/**
* 检查订单是否已处理过Redis中是否有运单号

View File

@@ -0,0 +1,14 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
/**
* 企微文本消息业务入口(由 wxSend 通过 HTTPS + 共享密钥调用)
*/
public interface IWeComInboundService {
/**
* @return 被动回复文本;无内容时返回空串
*/
String handleInbound(WeComInboundRequest request);
}

View File

@@ -59,4 +59,6 @@ public interface SuperAdminService
public int deleteSuperAdminById(Long id);
SuperAdmin selectSuperAdminByUnionId(Long unionId);
SuperAdmin selectSuperAdminByWecomUserId(String wxid);
}

View File

@@ -17,10 +17,13 @@ import org.springframework.beans.factory.annotation.Value;
import javax.annotation.Resource;
import javax.annotation.PostConstruct;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.springframework.util.DigestUtils;
/**
* 物流信息服务实现类
*/
@@ -31,6 +34,7 @@ public class LogisticsServiceImpl implements ILogisticsService {
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
private static final String REDIS_LOCK_KEY_PREFIX = "logistics:lock:order:";
private static final String REDIS_HEALTH_CHECK_ALERT_KEY = "logistics:health:alert:";
private static final String REDIS_ADHOC_WAYBILL_PREFIX = "logistics:adhoc:waybill:";
private static final String PUSH_URL = "https://wxts.van333.cn/wx/send/pdd";
private static final String PUSH_TOKEN = "super_token_b62190c26";
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
@@ -418,6 +422,76 @@ public class LogisticsServiceImpl implements ILogisticsService {
}
}
}
@Override
public boolean fetchLogisticsByShareLinkAndPush(String trackingUrl, String remark, String touser) {
if (!StringUtils.hasText(trackingUrl)) {
logger.warn("分享物流链接为空");
return false;
}
String url = trackingUrl.trim();
String dedupeKey = REDIS_ADHOC_WAYBILL_PREFIX + DigestUtils.md5DigestAsHex(url.getBytes(StandardCharsets.UTF_8));
try {
ILogisticsService.HealthCheckResult healthResult = checkHealth();
if (!healthResult.isHealthy()) {
logger.error("物流服务不可用adhoc 推送跳过: {}", healthResult.getMessage());
return false;
}
String externalUrl = externalApiUrlTemplate + URLEncoder.encode(url, "UTF-8");
String result = HttpUtils.sendGet(externalUrl);
if (!StringUtils.hasText(result)) {
logger.warn("物流接口空响应 adhoc");
return false;
}
JSONObject parsedData;
try {
Object parsed = JSON.parse(result);
if (!(parsed instanceof JSONObject)) {
return false;
}
parsedData = (JSONObject) parsed;
} catch (Exception e) {
logger.warn("物流响应非JSON adhoc: {}", e.getMessage());
return false;
}
JSONObject dataObj = parsedData.getJSONObject("data");
if (dataObj == null) {
return false;
}
String waybillNo = dataObj.getString("waybill_no");
if (!StringUtils.hasText(waybillNo)) {
logger.info("adhoc 暂未返回运单号(可能未发货)");
return false;
}
waybillNo = waybillNo.trim();
String existing = stringRedisTemplate.opsForValue().get(dedupeKey);
if (existing != null && existing.equals(waybillNo)) {
logger.info("adhoc 该链接已推送过运单 {}", waybillNo);
return true;
}
StringBuilder pushContent = new StringBuilder();
pushContent.append("【分享链接物流】\n");
pushContent.append("备注:").append(remark != null ? remark : "").append("\n");
pushContent.append("原链接:").append(url).append("\n");
pushContent.append("运单号:\n\n\n\n").append(waybillNo).append("\n");
JSONObject pushParam = new JSONObject();
pushParam.put("title", "JD物流(分享链接)");
pushParam.put("text", pushContent.toString());
if (StringUtils.hasText(touser)) {
pushParam.put("touser", touser.trim());
}
String pushResult = sendPostWithHeaders(PUSH_URL, pushParam.toJSONString(), PUSH_TOKEN);
boolean success = isPushResponseSuccess(pushResult);
if (success) {
stringRedisTemplate.opsForValue().set(dedupeKey, waybillNo, 30, TimeUnit.DAYS);
logger.info("adhoc 物流推送成功 waybill={}", waybillNo);
}
return success;
} catch (Exception e) {
logger.error("adhoc 物流推送异常", e);
return false;
}
}
/**
* 调用企业应用推送逻辑

View File

@@ -94,4 +94,9 @@ public class SuperAdminServiceImpl implements SuperAdminService
public SuperAdmin selectSuperAdminByUnionId(Long unionId) {
return superAdminMapper.selectSuperAdminByUnionId(unionId);
}
@Override
public SuperAdmin selectSuperAdminByWecomUserId(String wxid) {
return superAdminMapper.selectSuperAdminByWecomUserId(wxid);
}
}

View File

@@ -0,0 +1,133 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.SuperAdmin;
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
import com.ruoyi.jarvis.service.IInstructionService;
import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.jarvis.service.IWeComInboundService;
import com.ruoyi.jarvis.service.SuperAdminService;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* LinPingFan全部指令其他人员须 super_admin.wxid=企微UserID且仅「京*」指令 + 京东分享物流链接流程
*/
@Service
public class WeComInboundServiceImpl implements IWeComInboundService {
public static final String WE_COM_SUPER_USER_ID = "LinPingFan";
private static final String REDIS_PENDING = "jarvis:wecom:jdlog:wait:";
private static final int PENDING_TTL_HOURS = 24;
private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+");
private static final int REPLY_MAX_LEN = 3500;
@Resource
private SuperAdminService superAdminService;
@Resource
private IInstructionService instructionService;
@Resource
private ILogisticsService logisticsService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public String handleInbound(WeComInboundRequest req) {
if (req == null || !StringUtils.hasText(req.getFromUserName())) {
return "";
}
String from = req.getFromUserName().trim();
String content = req.getContent() != null ? req.getContent() : "";
SuperAdmin row = superAdminService.selectSuperAdminByWecomUserId(from);
boolean isSuper = WE_COM_SUPER_USER_ID.equals(from);
if (!isSuper && row == null) {
return "无权限:请在后台「超级管理员」中维护企微 UserIDwxid 字段)";
}
String pendingKey = REDIS_PENDING + from;
String pending = stringRedisTemplate.opsForValue().get(pendingKey);
if (StringUtils.hasText(pending)) {
String t = content.trim();
if ("取消".equals(t) || "取消录入".equals(t)) {
stringRedisTemplate.delete(pendingKey);
return "已取消物流链接录入";
}
if (isJdShareLogisticsMessage(content)) {
String url = extractJd3cnUrl(content);
if (url != null) {
stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS);
return "收到物流链接 " + url + " ,请输入备注信息";
}
}
if (t.startsWith("")) {
stringRedisTemplate.delete(pendingKey);
} else {
stringRedisTemplate.delete(pendingKey);
String touser = resolveTouser(row, isSuper);
boolean ok = logisticsService.fetchLogisticsByShareLinkAndPush(pending, content.trim(), touser);
return ok ? "已查询并推送运单(接收人见超级管理员 touser" : "物流查询或推送失败,请稍后重试";
}
}
if (isJdShareLogisticsMessage(content)) {
String url = extractJd3cnUrl(content);
if (url != null) {
stringRedisTemplate.opsForValue().set(pendingKey, url, PENDING_TTL_HOURS, TimeUnit.HOURS);
return "收到物流链接 " + url + " ,请输入备注信息";
}
}
if (!isSuper) {
String cmd = content.trim();
if (!cmd.startsWith("")) {
return "当前账号仅支持:以「京」开头的指令,或发送含 3.cn 京东物流分享链接";
}
}
List<String> parts = instructionService.execute(content, false, isSuper);
if (parts == null || parts.isEmpty()) {
return "";
}
String reply = String.join("\n", parts);
if (reply.length() > REPLY_MAX_LEN) {
reply = reply.substring(0, REPLY_MAX_LEN) + "\n...(截断)";
}
return reply;
}
private static boolean isJdShareLogisticsMessage(String text) {
if (!StringUtils.hasText(text)) {
return false;
}
return text.contains("https://3.cn/") || text.contains("http://3.cn/");
}
private static String extractJd3cnUrl(String text) {
Matcher m = JD_3CN.matcher(text);
if (m.find()) {
return m.group();
}
return null;
}
private String resolveTouser(SuperAdmin row, boolean isSuper) {
if (row != null && StringUtils.hasText(row.getTouser())) {
return row.getTouser().trim();
}
if (isSuper) {
SuperAdmin ping = superAdminService.selectSuperAdminByWecomUserId(WE_COM_SUPER_USER_ID);
if (ping != null && StringUtils.hasText(ping.getTouser())) {
return ping.getTouser().trim();
}
}
return null;
}
}

View File

@@ -43,6 +43,14 @@
where union_id = #{unionId}
</select>
<select id="selectSuperAdminByWecomUserId" parameterType="String" resultMap="SuperAdminResult">
<include refid="selectSuperAdminVo"/>
where wxid = #{wxid}
and (is_active is null or is_active = 1)
order by id asc
limit 1
</select>
<insert id="insertSuperAdmin" parameterType="SuperAdmin" useGeneratedKeys="true" keyProperty="id">
insert into super_admin
<trim prefix="(" suffix=")" suffixOverrides=",">