This commit is contained in:
van
2026-04-30 17:10:34 +08:00
parent cf8008bdc1
commit a88600788a
6 changed files with 270 additions and 0 deletions

View File

@@ -27,4 +27,15 @@ public interface WeComShareLinkLogisticsJobMapper {
List<WeComShareLinkLogisticsJob> selectJobsNeedingQueueReconcile(@Param("limit") int limit);
int deleteByJobKey(@Param("jobKey") String jobKey);
/**
* 机器人「京外物列表」:最近若干天内的任务,按 id 倒序,可选备注子串筛选。
*/
List<WeComShareLinkLogisticsJob> selectRecentForInstruction(@Param("remarkKeyword") String remarkKeyword,
@Param("days") int days, @Param("limit") int limit);
/**
* 机器人「京外物删」按备注与短链精确匹配trim物理删除返回删除行数。
*/
int deleteByRemarkAndTrackingUrl(@Param("remark") String remark, @Param("trackingUrl") String trackingUrl);
}

View File

@@ -16,4 +16,10 @@ public interface IWeComShareLinkLogisticsJobService {
* jobKey 固定为 tracebf{traceId},可重复执行跳过已存在项。
*/
Map<String, Object> backfillImportedFromInboundTrace();
List<WeComShareLinkLogisticsJob> selectRecentForInstruction(String remarkKeyword, int days, int limit);
int deleteByJobKey(String jobKey);
int deleteByRemarkAndTrackingUrl(String remark, String trackingUrl);
}

View File

