This commit is contained in:
van
2026-04-03 00:34:07 +08:00
parent 72d5856838
commit c841990b49
14 changed files with 634 additions and 22 deletions

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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}。

View File

@@ -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();
}

View File

@@ -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:";
/** 企微分享链+备注入队FIFORPUSH 入队、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.";
@@ -55,6 +60,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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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 &lt; #{beforeId}
and (content like '%https://3.cn/%' or content like '%http://3.cn/%')
order by id desc
limit 1
</select>
</mapper>

View File

@@ -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 &gt;= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''">
and create_time &lt;= concat(#{params.endTime}, ' 23:59:59')
</if>
</where>
order by id desc
</select>
</mapper>

View 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 行'
);

View 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)
);

View File

@@ -0,0 +1,7 @@
-- 若已执行过 wecom_share_link_logistics_job.sql 但无「从追踪补录」按钮,单独执行本文件。
-- menu_id 2096 若冲突请改未占用 IDparent_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 行'
);