@@ -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 "
+ " 说明:转链、礼金等请使用系统内「一键转链」页面。 " ;
}