@@ -2,12 +2,14 @@ package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.OrderRows;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
import com.ruoyi.jarvis.service.IInstructionService;
import com.ruoyi.jarvis.service.IOrderRowsService;
import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.service.IProductJdConfigService;
import com.ruoyi.jarvis.service.IPhoneReplaceConfigService;
import com.ruoyi.jarvis.service.SuperAdminService;
import com.ruoyi.jarvis.service.IWeComShareLinkLogisticsJobService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@@ -51,12 +53,20 @@ public class InstructionServiceImpl implements IInstructionService {
@Resource
private com.ruoyi.jarvis.service.ITencentDocTokenService tencentDocTokenService;
@Resource
private IWeComShareLinkLogisticsJobService weComShareLinkLogisticsJobService;
@Resource
private com.ruoyi.jarvis.config.TencentDocConfig tencentDocConfig;
@Resource
private com.ruoyi.common.core.redis.RedisCache redisCache;
@Autowired(required = false)
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService tencentDocDelayedPushService;
/** 与企微物流分享链解析一致 */
private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+");
private static final int EXTERNAL_LOGISTICS_LIST_DAYS = 60;
private static final int EXTERNAL_LOGISTICS_LIST_LIMIT = 35;
// 录单模板(与 jd/JDUtil 中 WENAN_D 保持一致)
private static final String WENAN_D = "单:\n" + "{单号} \n" + "分销标记:{分销标记}\n" + "第三方单号:{第三方单号}\n" + "—————————\n" + "下单链接(必须用这个):\n" + "{链接}\n" + "下单地址(注意带分机):\n" + "{地址}\n" + "—————————\n" + "型号:{型号}\n" + "\n" + "下单人(需填):\n" + "\n" + "下单付款(注意核对):\n" + "\n" + "后返金额(注意核对):\n" + "\n" + "订单号(需填):\n" + "\n" + "物流链接(需填):\n" + "\n" + "备注(下单号码有变动/没法带分机号的写这里):\n" + "{单的备注}\n" + "—————————\n" + "京粉实际价格:不用填";
@@ -198,6 +208,15 @@ public class InstructionServiceImpl implements IInstructionService {
return jingMenu();
}
if (action.startsWith("外物列表")) {
String kw = action.substring("外物列表".length()).trim();
return textExternalShareLinkLogisticsList(kw);
}
if (action.startsWith("外物删")) {
String rest = action.substring("外物删".length()).trim();
return textExternalShareLinkLogisticsDelete(rest);
}
// 取出所有订单(排除被删除/无效:这里沿用 OrderRowsService 的常规查询,必要时可增加过滤参数)
List<OrderRows> all = orderRowsService.selectOrderRowsList(new OrderRows());
if (all == null) all = Collections.emptyList();
@@ -2128,6 +2147,194 @@ public class InstructionServiceImpl implements IInstructionService {
}).collect(Collectors.toList());
}
/**
* 企微「外部分享链物流」登记查询:用于发现同一备注、不同短链等重复登记。
*/
private String textExternalShareLinkLogisticsList(String remarkKeyword) {
if (weComShareLinkLogisticsJobService == null) {
return "「外物列表」\n\n服务未就绪。";
}
List<WeComShareLinkLogisticsJob> rows = weComShareLinkLogisticsJobService.selectRecentForInstruction(
remarkKeyword, EXTERNAL_LOGISTICS_LIST_DAYS, EXTERNAL_LOGISTICS_LIST_LIMIT);
if (rows == null || rows.isEmpty()) {
return "「外物列表」\n\n近 " + EXTERNAL_LOGISTICS_LIST_DAYS + " 天内无匹配记录。"
+ (remarkKeyword.isEmpty() ? "" : "\n关键词" + remarkKeyword + "");
}
Map<String, Long> remarkCount = rows.stream()
.map(j -> j.getUserRemark() != null ? j.getUserRemark().trim() : "")
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
StringBuilder sb = new StringBuilder();
sb.append("「外物列表」近").append(EXTERNAL_LOGISTICS_LIST_DAYS).append("天,最多")
.append(EXTERNAL_LOGISTICS_LIST_LIMIT).append("");
if (!remarkKeyword.isEmpty()) {
sb.append(",关键词「").append(remarkKeyword).append("");
}
sb.append("\n\n");
int i = 0;
for (WeComShareLinkLogisticsJob j : rows) {
i++;
String rk = j.getJobKey() != null ? j.getJobKey() : "";
String st = j.getStatus() != null ? j.getStatus() : "";
String rm = j.getUserRemark() != null ? j.getUserRemark().trim() : "";
if (rm.length() > 80) {
rm = rm.substring(0, 80) + "";
}
String url = j.getTrackingUrl() != null ? j.getTrackingUrl().trim() : "";
long dup = remarkCount.getOrDefault(j.getUserRemark() != null ? j.getUserRemark().trim() : "", 0L);
sb.append(i).append(". ").append(st);
if (dup > 1) {
sb.append(" ·本批同备注").append(dup).append("");
}
sb.append("\nkey=").append(rk);
sb.append("\n备注").append(rm.isEmpty() ? "(空)" : rm);
sb.append("\n链").append(url);
if (j.getCreateTime() != null) {
sb.append("\n时").append(new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm")
.format(j.getCreateTime()));
}
sb.append("\n——————\n");
}
sb.append("删:京外物删 key\n或京外物删 后接换行写备注,最后一行写含 3.cn 的链接。");
return sb.toString();
}
/**
* 删除外部分享链物流任务行与后台物理删除一致Redis 中未消费项会因库无行而跳过)。
*/
private String textExternalShareLinkLogisticsDelete(String rest) {
if (weComShareLinkLogisticsJobService == null) {
return "「外物删」\n\n服务未就绪。";
}
if (rest == null || rest.isEmpty()) {
return "「外物删」\n\n用法\n1) 京外物删 <jobKey>(见「京外物列表」中的 key=\n"
+ "2) 京外物删\n<备注全文>\nhttps://3.cn/…";
}
String oneLine = rest.replace("\r\n", "\n").trim();
if (!oneLine.contains("\n") && looksLikeShareLinkJobKey(oneLine)) {
WeComShareLinkLogisticsJob existed = weComShareLinkLogisticsJobService.selectByJobKey(oneLine);
int n = weComShareLinkLogisticsJobService.deleteByJobKey(oneLine);
if (n <= 0) {
return "「外物删」\n\n未删除任何行jobKey 可能不存在):" + oneLine;
}
String hint = existed != null
? "\n备注" + shortenForReply(existed.getUserRemark(), 120)
+ "\n链" + nvl(existed.getTrackingUrl())
: "";
return "「外物删」\n\n已删除 " + n + " 条。" + hint;
}
ParsedRemarkAndUrl parsed = parseRemarkAnd3cnFromDeletePayload(rest);
if (parsed == null) {
return "「外物删」\n\n未能解析备注与 3.cn 链接。请多行发送:倒数第一个含 3.cn 的为链接,其上一行起为备注;"
+ "或单行:京外物删 <jobKey>";
}
int n = weComShareLinkLogisticsJobService.deleteByRemarkAndTrackingUrl(parsed.remark, parsed.trackingUrl);
if (n <= 0) {
return "「外物删」\n\n未找到完全匹配的行备注与短链需与登记一致含空格请对齐\n备注"
+ shortenForReply(parsed.remark, 200) + "\n链" + parsed.trackingUrl;
}
return "「外物删」\n\n已按备注+链接删除 " + n + " 条。";
}
private static boolean looksLikeShareLinkJobKey(String s) {
if (s == null) {
return false;
}
String t = s.trim();
return t.matches("^[a-fA-F0-9]{32}$") || t.matches("^tracebf\\d+$");
}
private static String shortenForReply(String s, int maxChars) {
if (s == null) {
return "";
}
String t = s.trim();
if (t.length() <= maxChars) {
return t;
}
return t.substring(0, maxChars) + "";
}
private static final class ParsedRemarkAndUrl {
final String remark;
final String trackingUrl;
ParsedRemarkAndUrl(String remark, String trackingUrl) {
this.remark = remark;
this.trackingUrl = trackingUrl;
}
}
private static ParsedRemarkAndUrl parseRemarkAnd3cnFromDeletePayload(String rest) {
if (rest == null) {
return null;
}
String normalized = rest.replace("\r\n", "\n").trim();
String[] lines = normalized.split("\n");
if (lines.length == 1) {
ParsedRemarkAndUrl one = tryParseRemarkAndUrlSingleLine(lines[0]);
if (one != null) {
return one;
}
}
int urlIndex = -1;
String canonicalUrl = null;
for (int i = lines.length - 1; i >= 0; i--) {
String u = extractJd3cnForInstruction(lines[i]);
if (u != null) {
urlIndex = i;
canonicalUrl = u;
break;
}
}
if (canonicalUrl == null || urlIndex < 0) {
return null;
}
StringBuilder rem = new StringBuilder();
for (int i = 0; i < urlIndex; i++) {
if (i > 0) {
rem.append('\n');
}
rem.append(lines[i].trim());
}
String remark = rem.toString().trim();
return new ParsedRemarkAndUrl(remark, canonicalUrl);
}
/** 单行「…备注… https://3.cn/…」 */
private static ParsedRemarkAndUrl tryParseRemarkAndUrlSingleLine(String line) {
if (line == null || line.isEmpty()) {
return null;
}
Matcher m = JD_3CN.matcher(line);
if (m.find()) {
String url = m.group();
String rem = line.substring(0, m.start()).trim();
return new ParsedRemarkAndUrl(rem, url);
}
Matcher m2 = Pattern.compile("http://3\\.cn/[A-Za-z0-9\\-]+").matcher(line);
if (m2.find()) {
String url = m2.group().replace("http://", "https://");
String rem = line.substring(0, m2.start()).trim();
return new ParsedRemarkAndUrl(rem, url);
}
return null;
}
private static String extractJd3cnForInstruction(String text) {
if (text == null) {
return null;
}
Matcher m = JD_3CN.matcher(text);
if (m.find()) {
return m.group();
}
Matcher m2 = Pattern.compile("http://3\\.cn/[A-Za-z0-9\\-]+").matcher(text);
if (m2.find()) {
return m2.group().replace("http://", "https://");
}
return null;
}
// ===== 工具 =====
private String jingMenu() {
return "「京粉 · 菜单」\n\n"
@@ -2138,6 +2345,8 @@ public class InstructionServiceImpl implements IInstructionService {
+ "这个月统计、上个月统计、总统计\n\n"
+ "—— 订单 ——\n"
+ "今日订单、昨日订单、七日订单\n\n"
+ "—— 外部分享链物流 ——\n"
+ "京外物列表 [关键词]、京外物删(见列表说明)\n\n"
+ "发「京」单独或「京菜单」可再次打开本列表。";
}
@@ -2148,6 +2357,7 @@ public class InstructionServiceImpl implements IInstructionService {
+ "· 京今日统计 / 京昨日统计 / 京七日统计 …\n"
+ "· 京今日订单 / 京昨日订单 / 京七日订单\n"
+ "· 慢搜 关键词、慢查 关键词(录单库模糊查询)\n"
+ "· 京外物列表 / 京外物删 — 企微 3.cn 分享链登记查询与删除\n"
+ "· 录单20250101-20250107 或 录单昨日|三日|七日(导出)\n\n"
+ "说明:转链、礼金等请使用系统内「一键转链」页面。";
}

View File

@@ -56,6 +56,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
+ "当前账号支持:\n"
+ "· 「京」开头的统计、订单类指令(可先发「京菜单」查看列表)\n"
+ "· 含 3.cn 的京东物流分享:先发链接,再发备注\n"
+ "· 京外物列表 / 京外物删 — 查询或删除外部分享链物流登记\n"
+ "· 以「单」或「开始」开头,且含「分销标记」的录单正文\n"
+ "· 以「开」或「慢开」开头且含手机号的查询\n\n"
+ "如需其他指令,请联系管理员。";

View File

@@ -47,6 +47,32 @@ public class WeComShareLinkLogisticsJobServiceImpl implements IWeComShareLinkLog
return weComShareLinkLogisticsJobMapper.selectWeComShareLinkLogisticsJobList(query);
}
@Override
public List<WeComShareLinkLogisticsJob> selectRecentForInstruction(String remarkKeyword, int days, int limit) {
int d = Math.max(1, Math.min(days, 365));
int lim = Math.max(1, Math.min(limit, 100));
String kw =
remarkKeyword != null && !remarkKeyword.trim().isEmpty() ? remarkKeyword.trim() : null;
return weComShareLinkLogisticsJobMapper.selectRecentForInstruction(kw, d, lim);
}
@Override
public int deleteByJobKey(String jobKey) {
if (jobKey == null || !StringUtils.hasText(jobKey.trim())) {
return 0;
}
return weComShareLinkLogisticsJobMapper.deleteByJobKey(jobKey.trim());
}
@Override
public int deleteByRemarkAndTrackingUrl(String remark, String trackingUrl) {
if (!StringUtils.hasText(trackingUrl)) {
return 0;
}
String r = remark != null ? remark : "";
return weComShareLinkLogisticsJobMapper.deleteByRemarkAndTrackingUrl(r, trackingUrl.trim());
}
@Override
public Map<String, Object> backfillImportedFromInboundTrace() {
int imported = 0;

View File

@@ -82,4 +82,20 @@
<delete id="deleteByJobKey">
delete from wecom_share_link_logistics_job where job_key = #{jobKey}
</delete>
<select id="selectRecentForInstruction" resultMap="WeComShareLinkLogisticsJobResult">
<include refid="selectVo"/>
where create_time >= date_sub(now(), interval #{days} day)
<if test="remarkKeyword != null and remarkKeyword != ''">
and remark like concat('%', #{remarkKeyword}, '%')
</if>
order by id desc
limit #{limit}
</select>
<delete id="deleteByRemarkAndTrackingUrl">
delete from wecom_share_link_logistics_job
where trim(remark) = trim(#{remark})
and trim(tracking_url) = trim(#{trackingUrl})
</delete>
</mapper>