1
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
import com.ruoyi.jarvis.service.IWeComShareLinkLogisticsJobService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/wecom/shareLinkLogisticsJob")
|
||||
public class WeComShareLinkLogisticsJobController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private IWeComShareLinkLogisticsJobService weComShareLinkLogisticsJobService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(WeComShareLinkLogisticsJob query) {
|
||||
startPage();
|
||||
List<WeComShareLinkLogisticsJob> list = weComShareLinkLogisticsJobService.selectList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@GetMapping("/{jobKey}")
|
||||
public AjaxResult getInfo(@PathVariable("jobKey") String jobKey) {
|
||||
return success(weComShareLinkLogisticsJobService.selectByJobKey(jobKey));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:import')")
|
||||
@PostMapping("/backfillFromInboundTrace")
|
||||
public AjaxResult backfillFromInboundTrace() {
|
||||
Map<String, Object> r = weComShareLinkLogisticsJobService.backfillImportedFromInboundTrace();
|
||||
return success(r);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 企微分享链物流任务 wecom_share_link_logistics_job
|
||||
*/
|
||||
public class WeComShareLinkLogisticsJob extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
@Excel(name = "任务Key")
|
||||
private String jobKey;
|
||||
|
||||
@Excel(name = "发送人UserID")
|
||||
private String fromUserName;
|
||||
|
||||
private String trackingUrl;
|
||||
|
||||
/** 用户填写的备注(表字段 remark,避免与 BaseEntity.remark 混淆) */
|
||||
@Excel(name = "用户备注")
|
||||
private String userRemark;
|
||||
|
||||
@Excel(name = "推送接收人")
|
||||
private String touserPush;
|
||||
|
||||
@Excel(name = "状态")
|
||||
private String status;
|
||||
|
||||
@Excel(name = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Excel(name = "扫描次数")
|
||||
private Integer scanAttempts;
|
||||
|
||||
private String lastNote;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getJobKey() {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
public void setJobKey(String jobKey) {
|
||||
this.jobKey = jobKey;
|
||||
}
|
||||
|
||||
public String getFromUserName() {
|
||||
return fromUserName;
|
||||
}
|
||||
|
||||
public void setFromUserName(String fromUserName) {
|
||||
this.fromUserName = fromUserName;
|
||||
}
|
||||
|
||||
public String getTrackingUrl() {
|
||||
return trackingUrl;
|
||||
}
|
||||
|
||||
public void setTrackingUrl(String trackingUrl) {
|
||||
this.trackingUrl = trackingUrl;
|
||||
}
|
||||
|
||||
public String getUserRemark() {
|
||||
return userRemark;
|
||||
}
|
||||
|
||||
public void setUserRemark(String userRemark) {
|
||||
this.userRemark = userRemark;
|
||||
}
|
||||
|
||||
public String getTouserPush() {
|
||||
return touserPush;
|
||||
}
|
||||
|
||||
public void setTouserPush(String touserPush) {
|
||||
this.touserPush = touserPush;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getWaybillNo() {
|
||||
return waybillNo;
|
||||
}
|
||||
|
||||
public void setWaybillNo(String waybillNo) {
|
||||
this.waybillNo = waybillNo;
|
||||
}
|
||||
|
||||
public Integer getScanAttempts() {
|
||||
return scanAttempts;
|
||||
}
|
||||
|
||||
public void setScanAttempts(Integer scanAttempts) {
|
||||
this.scanAttempts = scanAttempts;
|
||||
}
|
||||
|
||||
public String getLastNote() {
|
||||
return lastNote;
|
||||
}
|
||||
|
||||
public void setLastNote(String lastNote) {
|
||||
this.lastNote = lastNote;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComInboundTrace;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -15,4 +16,15 @@ public interface WeComInboundTraceMapper {
|
||||
int deleteWeComInboundTraceByIds(Long[] ids);
|
||||
|
||||
int deleteAllWeComInboundTrace();
|
||||
|
||||
/**
|
||||
* reply 中含「已加入查询队列」的消息(企微分享链备注提交成功后的被动回复)
|
||||
*/
|
||||
List<WeComInboundTrace> selectTracesShareLinkRemarkDone(@Param("replyMark") String replyMark);
|
||||
|
||||
/**
|
||||
* 同一用户、更早的一条含 3.cn 的消息(用于与备注消息配对出短链)
|
||||
*/
|
||||
WeComInboundTrace selectLatestPriorTraceWith3cnLink(@Param("fromUserName") String fromUserName,
|
||||
@Param("beforeId") long beforeId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface WeComShareLinkLogisticsJobMapper {
|
||||
|
||||
int insertWeComShareLinkLogisticsJob(WeComShareLinkLogisticsJob job);
|
||||
|
||||
int updateByJobKey(@Param("jobKey") String jobKey,
|
||||
@Param("status") String status,
|
||||
@Param("lastNote") String lastNote,
|
||||
@Param("scanAttempts") Integer scanAttempts,
|
||||
@Param("waybillNo") String waybillNo);
|
||||
|
||||
WeComShareLinkLogisticsJob selectByJobKey(String jobKey);
|
||||
|
||||
List<WeComShareLinkLogisticsJob> selectWeComShareLinkLogisticsJobList(WeComShareLinkLogisticsJob query);
|
||||
}
|
||||
@@ -20,8 +20,10 @@ public interface ILogisticsService {
|
||||
|
||||
/**
|
||||
* 企微备注提交后仅入队,由 {@link com.ruoyi.jarvis.task.LogisticsScanTask} 与订单扫描一并拉取物流,避免阻塞 HTTP 回调。
|
||||
*
|
||||
* @param fromWecomUserId 企微消息 FromUserName,入库监控并在未配 touser 时作为推送目标
|
||||
*/
|
||||
void enqueueShareLinkForScan(String trackingUrl, String remark, String touser);
|
||||
void enqueueShareLinkForScan(String trackingUrl, String remark, String touser, String fromWecomUserId);
|
||||
|
||||
/**
|
||||
* 定时任务内:依次弹出队列并调用 {@link #fetchLogisticsByShareLinkAndPush}。
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IWeComShareLinkLogisticsJobService {
|
||||
|
||||
WeComShareLinkLogisticsJob selectByJobKey(String jobKey);
|
||||
|
||||
List<WeComShareLinkLogisticsJob> selectList(WeComShareLinkLogisticsJob query);
|
||||
|
||||
/**
|
||||
* 从 wecom_inbound_trace 补录:reply 含「已加入查询队列」且能解析出 3.cn 短链。
|
||||
* jobKey 固定为 tracebf{traceId},可重复执行跳过已存在项。
|
||||
*/
|
||||
Map<String, Object> backfillImportedFromInboundTrace();
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.utils.http.HttpUtils;
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
import com.ruoyi.jarvis.mapper.WeComShareLinkLogisticsJobMapper;
|
||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||
import com.ruoyi.system.service.ISysConfigService;
|
||||
@@ -20,6 +22,7 @@ import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.util.DigestUtils;
|
||||
@@ -37,6 +40,8 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
||||
private static final String REDIS_ADHOC_WAYBILL_PREFIX = "logistics:adhoc:waybill:";
|
||||
/** 企微分享链+备注入队,FIFO:RPUSH 入队、LPOP 出队 */
|
||||
private static final String REDIS_ADHOC_PENDING_QUEUE = "logistics:adhoc:pending:queue";
|
||||
/** 单次队列项最多重新入队次数(约对应多天 × 每 10 分钟一轮) */
|
||||
private static final int ADHOC_MAX_REQUEUE_ATTEMPTS = 500;
|
||||
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.";
|
||||
@@ -54,6 +59,9 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
||||
|
||||
@Value("${jarvis.server.logistics.adhoc-pending-batch-size:50}")
|
||||
private int adhocPendingBatchSize;
|
||||
|
||||
@Resource
|
||||
private WeComShareLinkLogisticsJobMapper weComShareLinkLogisticsJobMapper;
|
||||
|
||||
private String externalApiUrlTemplate;
|
||||
private String healthCheckUrl;
|
||||
@@ -429,17 +437,34 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enqueueShareLinkForScan(String trackingUrl, String remark, String touser) {
|
||||
public void enqueueShareLinkForScan(String trackingUrl, String remark, String touser, String fromWecomUserId) {
|
||||
if (!StringUtils.hasText(trackingUrl)) {
|
||||
logger.warn("adhoc 入队跳过:分享链接为空");
|
||||
return;
|
||||
}
|
||||
String jobKey = UUID.randomUUID().toString().replace("-", "");
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("jobKey", jobKey);
|
||||
o.put("attempts", 0);
|
||||
o.put("trackingUrl", trackingUrl.trim());
|
||||
o.put("remark", remark != null ? remark : "");
|
||||
o.put("touser", touser != null ? touser : "");
|
||||
o.put("fromWecom", fromWecomUserId != null ? fromWecomUserId : "");
|
||||
try {
|
||||
WeComShareLinkLogisticsJob row = new WeComShareLinkLogisticsJob();
|
||||
row.setJobKey(jobKey);
|
||||
row.setFromUserName(StringUtils.hasText(fromWecomUserId) ? fromWecomUserId.trim() : null);
|
||||
row.setTrackingUrl(trackingUrl.trim());
|
||||
row.setUserRemark(remark != null ? remark : "");
|
||||
row.setTouserPush(touser != null ? touser : "");
|
||||
row.setStatus("PENDING");
|
||||
row.setScanAttempts(0);
|
||||
weComShareLinkLogisticsJobMapper.insertWeComShareLinkLogisticsJob(row);
|
||||
} catch (Exception e) {
|
||||
logger.warn("adhoc 分享链任务落库失败仍将入队 jobKey={} err={}", jobKey, e.toString());
|
||||
}
|
||||
stringRedisTemplate.opsForList().rightPush(REDIS_ADHOC_PENDING_QUEUE, o.toJSONString());
|
||||
logger.info("adhoc 分享链接已入队待定时扫描(与订单物流任务一致) trackingUrl={} queueKey={}", trackingUrl.trim(), REDIS_ADHOC_PENDING_QUEUE);
|
||||
logger.info("adhoc 分享链接已入队 jobKey={} trackingUrl={} queueKey={}", jobKey, trackingUrl.trim(), REDIS_ADHOC_PENDING_QUEUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -458,7 +483,23 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
||||
String url = o.getString("trackingUrl");
|
||||
String remark = o.getString("remark");
|
||||
String touser = o.getString("touser");
|
||||
fetchLogisticsByShareLinkAndPush(url, remark, touser);
|
||||
String jobKey = o.getString("jobKey");
|
||||
int attempts = o.getIntValue("attempts");
|
||||
AdhocTryResult tr = tryAdhocShareLinkOnce(url, remark, touser);
|
||||
if (tr.needsRequeue) {
|
||||
if (attempts >= ADHOC_MAX_REQUEUE_ATTEMPTS) {
|
||||
logger.warn("adhoc 已达最大重试次数 {},放弃 jobKey={} url={} note={}",
|
||||
ADHOC_MAX_REQUEUE_ATTEMPTS, jobKey, url, tr.note);
|
||||
touchShareLinkJobRow(jobKey, "ABANDONED", tr.note, attempts + 1, null);
|
||||
continue;
|
||||
}
|
||||
o.put("attempts", attempts + 1);
|
||||
stringRedisTemplate.opsForList().rightPush(REDIS_ADHOC_PENDING_QUEUE, o.toJSONString());
|
||||
touchShareLinkJobRow(jobKey, "WAITING", tr.note, attempts + 1, null);
|
||||
} else {
|
||||
touchShareLinkJobRow(jobKey, "PUSHED",
|
||||
tr.note, attempts + 1, StringUtils.hasText(tr.waybillNo) ? tr.waybillNo : null);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("adhoc 队列项处理失败 raw={}", json.length() > 200 ? json.substring(0, 200) + "..." : json, e);
|
||||
}
|
||||
@@ -477,45 +518,67 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
||||
logger.warn("分享物流链接为空");
|
||||
return false;
|
||||
}
|
||||
AdhocTryResult tr = tryAdhocShareLinkOnce(trackingUrl, remark, touser);
|
||||
return !tr.needsRequeue;
|
||||
}
|
||||
|
||||
private void touchShareLinkJobRow(String jobKey, String status, String lastNote, int scanAttempts, String waybillNo) {
|
||||
if (!StringUtils.hasText(jobKey)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
weComShareLinkLogisticsJobMapper.updateByJobKey(jobKey.trim(), status, lastNote, scanAttempts, waybillNo);
|
||||
} catch (Exception e) {
|
||||
logger.warn("adhoc 任务状态更新失败 jobKey={} err={}", jobKey, e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一条分享链:需要重新入队时 {@link #needsRequeue} 为 true(未发货、接口异常、推送失败等)。
|
||||
*/
|
||||
private AdhocTryResult tryAdhocShareLinkOnce(String trackingUrl, String remark, String touser) {
|
||||
if (!StringUtils.hasText(trackingUrl)) {
|
||||
return AdhocTryResult.requeue("empty_url");
|
||||
}
|
||||
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;
|
||||
logger.error("物流服务不可用,adhoc 将重试: {}", healthResult.getMessage());
|
||||
return AdhocTryResult.requeue("health:" + healthResult.getMessage());
|
||||
}
|
||||
String externalUrl = externalApiUrlTemplate + URLEncoder.encode(url, "UTF-8");
|
||||
String result = HttpUtils.sendGet(externalUrl);
|
||||
if (!StringUtils.hasText(result)) {
|
||||
logger.warn("物流接口空响应 adhoc");
|
||||
return false;
|
||||
logger.warn("物流接口空响应 adhoc,将重试");
|
||||
return AdhocTryResult.requeue("empty_http_body");
|
||||
}
|
||||
JSONObject parsedData;
|
||||
try {
|
||||
Object parsed = JSON.parse(result);
|
||||
if (!(parsed instanceof JSONObject)) {
|
||||
return false;
|
||||
return AdhocTryResult.requeue("not_json_object");
|
||||
}
|
||||
parsedData = (JSONObject) parsed;
|
||||
} catch (Exception e) {
|
||||
logger.warn("物流响应非JSON adhoc: {}", e.getMessage());
|
||||
return false;
|
||||
return AdhocTryResult.requeue("json_parse:" + e.getMessage());
|
||||
}
|
||||
JSONObject dataObj = parsedData.getJSONObject("data");
|
||||
if (dataObj == null) {
|
||||
return false;
|
||||
return AdhocTryResult.requeue("no_data_field");
|
||||
}
|
||||
String waybillNo = dataObj.getString("waybill_no");
|
||||
if (!StringUtils.hasText(waybillNo)) {
|
||||
logger.info("adhoc 暂未返回运单号(可能未发货)");
|
||||
return false;
|
||||
logger.info("adhoc 暂未返回运单号(可能未发货),将重新入队");
|
||||
return AdhocTryResult.requeue("no_waybill_yet");
|
||||
}
|
||||
waybillNo = waybillNo.trim();
|
||||
String existing = stringRedisTemplate.opsForValue().get(dedupeKey);
|
||||
if (existing != null && existing.equals(waybillNo)) {
|
||||
logger.info("adhoc 该链接已推送过运单 {}", waybillNo);
|
||||
return true;
|
||||
return AdhocTryResult.done("already_pushed", waybillNo);
|
||||
}
|
||||
StringBuilder pushContent = new StringBuilder();
|
||||
pushContent.append("【分享链接物流】\n");
|
||||
@@ -527,17 +590,42 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
||||
pushParam.put("text", pushContent.toString());
|
||||
if (StringUtils.hasText(touser)) {
|
||||
pushParam.put("touser", touser.trim());
|
||||
} else {
|
||||
logger.info("adhoc 推送未设置接收人 touser,将使用远程接口默认接收人(与订单未配分销接收人时一致)");
|
||||
}
|
||||
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 AdhocTryResult.done("pushed", waybillNo);
|
||||
}
|
||||
return success;
|
||||
logger.warn("adhoc 推送接口未成功,将重新入队 waybill={} respSnippet={}",
|
||||
waybillNo, pushResult != null && pushResult.length() > 120 ? pushResult.substring(0, 120) : pushResult);
|
||||
return AdhocTryResult.requeue("push_failed");
|
||||
} catch (Exception e) {
|
||||
logger.error("adhoc 物流推送异常", e);
|
||||
return false;
|
||||
logger.error("adhoc 物流推送异常,将重新入队", e);
|
||||
return AdhocTryResult.requeue("exception:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static final class AdhocTryResult {
|
||||
final boolean needsRequeue;
|
||||
final String note;
|
||||
final String waybillNo;
|
||||
|
||||
private AdhocTryResult(boolean needsRequeue, String note, String waybillNo) {
|
||||
this.needsRequeue = needsRequeue;
|
||||
this.note = note;
|
||||
this.waybillNo = waybillNo;
|
||||
}
|
||||
|
||||
static AdhocTryResult requeue(String note) {
|
||||
return new AdhocTryResult(true, note, null);
|
||||
}
|
||||
|
||||
static AdhocTryResult done(String note, String waybillNo) {
|
||||
return new AdhocTryResult(false, note, waybillNo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,9 +130,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
String url = extractJd3cnUrl(content);
|
||||
if (url != null && url.equals(session.getLogisticsUrl())) {
|
||||
weComChatSessionService.delete(from);
|
||||
String touser = resolveTouser(row, isSuper);
|
||||
String touser = resolveTouser(row, isSuper, from);
|
||||
log.info("企微物流会话备注(含同款链接)提交 user={} url={} remarkLen={}", from, url, t.length());
|
||||
logisticsService.enqueueShareLinkForScan(url, content.trim(), touser);
|
||||
logisticsService.enqueueShareLinkForScan(url, content.trim(), touser, from);
|
||||
return WeComInboundResult.passiveOnly(replyLogisticsRemarkDone());
|
||||
}
|
||||
if (url != null) {
|
||||
@@ -145,9 +145,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
} else {
|
||||
String url = session.getLogisticsUrl();
|
||||
weComChatSessionService.delete(from);
|
||||
String touser = resolveTouser(row, isSuper);
|
||||
String touser = resolveTouser(row, isSuper, from);
|
||||
log.info("企微物流会话提交备注 user={} url={} remarkLen={}", from, url, t.length());
|
||||
logisticsService.enqueueShareLinkForScan(url, content.trim(), touser);
|
||||
logisticsService.enqueueShareLinkForScan(url, content.trim(), touser, from);
|
||||
return WeComInboundResult.passiveOnly(replyLogisticsRemarkDone());
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,11 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String resolveTouser(SuperAdmin row, boolean isSuper) {
|
||||
/**
|
||||
* 分享链推送目标:超级管理员表中的 touser(可多选)优先;否则回退为发消息成员的企微 UserID。
|
||||
* 仅配 wxid 未配 touser 时,过去会导致请求不带接收人、依赖远端默认路由,易出现「看不到推送」。
|
||||
*/
|
||||
private String resolveTouser(SuperAdmin row, boolean isSuper, String fromWecomUserId) {
|
||||
if (row != null && StringUtils.hasText(row.getTouser())) {
|
||||
return row.getTouser().trim();
|
||||
}
|
||||
@@ -235,6 +239,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||
return ping.getTouser().trim();
|
||||
}
|
||||
}
|
||||
if (StringUtils.hasText(fromWecomUserId)) {
|
||||
return fromWecomUserId.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.ruoyi.jarvis.service.impl;
|
||||
|
||||
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComInboundTrace;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
|
||||
import com.ruoyi.jarvis.mapper.WeComInboundTraceMapper;
|
||||
|
||||
import com.ruoyi.jarvis.mapper.WeComShareLinkLogisticsJobMapper;
|
||||
|
||||
import com.ruoyi.jarvis.service.IWeComShareLinkLogisticsJobService;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
|
||||
@Service
|
||||
|
||||
public class WeComShareLinkLogisticsJobServiceImpl implements IWeComShareLinkLogisticsJobService {
|
||||
|
||||
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WeComShareLinkLogisticsJobServiceImpl.class);
|
||||
|
||||
|
||||
|
||||
/** 与 WeComInboundServiceImpl.replyLogisticsRemarkDone 中文案一致 */
|
||||
|
||||
private static final String REPLY_MARK_SHARE_LINK_DONE = "已加入查询队列";
|
||||
|
||||
|
||||
|
||||
private static final Pattern JD_3CN_HTTPS = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+");
|
||||
|
||||
private static final Pattern JD_3CN_HTTP = Pattern.compile("http://3\\.cn/[A-Za-z0-9\\-]+");
|
||||
|
||||
|
||||
|
||||
@Resource
|
||||
|
||||
private WeComShareLinkLogisticsJobMapper weComShareLinkLogisticsJobMapper;
|
||||
|
||||
@Resource
|
||||
|
||||
private WeComInboundTraceMapper weComInboundTraceMapper;
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
|
||||
public WeComShareLinkLogisticsJob selectByJobKey(String jobKey) {
|
||||
|
||||
return weComShareLinkLogisticsJobMapper.selectByJobKey(jobKey);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
|
||||
public List<WeComShareLinkLogisticsJob> selectList(WeComShareLinkLogisticsJob query) {
|
||||
|
||||
return weComShareLinkLogisticsJobMapper.selectWeComShareLinkLogisticsJobList(query);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
|
||||
public Map<String, Object> backfillImportedFromInboundTrace() {
|
||||
|
||||
int imported = 0;
|
||||
|
||||
int skippedDuplicate = 0;
|
||||
|
||||
int skippedNoUrl = 0;
|
||||
|
||||
int scanned = 0;
|
||||
|
||||
|
||||
|
||||
List<WeComInboundTrace> traces = weComInboundTraceMapper.selectTracesShareLinkRemarkDone(REPLY_MARK_SHARE_LINK_DONE);
|
||||
|
||||
if (traces != null) {
|
||||
|
||||
for (WeComInboundTrace t : traces) {
|
||||
|
||||
if (t == null || t.getId() == null || !StringUtils.hasText(t.getFromUserName())) {
|
||||
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
scanned++;
|
||||
|
||||
String jobKey = "tracebf" + t.getId();
|
||||
|
||||
if (weComShareLinkLogisticsJobMapper.selectByJobKey(jobKey) != null) {
|
||||
|
||||
skippedDuplicate++;
|
||||
|
||||
continue;
|
||||
@@ -65,4 +65,19 @@
|
||||
<delete id="deleteAllWeComInboundTrace">
|
||||
delete from wecom_inbound_trace
|
||||
</delete>
|
||||
|
||||
<select id="selectTracesShareLinkRemarkDone" resultMap="WeComInboundTraceResult">
|
||||
<include refid="selectVo"/>
|
||||
where reply_content like concat('%', #{replyMark}, '%')
|
||||
order by id asc
|
||||
</select>
|
||||
|
||||
<select id="selectLatestPriorTraceWith3cnLink" resultMap="WeComInboundTraceResult">
|
||||
<include refid="selectVo"/>
|
||||
where from_user_name = #{fromUserName}
|
||||
and id < #{beforeId}
|
||||
and (content like '%https://3.cn/%' or content like '%http://3.cn/%')
|
||||
order by id desc
|
||||
limit 1
|
||||
</select>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.ruoyi.jarvis.mapper.WeComShareLinkLogisticsJobMapper">
|
||||
|
||||
<resultMap id="WeComShareLinkLogisticsJobResult" type="WeComShareLinkLogisticsJob">
|
||||
<id property="id" column="id"/>
|
||||
<result property="jobKey" column="job_key"/>
|
||||
<result property="fromUserName" column="from_user_name"/>
|
||||
<result property="trackingUrl" column="tracking_url"/>
|
||||
<result property="userRemark" column="remark"/>
|
||||
<result property="touserPush" column="touser_push"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="waybillNo" column="waybill_no"/>
|
||||
<result property="scanAttempts" column="scan_attempts"/>
|
||||
<result property="lastNote" column="last_note"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectVo">
|
||||
select id, job_key, from_user_name, tracking_url, remark, touser_push, status, waybill_no,
|
||||
scan_attempts, last_note, create_time, update_time
|
||||
from wecom_share_link_logistics_job
|
||||
</sql>
|
||||
|
||||
<insert id="insertWeComShareLinkLogisticsJob" parameterType="WeComShareLinkLogisticsJob" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into wecom_share_link_logistics_job (
|
||||
job_key, from_user_name, tracking_url, remark, touser_push, status, scan_attempts, last_note
|
||||
<if test="createTime != null">, create_time, update_time</if>
|
||||
) values (
|
||||
#{jobKey}, #{fromUserName}, #{trackingUrl}, #{userRemark}, #{touserPush}, #{status},
|
||||
#{scanAttempts}, #{lastNote}
|
||||
<if test="createTime != null">, #{createTime}, #{createTime}</if>
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateByJobKey">
|
||||
update wecom_share_link_logistics_job
|
||||
<set>
|
||||
<if test="status != null">status = #{status},</if>
|
||||
<if test="lastNote != null">last_note = #{lastNote},</if>
|
||||
<if test="scanAttempts != null">scan_attempts = #{scanAttempts},</if>
|
||||
<if test="waybillNo != null">waybill_no = #{waybillNo},</if>
|
||||
update_time = now()
|
||||
</set>
|
||||
where job_key = #{jobKey}
|
||||
</update>
|
||||
|
||||
<select id="selectByJobKey" resultMap="WeComShareLinkLogisticsJobResult">
|
||||
<include refid="selectVo"/>
|
||||
where job_key = #{jobKey}
|
||||
</select>
|
||||
|
||||
<select id="selectWeComShareLinkLogisticsJobList" parameterType="WeComShareLinkLogisticsJob" resultMap="WeComShareLinkLogisticsJobResult">
|
||||
<include refid="selectVo"/>
|
||||
<where>
|
||||
<if test="jobKey != null and jobKey != ''">and job_key = #{jobKey}</if>
|
||||
<if test="fromUserName != null and fromUserName != ''">and from_user_name like concat('%', #{fromUserName}, '%')</if>
|
||||
<if test="status != null and status != ''">and status = #{status}</if>
|
||||
<if test="trackingUrl != null and trackingUrl != ''">and tracking_url like concat('%', #{trackingUrl}, '%')</if>
|
||||
<if test="userRemark != null and userRemark != ''">and remark like concat('%', #{userRemark}, '%')</if>
|
||||
<if test="waybillNo != null and waybillNo != ''">and waybill_no like concat('%', #{waybillNo}, '%')</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and create_time >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and create_time <= concat(#{params.endTime}, ' 23:59:59')
|
||||
</if>
|
||||
</where>
|
||||
order by id desc
|
||||
</select>
|
||||
</mapper>
|
||||
30
sql/wecom_share_link_logistics_job.sql
Normal file
30
sql/wecom_share_link_logistics_job.sql
Normal file
@@ -0,0 +1,30 @@
|
||||
-- 企微「3.cn 分享链 + 备注」入队后的扫描/推送任务(便于排查未推送原因)
|
||||
CREATE TABLE IF NOT EXISTS `wecom_share_link_logistics_job` (
|
||||
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
`job_key` varchar(64) NOT NULL COMMENT '与 Redis 队列 JSON 中 jobKey 一致',
|
||||
`from_user_name` varchar(128) DEFAULT NULL COMMENT '发消息的企微 UserID',
|
||||
`tracking_url` varchar(768) NOT NULL COMMENT '3.cn 物流短链',
|
||||
`remark` mediumtext COMMENT '用户备注',
|
||||
`touser_push` varchar(512) DEFAULT NULL COMMENT '解析后的推送接收人(企微成员 UserID,多个逗号分隔)',
|
||||
`status` varchar(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/WAITING/PUSHED/ABANDONED/IMPORTED',
|
||||
`waybill_no` varchar(128) DEFAULT NULL COMMENT '成功解析并推送后的运单号',
|
||||
`scan_attempts` int(11) NOT NULL DEFAULT 0 COMMENT '已扫描次数(含重新入队)',
|
||||
`last_note` varchar(512) DEFAULT NULL COMMENT '最近一次处理说明',
|
||||
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_job_key` (`job_key`),
|
||||
KEY `idx_status` (`status`),
|
||||
KEY `idx_from_user` (`from_user_name`),
|
||||
KEY `idx_create_time` (`create_time`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企微分享链物流扫描任务';
|
||||
|
||||
-- 菜单(与 wecom_inbound_trace 并列);menu_id 若冲突请在库中改大号
|
||||
INSERT INTO sys_menu VALUES (
|
||||
2094, '企微分享链物流', 2, 9, 'wecomShareLinkLogistics', 'jarvis/wecomShareLinkLogistics/index', '', '', 1, 0, 'C', '0', '0',
|
||||
'jarvis:wecom:shareLinkLog:list', 'guide', 'admin', sysdate(), '', NULL, '监控企微录入的 3.cn 物流任务与推送状态'
|
||||
);
|
||||
INSERT INTO sys_menu VALUES (
|
||||
2096, '从追踪补录历史', 2094, 1, '#', '', '', '', 1, 0, 'F', '0', '0',
|
||||
'jarvis:wecom:shareLinkLog:import', '#', 'admin', sysdate(), '', NULL, '从 wecom_inbound_trace 补录 IMPORTED 行'
|
||||
);
|
||||
48
sql/wecom_share_link_logistics_job_backfill_historical.sql
Normal file
48
sql/wecom_share_link_logistics_job_backfill_historical.sql
Normal file
@@ -0,0 +1,48 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 历史补录(可选):从 wecom_inbound_trace 批量写入 wecom_share_link_logistics_job
|
||||
--
|
||||
-- 优先使用后台「企微分享链物流」页「从追踪补录历史」按钮(Java 与线上一致,支持 http/https、备注行含链等)。
|
||||
--
|
||||
-- 需 MySQL 8.0.14+(LATERAL、REGEXP_SUBSTR)。可重复执行(按 job_key 去重)。
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
INSERT INTO wecom_share_link_logistics_job (
|
||||
job_key, from_user_name, tracking_url, remark, touser_push, status, scan_attempts, last_note,
|
||||
create_time, update_time
|
||||
)
|
||||
SELECT
|
||||
CONCAT('tracebf', t.id),
|
||||
t.from_user_name,
|
||||
COALESCE(
|
||||
NULLIF(REGEXP_SUBSTR(t.content, 'https://3\\.cn/[A-Za-z0-9\\-]+'), ''),
|
||||
NULLIF(REGEXP_SUBSTR(t.content, 'http://3\\.cn/[A-Za-z0-9\\-]+'), ''),
|
||||
NULLIF(REGEXP_SUBSTR(p.content, 'https://3\\.cn/[A-Za-z0-9\\-]+'), ''),
|
||||
NULLIF(REGEXP_SUBSTR(p.content, 'http://3\\.cn/[A-Za-z0-9\\-]+'), '')
|
||||
),
|
||||
t.content,
|
||||
t.from_user_name,
|
||||
'IMPORTED',
|
||||
0,
|
||||
CONCAT('from_trace_id=', t.id, '|sql_batch'),
|
||||
t.create_time,
|
||||
t.create_time
|
||||
FROM wecom_inbound_trace t
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT p0.content
|
||||
FROM wecom_inbound_trace p0
|
||||
WHERE p0.from_user_name = t.from_user_name
|
||||
AND p0.id < t.id
|
||||
AND (p0.content LIKE '%https://3.cn/%' OR p0.content LIKE '%http://3.cn/%')
|
||||
ORDER BY p0.id DESC
|
||||
LIMIT 1
|
||||
) AS p ON TRUE
|
||||
WHERE t.reply_content LIKE '%已加入查询队列%'
|
||||
AND COALESCE(
|
||||
NULLIF(REGEXP_SUBSTR(t.content, 'https://3\\.cn/[A-Za-z0-9\\-]+'), ''),
|
||||
NULLIF(REGEXP_SUBSTR(t.content, 'http://3\\.cn/[A-Za-z0-9\\-]+'), ''),
|
||||
NULLIF(REGEXP_SUBSTR(p.content, 'https://3\\.cn/[A-Za-z0-9\\-]+'), ''),
|
||||
NULLIF(REGEXP_SUBSTR(p.content, 'http://3\\.cn/[A-Za-z0-9\\-]+'), '')
|
||||
) IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM wecom_share_link_logistics_job e WHERE e.job_key = CONCAT('tracebf', t.id)
|
||||
);
|
||||
7
sql/wecom_share_link_logistics_job_patch_menu_import.sql
Normal file
7
sql/wecom_share_link_logistics_job_patch_menu_import.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- 若已执行过 wecom_share_link_logistics_job.sql 但无「从追踪补录」按钮,单独执行本文件。
|
||||
-- menu_id 2096 若冲突请改未占用 ID;parent_id 2094 为「企微分享链物流」菜单 ID,若不同请改。
|
||||
|
||||
INSERT INTO sys_menu VALUES (
|
||||
2096, '从追踪补录历史', 2094, 1, '#', '', '', '', 1, 0, 'F', '0', '0',
|
||||
'jarvis:wecom:shareLinkLog:import', '#', 'admin', sysdate(), '', NULL, '从 wecom_inbound_trace 补录 IMPORTED 行'
|
||||
);
|
||||
Reference in New Issue
Block a user