Compare commits
34 Commits
ede30b5f36
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fb46cc203 | ||
|
|
7582868b2c | ||
|
|
9d03cca517 | ||
|
|
8b5abb44ee | ||
|
|
e75f71d37b | ||
|
|
5da74a155c | ||
|
|
a88600788a | ||
|
|
cf8008bdc1 | ||
|
|
d97a977a0e | ||
|
|
1f25cc5d15 | ||
|
|
66aa339906 | ||
|
|
acd693f122 | ||
|
|
751844493b | ||
|
|
256b54ffab | ||
|
|
0ff357148b | ||
|
|
7cd7440f1f | ||
|
|
f5f14c730f | ||
|
|
a7068053e1 | ||
|
|
babe687679 | ||
|
|
de335831d4 | ||
|
|
a10d561fcb | ||
|
|
656f3d28a9 | ||
|
|
e420aaeb9e | ||
|
|
01bea5005e | ||
|
|
c825c6b81a | ||
|
|
1446ea2432 | ||
|
|
52b8f13b2d | ||
|
|
94f319514e | ||
|
|
5205d8c155 | ||
|
|
fed0158444 | ||
|
|
24cf538475 | ||
|
|
c50975bce5 | ||
|
|
042068ccf1 | ||
|
|
52d0adfc85 |
@@ -17,7 +17,9 @@ import java.security.NoSuchAlgorithmException;
|
|||||||
/**
|
/**
|
||||||
* 闲管家开放平台推送回调(请在开放平台填写真实 URL)
|
* 闲管家开放平台推送回调(请在开放平台填写真实 URL)
|
||||||
* 订单:POST .../open/callback/order/receive?appid=×tamp=&sign=
|
* 订单:POST .../open/callback/order/receive?appid=×tamp=&sign=
|
||||||
* 响应须与平台「强校验」一致:一般 {@code {"code":0,"msg":"OK","data":{}}} ,勿使用 {@code result} 等非标准字段。
|
* <p>
|
||||||
|
* 成功/失败体须与《订单推送通知》OpenAPI 一致:{@code result=success|fail} + {@code msg};
|
||||||
|
* 仅当 {@code result} 为 success 时平台认为接收成功(失败最多重试 3 次;建议业务异步处理、快速返回)。
|
||||||
*/
|
*/
|
||||||
@Anonymous
|
@Anonymous
|
||||||
@RestController
|
@RestController
|
||||||
@@ -41,7 +43,7 @@ public class OpenCallbackController {
|
|||||||
String normalizedBody = normalizeJsonBody(rawBody);
|
String normalizedBody = normalizeJsonBody(rawBody);
|
||||||
IERPAccount account = erpAccountResolver.resolveStrict(appid);
|
IERPAccount account = erpAccountResolver.resolveStrict(appid);
|
||||||
if (!verifyGoofishSign(account, timestamp, sellerId, sign, normalizedBody)) {
|
if (!verifyGoofishSign(account, timestamp, sellerId, sign, normalizedBody)) {
|
||||||
return failCallback(1, "签名失败");
|
return failCallback("签名失败");
|
||||||
}
|
}
|
||||||
return successCallback();
|
return successCallback();
|
||||||
}
|
}
|
||||||
@@ -57,21 +59,21 @@ public class OpenCallbackController {
|
|||||||
String normalizedBody = normalizeJsonBody(rawBody);
|
String normalizedBody = normalizeJsonBody(rawBody);
|
||||||
IERPAccount account = erpAccountResolver.resolveStrict(appid);
|
IERPAccount account = erpAccountResolver.resolveStrict(appid);
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
return failCallback(1, "未找到启用的 AppKey 配置");
|
return failCallback("未找到启用的 AppKey 配置");
|
||||||
}
|
}
|
||||||
if (!verifyGoofishSign(account, timestamp, sellerId, sign, normalizedBody)) {
|
if (!verifyGoofishSign(account, timestamp, sellerId, sign, normalizedBody)) {
|
||||||
return failCallback(1, "签名失败");
|
return failCallback("签名失败");
|
||||||
}
|
}
|
||||||
JSONObject body;
|
JSONObject body;
|
||||||
try {
|
try {
|
||||||
body = "{}".equals(normalizedBody) ? new JSONObject() : JSON.parseObject(normalizedBody);
|
body = "{}".equals(normalizedBody) ? new JSONObject() : JSON.parseObject(normalizedBody);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return failCallback(2, "请求体不是合法JSON");
|
return failCallback("请求体不是合法JSON");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
erpGoofishOrderService.publishOrProcessNotify(appid, timestamp, body);
|
erpGoofishOrderService.publishOrProcessNotify(appid, timestamp, body);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return failCallback(3, "入队异常");
|
return failCallback("入队异常");
|
||||||
}
|
}
|
||||||
return successCallback();
|
return successCallback();
|
||||||
}
|
}
|
||||||
@@ -105,20 +107,19 @@ public class OpenCallbackController {
|
|||||||
return t.isEmpty() ? "{}" : t;
|
return t.isEmpty() ? "{}" : t;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 与平台开放接口成功响应字段类型对齐:code 为数值、msg 为字符串、data 为对象 */
|
/** 与《订单推送通知》notify_resp_ok 一致:result=success 时平台才停止重试 */
|
||||||
private static JSONObject successCallback() {
|
private static JSONObject successCallback() {
|
||||||
JSONObject ok = new JSONObject();
|
JSONObject ok = new JSONObject();
|
||||||
ok.put("code", 0);
|
ok.put("result", "success");
|
||||||
ok.put("msg", "OK");
|
ok.put("msg", "接收成功");
|
||||||
ok.put("data", new JSONObject());
|
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static JSONObject failCallback(int code, String msg) {
|
/** 与 notify_resp_fail 一致:result=fail */
|
||||||
|
private static JSONObject failCallback(String msg) {
|
||||||
JSONObject j = new JSONObject();
|
JSONObject j = new JSONObject();
|
||||||
j.put("code", code);
|
j.put("result", "fail");
|
||||||
j.put("msg", msg == null ? "fail" : msg);
|
j.put("msg", msg == null || msg.isEmpty() ? "处理失败" : msg);
|
||||||
j.put("data", new JSONObject());
|
|
||||||
return j;
|
return j;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.ruoyi.common.enums.BusinessType;
|
|||||||
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
|
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||||
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||||
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
|
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@@ -27,6 +28,17 @@ public class ErpGoofishOrderController extends BaseController {
|
|||||||
@Resource
|
@Resource
|
||||||
private JarvisGoofishProperties goofishProperties;
|
private JarvisGoofishProperties goofishProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 订单变更日志全表检索(跨订单排查),须配置在 /{id} 之前避免歧义;对应日志标记 [goofish-order-event]
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:list')")
|
||||||
|
@GetMapping("/eventLog/list")
|
||||||
|
public TableDataInfo eventLogList(ErpGoofishOrderEventLogQuery query) {
|
||||||
|
startPage();
|
||||||
|
List<ErpGoofishOrderEventLog> list = erpGoofishOrderService.selectEventLogList(query);
|
||||||
|
return getDataTable(list);
|
||||||
|
}
|
||||||
|
|
||||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:list')")
|
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:list')")
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(ErpGoofishOrder query) {
|
public TableDataInfo list(ErpGoofishOrder query) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.web.controller.jarvis;
|
package com.ruoyi.web.controller.jarvis;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -17,7 +18,9 @@ import com.ruoyi.common.core.controller.BaseController;
|
|||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
import com.ruoyi.jarvis.domain.ErpProduct;
|
import com.ruoyi.jarvis.domain.ErpProduct;
|
||||||
|
import com.ruoyi.jarvis.domain.ErpProductExportRow;
|
||||||
import com.ruoyi.jarvis.service.IErpProductService;
|
import com.ruoyi.jarvis.service.IErpProductService;
|
||||||
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||||
import com.ruoyi.common.core.page.TableDataInfo;
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
|
||||||
@@ -51,12 +54,14 @@ public class ErpProductController extends BaseController
|
|||||||
*/
|
*/
|
||||||
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:export')")
|
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:export')")
|
||||||
@Log(title = "闲鱼商品", businessType = BusinessType.EXPORT)
|
@Log(title = "闲鱼商品", businessType = BusinessType.EXPORT)
|
||||||
@GetMapping("/export")
|
@PostMapping("/export")
|
||||||
public AjaxResult export(ErpProduct erpProduct)
|
public void export(HttpServletResponse response, ErpProduct erpProduct)
|
||||||
{
|
{
|
||||||
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
|
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
|
||||||
ExcelUtil<ErpProduct> util = new ExcelUtil<ErpProduct>(ErpProduct.class);
|
String batchAt = DateUtils.getTime();
|
||||||
return util.exportExcel(list, "闲鱼商品数据");
|
List<ErpProductExportRow> rows = ErpProductExportRow.fromList(list, batchAt);
|
||||||
|
ExcelUtil<ErpProductExportRow> util = new ExcelUtil<ErpProductExportRow>(ErpProductExportRow.class);
|
||||||
|
util.exportExcel(response, rows, "闲鱼商品_AI明细");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ public class InstructionController extends BaseController {
|
|||||||
public AjaxResult execute(@RequestBody Map<String, Object> body) {
|
public AjaxResult execute(@RequestBody Map<String, Object> body) {
|
||||||
String cmd = body != null ? (body.get("command") != null ? String.valueOf(body.get("command")) : null) : null;
|
String cmd = body != null ? (body.get("command") != null ? String.valueOf(body.get("command")) : null) : null;
|
||||||
boolean forceGenerate = body != null && body.get("forceGenerate") != null && Boolean.parseBoolean(String.valueOf(body.get("forceGenerate")));
|
boolean forceGenerate = body != null && body.get("forceGenerate") != null && Boolean.parseBoolean(String.valueOf(body.get("forceGenerate")));
|
||||||
// 控制台入口,传递 isFromConsole=true,跳过订单查询校验
|
// 控制台入口:全量统计视角(排除后台标记不参与统计的联盟),非单个企微成员
|
||||||
java.util.List<String> result = instructionService.execute(cmd, forceGenerate, true);
|
java.util.List<String> result = instructionService.execute(cmd, forceGenerate, true, null);
|
||||||
return AjaxResult.success(result);
|
return AjaxResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import com.ruoyi.common.utils.http.HttpUtils;
|
|||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
import com.ruoyi.jarvis.domain.OrderRows;
|
import com.ruoyi.jarvis.domain.OrderRows;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||||
|
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||||
import com.ruoyi.jarvis.service.IOrderRowsService;
|
import com.ruoyi.jarvis.service.IOrderRowsService;
|
||||||
import com.ruoyi.jarvis.service.IGiftCouponService;
|
import com.ruoyi.jarvis.service.IGiftCouponService;
|
||||||
import com.ruoyi.jarvis.domain.GiftCoupon;
|
import com.ruoyi.jarvis.domain.GiftCoupon;
|
||||||
@@ -41,6 +42,7 @@ public class JDOrderController extends BaseController {
|
|||||||
private final IOrderRowsService orderRowsService;
|
private final IOrderRowsService orderRowsService;
|
||||||
private final IGiftCouponService giftCouponService;
|
private final IGiftCouponService giftCouponService;
|
||||||
private final ISysConfigService sysConfigService;
|
private final ISysConfigService sysConfigService;
|
||||||
|
private final ILogisticsService logisticsService;
|
||||||
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
|
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
|
||||||
private static final java.util.regex.Pattern URL_DETECT_PATTERN = java.util.regex.Pattern.compile(
|
private static final java.util.regex.Pattern URL_DETECT_PATTERN = java.util.regex.Pattern.compile(
|
||||||
"(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)",
|
"(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)",
|
||||||
@@ -53,11 +55,13 @@ public class JDOrderController extends BaseController {
|
|||||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService,
|
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService,
|
||||||
IGiftCouponService giftCouponService, ISysConfigService sysConfigService) {
|
IGiftCouponService giftCouponService, ISysConfigService sysConfigService,
|
||||||
|
ILogisticsService logisticsService) {
|
||||||
this.jdOrderService = jdOrderService;
|
this.jdOrderService = jdOrderService;
|
||||||
this.orderRowsService = orderRowsService;
|
this.orderRowsService = orderRowsService;
|
||||||
this.giftCouponService = giftCouponService;
|
this.giftCouponService = giftCouponService;
|
||||||
this.sysConfigService = sysConfigService;
|
this.sysConfigService = sysConfigService;
|
||||||
|
this.logisticsService = logisticsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private final static String skey = "2192057370ef8140c201079969c956a3";
|
private final static String skey = "2192057370ef8140c201079969c956a3";
|
||||||
@@ -68,12 +72,6 @@ public class JDOrderController extends BaseController {
|
|||||||
@Value("${jarvis.server.jarvis-java.jd-api-path:/jd}")
|
@Value("${jarvis.server.jarvis-java.jd-api-path:/jd}")
|
||||||
private String jdApiPath;
|
private String jdApiPath;
|
||||||
|
|
||||||
@Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}")
|
|
||||||
private String logisticsBaseUrl;
|
|
||||||
|
|
||||||
@Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}")
|
|
||||||
private String logisticsFetchPath;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取JD接口请求URL
|
* 获取JD接口请求URL
|
||||||
*/
|
*/
|
||||||
@@ -947,9 +945,7 @@ public class JDOrderController extends BaseController {
|
|||||||
logger.info("手动获取物流信息 - 订单ID: {}, 订单号: {}, 分销标识: {}, 物流链接: {}",
|
logger.info("手动获取物流信息 - 订单ID: {}, 订单号: {}, 分销标识: {}, 物流链接: {}",
|
||||||
orderId, order.getOrderId(), distributionMark, logisticsLink);
|
orderId, order.getOrderId(), distributionMark, logisticsLink);
|
||||||
|
|
||||||
// 构建外部接口URL
|
String externalUrl = logisticsService.buildFetchLogisticsRequestUrl(logisticsLink);
|
||||||
String externalUrl = logisticsBaseUrl + logisticsFetchPath + "?tracking_url=" +
|
|
||||||
java.net.URLEncoder.encode(logisticsLink, "UTF-8");
|
|
||||||
|
|
||||||
logger.info("准备调用外部接口 - URL: {}", externalUrl);
|
logger.info("准备调用外部接口 - URL: {}", externalUrl);
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ public class OrderRowsController extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建统计数据。
|
* 构建统计数据。
|
||||||
|
* <p>汇总口径:{@code totalCommission} 为预估佣金合计,排除取消单(validCode=3);{@code totalActualFee} 为实际佣金,仅统计已完成(validCode=17);
|
||||||
|
* {@code estimatePaidPending} 为已付款待收货(validCode=16)的预估佣金,与分组 {@code paid} 一致。</p>
|
||||||
* @param forList true=与列表同数据源(不排除 isCount=0),保证总订单数与分页一致;false=独立统计(排除 isCount=0)
|
* @param forList true=与列表同数据源(不排除 isCount=0),保证总订单数与分页一致;false=独立统计(排除 isCount=0)
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> buildStatistics(OrderRows orderRows, Date beginTime, Date endTime, boolean forList) {
|
private Map<String, Object> buildStatistics(OrderRows orderRows, Date beginTime, Date endTime, boolean forList) {
|
||||||
@@ -209,7 +211,7 @@ public class OrderRowsController extends BaseController
|
|||||||
groupStats.put("cancel", createGroupStat("取消", "cancel"));
|
groupStats.put("cancel", createGroupStat("取消", "cancel"));
|
||||||
groupStats.put("invalid", createGroupStat("无效", "invalid"));
|
groupStats.put("invalid", createGroupStat("无效", "invalid"));
|
||||||
groupStats.put("pending", createGroupStat("待付款", "pending"));
|
groupStats.put("pending", createGroupStat("待付款", "pending"));
|
||||||
groupStats.put("paid", createGroupStat("已付款", "paid"));
|
groupStats.put("paid", createGroupStat("已付款(待结算)", "paid"));
|
||||||
groupStats.put("finished", createGroupStat("已完成", "finished"));
|
groupStats.put("finished", createGroupStat("已完成", "finished"));
|
||||||
groupStats.put("deposit", createGroupStat("已付定金", "deposit"));
|
groupStats.put("deposit", createGroupStat("已付定金", "deposit"));
|
||||||
groupStats.put("illegal", createGroupStat("违规", "illegal"));
|
groupStats.put("illegal", createGroupStat("违规", "illegal"));
|
||||||
@@ -263,8 +265,14 @@ public class OrderRowsController extends BaseController
|
|||||||
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
|
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
totalCommission += commissionAmount;
|
// 顶部「预估佣金」汇总:排除取消单(validCode=3),其余状态累加单条 commissionAmount
|
||||||
totalActualFee += actualFeeAmount;
|
if (!"3".equals(validCode)) {
|
||||||
|
totalCommission += commissionAmount;
|
||||||
|
}
|
||||||
|
// 顶部「实际佣金」汇总:仅已完成(validCode=17),与联盟「已结算」口径一致
|
||||||
|
if ("17".equals(validCode)) {
|
||||||
|
totalActualFee += actualFeeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
if (validCode != null) {
|
if (validCode != null) {
|
||||||
for (Map.Entry<String, List<String>> group : groups.entrySet()) {
|
for (Map.Entry<String, List<String>> group : groups.entrySet()) {
|
||||||
@@ -290,6 +298,8 @@ public class OrderRowsController extends BaseController
|
|||||||
result.put("totalCosPrice", totalCosPrice);
|
result.put("totalCosPrice", totalCosPrice);
|
||||||
result.put("totalCommission", totalCommission);
|
result.put("totalCommission", totalCommission);
|
||||||
result.put("totalActualFee", totalActualFee);
|
result.put("totalActualFee", totalActualFee);
|
||||||
|
// 已付款待结算:与分组 paid 的预估佣金口径一致,便于独立展示卡片
|
||||||
|
result.put("estimatePaidPending", (Double) groupStats.get("paid").get("commission"));
|
||||||
result.put("totalSkuNum", totalSkuNum);
|
result.put("totalSkuNum", totalSkuNum);
|
||||||
result.put("violationOrders", violationOrders);
|
result.put("violationOrders", violationOrders);
|
||||||
result.put("violationCommission", violationCommission);
|
result.put("violationCommission", violationCommission);
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ public class TencentDocController extends BaseController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService;
|
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||||
|
|
||||||
/** 单次请求最大行数(腾讯文档 API:行数≤1000) */
|
/** 单次请求最大行数(腾讯文档 API:行数≤1000) */
|
||||||
private static final int API_MAX_ROWS_PER_REQUEST = 200;
|
private static final int API_MAX_ROWS_PER_REQUEST = 200;
|
||||||
/** 用 rowTotal 时接口实际单次只能读 200 行 */
|
/** 用 rowTotal 时接口实际单次只能读 200 行 */
|
||||||
@@ -596,7 +599,8 @@ public class TencentDocController extends BaseController {
|
|||||||
if (cell.containsKey("cellValue")) {
|
if (cell.containsKey("cellValue")) {
|
||||||
String cellText = cell.getJSONObject("cellValue").getString("text");
|
String cellText = cell.getJSONObject("cellValue").getString("text");
|
||||||
if (cellText != null) {
|
if (cellText != null) {
|
||||||
if (cellText.contains("单号")) {
|
// 「物流单号」也含「单号」,须排除,否则会误把物流列当成单号列
|
||||||
|
if (cellText.contains("单号") && !cellText.contains("物流")) {
|
||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
} else if (cellText.contains("物流")) {
|
} else if (cellText.contains("物流")) {
|
||||||
logisticsColumn = i;
|
logisticsColumn = i;
|
||||||
@@ -608,7 +612,7 @@ public class TencentDocController extends BaseController {
|
|||||||
if (orderNoColumn == -1 || logisticsColumn == -1) {
|
if (orderNoColumn == -1 || logisticsColumn == -1) {
|
||||||
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
|
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
|
||||||
"FAILED", "未找到'单号'或'物流'列");
|
"FAILED", "未找到'单号'或'物流'列");
|
||||||
return AjaxResult.error("未找到'单号'或'物流'列,请检查表头配置");
|
return AjaxResult.error("未找到「单号/客户单号/第三方单号」或「物流」列,请检查表头配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("表头解析完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn);
|
log.info("表头解析完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn);
|
||||||
@@ -896,6 +900,56 @@ public class TencentDocController extends BaseController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量同步中出现报错时,将摘要推送到企业微信(wxSend 闲鱼应用通道)
|
||||||
|
*/
|
||||||
|
private void pushTencentDocRowErrorsToWeCom(String batchId, String fileId, String sheetId,
|
||||||
|
int filledCount, int skippedCount, int errorCount,
|
||||||
|
List<Map<String, Object>> errorLogs) {
|
||||||
|
if (errorCount <= 0 && (errorLogs == null || errorLogs.isEmpty())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("【腾讯文档推送】同步存在报错\n");
|
||||||
|
if (batchId != null && !batchId.isEmpty()) {
|
||||||
|
sb.append("批次: ").append(batchId).append("\n");
|
||||||
|
}
|
||||||
|
if (fileId != null && !fileId.isEmpty()) {
|
||||||
|
sb.append("fileId: ").append(fileId).append("\n");
|
||||||
|
}
|
||||||
|
if (sheetId != null && !sheetId.isEmpty()) {
|
||||||
|
sb.append("sheetId: ").append(sheetId).append("\n");
|
||||||
|
}
|
||||||
|
sb.append(String.format("成功队列: %d, 跳过: %d, 错误: %d\n", filledCount, skippedCount, errorCount));
|
||||||
|
if (errorLogs != null && !errorLogs.isEmpty()) {
|
||||||
|
int max = Math.min(15, errorLogs.size());
|
||||||
|
for (int i = 0; i < max; i++) {
|
||||||
|
Map<String, Object> el = errorLogs.get(i);
|
||||||
|
Object on = el != null ? el.get("orderNo") : null;
|
||||||
|
Object row = el != null ? el.get("row") : null;
|
||||||
|
Object em = el != null ? el.get("errorMessage") : null;
|
||||||
|
Object et = el != null ? el.get("errorType") : null;
|
||||||
|
sb.append(String.format("%d. 单号:%s 行:%s", i + 1,
|
||||||
|
on != null ? on : "-", row != null ? row : "-"));
|
||||||
|
if (et != null && String.valueOf(et).length() > 0) {
|
||||||
|
sb.append(" ").append(et);
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
if (em != null) {
|
||||||
|
String msg = String.valueOf(em);
|
||||||
|
if (msg.length() > 120) {
|
||||||
|
msg = msg.substring(0, 119) + "…";
|
||||||
|
}
|
||||||
|
sb.append(" ").append(msg).append("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorLogs.size() > max) {
|
||||||
|
sb.append("… 共 ").append(errorLogs.size()).append(" 条错误\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "", sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 合并同一行的腾讯文档填充任务(物流更新与仅补京东单号合并为一次 batchUpdate)
|
* 合并同一行的腾讯文档填充任务(物流更新与仅补京东单号合并为一次 batchUpdate)
|
||||||
*/
|
*/
|
||||||
@@ -938,6 +992,7 @@ public class TencentDocController extends BaseController {
|
|||||||
@Anonymous
|
@Anonymous
|
||||||
@PostMapping("/fillLogisticsByOrderNo")
|
@PostMapping("/fillLogisticsByOrderNo")
|
||||||
public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
|
public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
|
||||||
|
String batchId = null;
|
||||||
try {
|
try {
|
||||||
// 直接尝试刷新token(如果失败,说明需要首次授权)
|
// 直接尝试刷新token(如果失败,说明需要首次授权)
|
||||||
String accessToken;
|
String accessToken;
|
||||||
@@ -955,7 +1010,7 @@ public class TencentDocController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从参数获取批次ID(如果是批量调用会传入)
|
// 从参数获取批次ID(如果是批量调用会传入)
|
||||||
String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
|
batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
|
||||||
|
|
||||||
// 从参数或配置中获取文档信息
|
// 从参数或配置中获取文档信息
|
||||||
String fileId = (String) params.get("fileId");
|
String fileId = (String) params.get("fileId");
|
||||||
@@ -1056,7 +1111,7 @@ public class TencentDocController extends BaseController {
|
|||||||
return AjaxResult.error("无法识别表头,表头数据为空");
|
return AjaxResult.error("无法识别表头,表头数据为空");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 列名须与表格完全一致(仅忽略首尾空白、不间断空格等):备注、是否安排、物流单号、下单电话、标记、京东下单订单号;另需「单号」或「第三方单号」
|
// 列名须与表格完全一致(仅忽略首尾空白、不间断空格等):备注、是否安排、物流单号、下单电话、标记、京东下单订单号;另需「单号」「客户单号」或「第三方单号」之一
|
||||||
log.info("开始识别表头列(完全匹配列名),共 {} 列", headerRowData.size());
|
log.info("开始识别表头列(完全匹配列名),共 {} 列", headerRowData.size());
|
||||||
for (int i = 0; i < headerRowData.size(); i++) {
|
for (int i = 0; i < headerRowData.size(); i++) {
|
||||||
String cellValue = headerRowData.getString(i);
|
String cellValue = headerRowData.getString(i);
|
||||||
@@ -1074,6 +1129,10 @@ public class TencentDocController extends BaseController {
|
|||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
}
|
}
|
||||||
|
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
|
||||||
|
orderNoColumn = i;
|
||||||
|
log.info("\u2713 列名完全匹配「客户单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
|
}
|
||||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
@@ -1107,7 +1166,7 @@ public class TencentDocController extends BaseController {
|
|||||||
|
|
||||||
// 检查必需的列是否都已识别
|
// 检查必需的列是否都已识别
|
||||||
if (orderNoColumn == null) {
|
if (orderNoColumn == null) {
|
||||||
return AjaxResult.error("无法找到列名完全为「单号」或「第三方单号」的列(请与表格列名一致,勿加空格或后缀)");
|
return AjaxResult.error("无法找到列名完全为「单号」「客户单号」或「第三方单号」的列(请与表格列名一致,勿加空格或后缀)");
|
||||||
}
|
}
|
||||||
if (logisticsLinkColumn == null) {
|
if (logisticsLinkColumn == null) {
|
||||||
return AjaxResult.error("无法找到列名完全为「物流单号」的列(兼容「物流链接」);请与表格列名一致");
|
return AjaxResult.error("无法找到列名完全为「物流单号」的列(兼容「物流链接」);请与表格列名一致");
|
||||||
@@ -1400,7 +1459,7 @@ public class TencentDocController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 根据第三方单号查询订单
|
// 根据第三方单号查询订单(与文档「单号/客户单号/第三方单号」列单元格一致)
|
||||||
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);
|
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);
|
||||||
|
|
||||||
if (order == null) {
|
if (order == null) {
|
||||||
@@ -1920,7 +1979,7 @@ public class TencentDocController extends BaseController {
|
|||||||
if (errorCount > 0 && successUpdates == 0) {
|
if (errorCount > 0 && successUpdates == 0) {
|
||||||
status = "FAILED";
|
status = "FAILED";
|
||||||
} else if (errorCount > 0) {
|
} else if (errorCount > 0) {
|
||||||
status = "PARTIAL_SUCCESS";
|
status = "PARTIAL";
|
||||||
}
|
}
|
||||||
batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount,
|
batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount,
|
||||||
message, null);
|
message, null);
|
||||||
@@ -1948,7 +2007,21 @@ public class TencentDocController extends BaseController {
|
|||||||
return AjaxResult.success("填充物流链接完成", result);
|
return AjaxResult.success("填充物流链接完成", result);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("填充物流链接失败", e);
|
log.error("填充物流链接失败", e);
|
||||||
return AjaxResult.error("填充物流链接失败: " + e.getMessage());
|
String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
|
||||||
|
if (batchId != null && !batchId.trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, errMsg);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("异常后更新批量推送记录失败 batchId={}", batchId, ex);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "",
|
||||||
|
"【腾讯文档推送】批量同步异常\n批次: " + batchId + "\n" + errMsg);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("腾讯文档推送异常企微通知失败: {}", ex.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AjaxResult.error("填充物流链接失败: " + errMsg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2093,6 +2166,10 @@ public class TencentDocController extends BaseController {
|
|||||||
String fileId = tencentDocConfig.getFileId();
|
String fileId = tencentDocConfig.getFileId();
|
||||||
String sheetId = tencentDocConfig.getSheetId();
|
String sheetId = tencentDocConfig.getSheetId();
|
||||||
|
|
||||||
|
if (fileId != null && !fileId.trim().isEmpty()) {
|
||||||
|
batchPushService.reconcileStaleRunningRecords(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
if (fileId != null && sheetId != null) {
|
if (fileId != null && sheetId != null) {
|
||||||
com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess =
|
com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess =
|
||||||
batchPushService.getLastSuccessRecord(fileId, sheetId);
|
batchPushService.getLastSuccessRecord(fileId, sheetId);
|
||||||
@@ -2239,6 +2316,10 @@ public class TencentDocController extends BaseController {
|
|||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
}
|
}
|
||||||
|
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
|
||||||
|
orderNoColumn = i;
|
||||||
|
log.info("\u2713 列名完全匹配「客户单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
|
}
|
||||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
@@ -2254,7 +2335,7 @@ public class TencentDocController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (orderNoColumn == null || logisticsLinkColumn == null) {
|
if (orderNoColumn == null || logisticsLinkColumn == null) {
|
||||||
return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」或「第三方单号」,以及「物流单号」(或「物流链接」)");
|
return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」「客户单号」或「第三方单号」,以及「物流单号」(或「物流链接」)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计结果
|
// 统计结果
|
||||||
@@ -2543,6 +2624,10 @@ public class TencentDocController extends BaseController {
|
|||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
}
|
}
|
||||||
|
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
|
||||||
|
orderNoColumn = i;
|
||||||
|
log.info("\u2713 列名完全匹配「客户单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
|
}
|
||||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
||||||
@@ -2558,7 +2643,7 @@ public class TencentDocController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (orderNoColumn == null || logisticsLinkColumn == null) {
|
if (orderNoColumn == null || logisticsLinkColumn == null) {
|
||||||
return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」或「第三方单号」,以及「物流单号」(或「物流链接」)");
|
return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」「客户单号」或「第三方单号」,以及「物流单号」(或「物流链接」)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计结果
|
// 统计结果
|
||||||
@@ -2650,11 +2735,10 @@ public class TencentDocController extends BaseController {
|
|||||||
String cleanedLogisticsLink = cleanLogisticsLink(logisticsLinkFromDoc);
|
String cleanedLogisticsLink = cleanLogisticsLink(logisticsLinkFromDoc);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 通过第三方单号查找本地订单
|
// 通过第三方单号查找本地订单;找不到再按内部单号(remark)
|
||||||
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNoFromDoc.trim());
|
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNoFromDoc.trim());
|
||||||
|
|
||||||
if (order == null) {
|
if (order == null) {
|
||||||
// 如果通过第三方单号找不到,尝试通过内部单号(remark)查找
|
|
||||||
order = jdOrderService.selectJDOrderByRemark(orderNoFromDoc.trim());
|
order = jdOrderService.selectJDOrderByRemark(orderNoFromDoc.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3080,6 +3164,14 @@ public class TencentDocController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (errorCount > 0 || (errorLogs != null && !errorLogs.isEmpty())) {
|
||||||
|
try {
|
||||||
|
pushTencentDocRowErrorsToWeCom(batchId, fileId, sheetId, filledCount, skippedCount, errorCount, errorLogs);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("腾讯文档报错企微推送失败: {}", ex.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 构建请求体
|
// 构建请求体
|
||||||
JSONObject requestBody = new JSONObject();
|
JSONObject requestBody = new JSONObject();
|
||||||
requestBody.put("title", "腾讯文档同步成功");
|
requestBody.put("title", "腾讯文档同步成功");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
@@ -12,6 +13,7 @@ import com.ruoyi.common.utils.http.HttpUtils;
|
|||||||
import com.ruoyi.framework.web.domain.Server;
|
import com.ruoyi.framework.web.domain.Server;
|
||||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||||
import com.ruoyi.jarvis.service.IWxSendService;
|
import com.ruoyi.jarvis.service.IWxSendService;
|
||||||
|
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -32,6 +34,9 @@ public class ServerController
|
|||||||
@Resource
|
@Resource
|
||||||
private IWxSendService wxSendService;
|
private IWxSendService wxSendService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||||
|
|
||||||
/** Ollama 服务地址,用于健康检查 */
|
/** Ollama 服务地址,用于健康检查 */
|
||||||
@Value("${jarvis.ollama.base-url:http://192.168.8.34:11434}")
|
@Value("${jarvis.ollama.base-url:http://192.168.8.34:11434}")
|
||||||
private String ollamaBaseUrl;
|
private String ollamaBaseUrl;
|
||||||
@@ -72,23 +77,23 @@ public class ServerController
|
|||||||
healthMap.put("logistics", logisticsMap);
|
healthMap.put("logistics", logisticsMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信推送服务健康检测
|
// 微信推送:不在此自动下发消息,仅展示配置地址;真实检测见 POST /monitor/server/health/wx-send-test
|
||||||
try {
|
Map<String, Object> wxSendMap = new HashMap<>();
|
||||||
IWxSendService.HealthCheckResult wxSendHealth = wxSendService.checkHealth();
|
wxSendMap.put("manualOnly", true);
|
||||||
Map<String, Object> wxSendMap = new HashMap<>();
|
wxSendMap.put("healthy", null);
|
||||||
wxSendMap.put("healthy", wxSendHealth.isHealthy());
|
wxSendMap.put("status", "未检测");
|
||||||
wxSendMap.put("status", wxSendHealth.getStatus());
|
wxSendMap.put("message", "点击「测试」将发送一条健康检查消息(会真实推送到微信)");
|
||||||
wxSendMap.put("message", wxSendHealth.getMessage());
|
wxSendMap.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
|
||||||
wxSendMap.put("serviceUrl", wxSendHealth.getServiceUrl());
|
healthMap.put("wxSend", wxSendMap);
|
||||||
healthMap.put("wxSend", wxSendMap);
|
|
||||||
} catch (Exception e) {
|
// 企微闲鱼通知:仅展示接口地址;真实检测见 POST /monitor/server/health/goofish-notify-test
|
||||||
Map<String, Object> wxSendMap = new HashMap<>();
|
Map<String, Object> goofishMap = new HashMap<>();
|
||||||
wxSendMap.put("healthy", false);
|
goofishMap.put("manualOnly", true);
|
||||||
wxSendMap.put("status", "异常");
|
goofishMap.put("healthy", null);
|
||||||
wxSendMap.put("message", "健康检测异常: " + e.getMessage());
|
goofishMap.put("status", "未检测");
|
||||||
wxSendMap.put("serviceUrl", "");
|
goofishMap.put("message", "点击「测试」将经 wxSend 向企微闲鱼应用发送一条测试文本");
|
||||||
healthMap.put("wxSend", wxSendMap);
|
goofishMap.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
|
||||||
}
|
healthMap.put("goofishNotify", goofishMap);
|
||||||
|
|
||||||
// Ollama 服务健康检测(调试用)
|
// Ollama 服务健康检测(调试用)
|
||||||
try {
|
try {
|
||||||
@@ -115,6 +120,54 @@ public class ServerController
|
|||||||
|
|
||||||
return AjaxResult.success(healthMap);
|
return AjaxResult.success(healthMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动测试微信推送(会真实下发一条消息)
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||||
|
@PostMapping("/health/wx-send-test")
|
||||||
|
public AjaxResult testWxSendHealth() {
|
||||||
|
try {
|
||||||
|
IWxSendService.HealthCheckResult r = wxSendService.checkHealth();
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("manualOnly", true);
|
||||||
|
m.put("healthy", r.isHealthy());
|
||||||
|
m.put("status", r.getStatus());
|
||||||
|
m.put("message", r.getMessage());
|
||||||
|
m.put("serviceUrl", r.getServiceUrl());
|
||||||
|
return AjaxResult.success(m);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("manualOnly", true);
|
||||||
|
m.put("healthy", false);
|
||||||
|
m.put("status", "异常");
|
||||||
|
m.put("message", "检测异常: " + e.getMessage());
|
||||||
|
m.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
|
||||||
|
return AjaxResult.success(m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动测试企微闲鱼通知(经 wxSend POST /wx/send/goofish,与 /send/pdd 相同 vanToken + title/text/touser)
|
||||||
|
*/
|
||||||
|
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||||
|
@PostMapping("/health/goofish-notify-test")
|
||||||
|
public AjaxResult testGoofishNotify() {
|
||||||
|
String err = wxSendGoofishNotifyClient.testGoofishNotify();
|
||||||
|
Map<String, Object> m = new HashMap<>();
|
||||||
|
m.put("manualOnly", true);
|
||||||
|
m.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
|
||||||
|
if (err == null) {
|
||||||
|
m.put("healthy", true);
|
||||||
|
m.put("status", "正常");
|
||||||
|
m.put("message", "闲鱼通知测试消息已发送");
|
||||||
|
} else {
|
||||||
|
m.put("healthy", false);
|
||||||
|
m.put("status", "异常");
|
||||||
|
m.put("message", err);
|
||||||
|
}
|
||||||
|
return AjaxResult.success(m);
|
||||||
|
}
|
||||||
|
|
||||||
private void putOllamaUnhealthy(Map<String, Object> healthMap, String url, String message) {
|
private void putOllamaUnhealthy(Map<String, Object> healthMap, String url, String message) {
|
||||||
Map<String, Object> ollamaMap = new HashMap<>();
|
Map<String, Object> ollamaMap = new HashMap<>();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
|||||||
import com.ruoyi.common.enums.BusinessType;
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
import com.ruoyi.jarvis.domain.dto.JDOrderSimpleDTO;
|
import com.ruoyi.jarvis.domain.dto.JDOrderSimpleDTO;
|
||||||
|
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderProfitService;
|
import com.ruoyi.jarvis.service.IJDOrderProfitService;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||||
import com.ruoyi.jarvis.service.IInstructionService;
|
import com.ruoyi.jarvis.service.IInstructionService;
|
||||||
@@ -157,6 +158,15 @@ public class JDOrderListController extends BaseController
|
|||||||
return dataTable;
|
return dataTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷录单页:型号下拉数据;每型号取 jd_order 主键最大的一条的付款与后返(通常即最近落库单)
|
||||||
|
*/
|
||||||
|
@GetMapping("/quickRecord/modelOptions")
|
||||||
|
public AjaxResult quickRecordModelOptions() {
|
||||||
|
List<QuickRecordModelOption> options = jdOrderService.selectQuickRecordModelOptions();
|
||||||
|
return AjaxResult.success(options);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导入跟团返现类 Excel:按「单号/订单号」匹配系统订单,将「是否返现」「总共返现」等写入后返备注(可多次导入累加);文件落盘并记上传记录。
|
* 导入跟团返现类 Excel:按「单号/订单号」匹配系统订单,将「是否返现」「总共返现」等写入后返备注(可多次导入累加);文件落盘并记上传记录。
|
||||||
*/
|
*/
|
||||||
@@ -512,6 +522,9 @@ public class JDOrderListController extends BaseController
|
|||||||
if (query.getModelNumber() != null && !query.getModelNumber().trim().isEmpty()) {
|
if (query.getModelNumber() != null && !query.getModelNumber().trim().isEmpty()) {
|
||||||
query.setModelNumber(query.getModelNumber().trim());
|
query.setModelNumber(query.getModelNumber().trim());
|
||||||
}
|
}
|
||||||
|
if (query.getModelNumberExclude() != null && !query.getModelNumberExclude().trim().isEmpty()) {
|
||||||
|
query.setModelNumberExclude(query.getModelNumberExclude().trim());
|
||||||
|
}
|
||||||
if (query.getBuyer() != null && !query.getBuyer().trim().isEmpty()) {
|
if (query.getBuyer() != null && !query.getBuyer().trim().isEmpty()) {
|
||||||
query.setBuyer(query.getBuyer().trim());
|
query.setBuyer(query.getBuyer().trim());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,10 +200,18 @@ jarvis:
|
|||||||
# 物流接口服务地址
|
# 物流接口服务地址
|
||||||
logistics:
|
logistics:
|
||||||
base-url: http://192.168.8.88:5001
|
base-url: http://192.168.8.88:5001
|
||||||
|
# 同机多进程多端口时配置逗号分隔列表;非空时仅按下列地址轮询,不再使用 base-url
|
||||||
|
base-urls: http://192.168.8.88:5001,http://192.168.8.88:5002,http://192.168.8.88:5003
|
||||||
fetch-path: /fetch_logistics
|
fetch-path: /fetch_logistics
|
||||||
health-path: /health
|
health-path: /health
|
||||||
# 每次定时任务最多处理多少条企微分享链待队列(RPUSH 入队、LPOP 出队)
|
# 每次定时任务最多处理多少条企微分享链待队列(RPUSH 入队、LPOP 出队)
|
||||||
adhoc-pending-batch-size: 50
|
adhoc-pending-batch-size: 50
|
||||||
|
# 物流扫描(LogisticsScanTask):轮询 JD 单拉运单 + drain 分享链队列
|
||||||
|
scan:
|
||||||
|
cron: "0 */20 * * * ?"
|
||||||
|
order-delay-ms: 250
|
||||||
|
# 0=不限制;例如 40 可控制单轮最长耗时(余下下轮再扫)
|
||||||
|
max-orders-per-round: 0
|
||||||
# 获取评论接口服务地址(后端转发,避免前端跨域)
|
# 获取评论接口服务地址(后端转发,避免前端跨域)
|
||||||
fetch-comments:
|
fetch-comments:
|
||||||
base-url: http://192.168.8.60:5008
|
base-url: http://192.168.8.60:5008
|
||||||
@@ -214,15 +222,15 @@ jarvis:
|
|||||||
wxsend-base-url: http://127.0.0.1:36699
|
wxsend-base-url: http://127.0.0.1:36699
|
||||||
# 须与 wxSend jarvis.wecom.push-secret 一致(Header X-WxSend-WeCom-Push-Secret)
|
# 须与 wxSend jarvis.wecom.push-secret 一致(Header X-WxSend-WeCom-Push-Secret)
|
||||||
push-secret: jarvis_wecom_push_change_me
|
push-secret: jarvis_wecom_push_change_me
|
||||||
# 闲鱼订单事件 → wxSend POST /wecom/goofish-active-push(须与 wxSend goofish-push-secret 一致)
|
# 与 /wx/send/pdd、/wx/send/goofish 请求头 vanToken 一致(wxSend TokenUtil)
|
||||||
goofish-push-secret: jarvis_wecom_goofish_push_change_me
|
wxsend-van-token: super_token_b62190c26
|
||||||
# 接收企微通知的成员 UserID,多个逗号或 |;留空则不推送
|
# 接收企微通知的成员 UserID,多个逗号或 |;留空则不推送
|
||||||
goofish-notify-touser: "LinPinFan"
|
goofish-notify-touser: "LinPinFan"
|
||||||
# 多轮会话:与 JDUtil interaction_state 类似,TTL 与空闲超时(分钟)
|
# 多轮会话:与 JDUtil interaction_state 类似,TTL 与空闲超时(分钟)
|
||||||
session-ttl-minutes: 30
|
session-ttl-minutes: 30
|
||||||
session-idle-timeout-minutes: 30
|
session-idle-timeout-minutes: 30
|
||||||
session-sweep-ms: 60000
|
session-sweep-ms: 60000
|
||||||
# 企微「开」+ 手机号:Jarvis POST 该局域网接口,将响应中的 reply_text 被动回复给用户
|
# 企微「开」/「慢开」+ 手机号:POST body 含 text(手机号)与 bot;响应 reply_text 被动回复用户
|
||||||
phone-forward:
|
phone-forward:
|
||||||
enabled: true
|
enabled: true
|
||||||
base-url: http://192.168.8.60:18080
|
base-url: http://192.168.8.60:18080
|
||||||
@@ -231,7 +239,12 @@ jarvis:
|
|||||||
# wait_reply 时服务端会等多条 Bot 回复,宜适当加大
|
# wait_reply 时服务端会等多条 Bot 回复,宜适当加大
|
||||||
read-timeout-ms: 120000
|
read-timeout-ms: 120000
|
||||||
wait-reply: true
|
wait-reply: true
|
||||||
reply-take-nth: 2
|
# 多台企微线程同时触发时串行调用 tg_bridge;排队超过该毫秒则提示「正忙」(0 表示一直等到上一条结束)
|
||||||
|
lock-acquire-timeout-ms: 180000
|
||||||
|
# 连续失败后熔断,不再发起 HTTP(与 tg_bridge 侧熔断互不替代)
|
||||||
|
circuit-failure-threshold: 5
|
||||||
|
circuit-open-ms: 120000
|
||||||
|
# reply_take_nth:仅「开」用 2;「慢开」由 tg_bridge reply_adaptive_skip_middle_ad 在 2/3 条间自适应
|
||||||
# Ollama 大模型服务(监控健康度调试用)
|
# Ollama 大模型服务(监控健康度调试用)
|
||||||
ollama:
|
ollama:
|
||||||
base-url: http://192.168.8.34:11434
|
base-url: http://192.168.8.34:11434
|
||||||
@@ -270,7 +283,7 @@ jarvis:
|
|||||||
mq-topic: jarvis-goofish-erp-order
|
mq-topic: jarvis-goofish-erp-order
|
||||||
consumer-group: jarvis-goofish-order-consumer
|
consumer-group: jarvis-goofish-order-consumer
|
||||||
pull-lookback-hours: 72
|
pull-lookback-hours: 72
|
||||||
pull-cron: "0 0/15 * * * ?"
|
pull-cron: "0 * * * * ?"
|
||||||
auto-ship-cron: "0 2/10 * * * ?"
|
auto-ship-cron: "0 2/10 * * * ?"
|
||||||
# 订单列表:每页条数(最大 100)
|
# 订单列表:每页条数(最大 100)
|
||||||
pull-page-size: 100
|
pull-page-size: 100
|
||||||
@@ -282,6 +295,8 @@ jarvis:
|
|||||||
pull-max-update-time-range-seconds: 15552000
|
pull-max-update-time-range-seconds: 15552000
|
||||||
# 全量拉单起点:距今多少天(默认约 3 年)
|
# 全量拉单起点:距今多少天(默认约 3 年)
|
||||||
pull-full-history-days: 1095
|
pull-full-history-days: 1095
|
||||||
|
# true=仅拉 auto-ship-order-statuses(省调用,其它状态依赖推送);false=时间窗内全状态(推荐,与本地对齐)
|
||||||
|
pull-list-only-auto-ship-statuses: false
|
||||||
auto-ship-batch-size: 20
|
auto-ship-batch-size: 20
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -200,18 +200,24 @@ jarvis:
|
|||||||
# 物流接口服务地址
|
# 物流接口服务地址
|
||||||
logistics:
|
logistics:
|
||||||
base-url: http://127.0.0.1:5001
|
base-url: http://127.0.0.1:5001
|
||||||
|
# 同机多进程多端口时配置逗号分隔列表;非空时仅按下列地址轮询,不再使用 base-url
|
||||||
|
base-urls: http://127.0.0.1:5001,http://127.0.0.1:5002,http://127.0.0.1:5003
|
||||||
fetch-path: /fetch_logistics
|
fetch-path: /fetch_logistics
|
||||||
health-path: /health
|
health-path: /health
|
||||||
adhoc-pending-batch-size: 50
|
adhoc-pending-batch-size: 50
|
||||||
|
scan:
|
||||||
|
cron: "0 */20 * * * ?"
|
||||||
|
order-delay-ms: 250
|
||||||
|
max-orders-per-round: 0
|
||||||
# 获取评论接口服务地址(后端转发)
|
# 获取评论接口服务地址(后端转发)
|
||||||
fetch-comments:
|
fetch-comments:
|
||||||
base-url: http://192.168.8.60:5008
|
base-url: http://192.168.8.60:5008
|
||||||
wecom:
|
wecom:
|
||||||
inbound-secret: jarvis_wecom_bridge_change_me
|
inbound-secret: jarvis_wecom_bridge_change_me
|
||||||
wxsend-base-url: http://127.0.0.1:36699
|
wxsend-base-url: https://wxts.van333.cn
|
||||||
push-secret: jarvis_wecom_push_change_me
|
push-secret: jarvis_wecom_push_change_me
|
||||||
goofish-push-secret: jarvis_wecom_goofish_push_change_me
|
wxsend-van-token: super_token_b62190c26
|
||||||
goofish-notify-touser: "LinPinFan"
|
goofish-notify-touser: "LinPingFan"
|
||||||
session-ttl-minutes: 30
|
session-ttl-minutes: 30
|
||||||
session-idle-timeout-minutes: 30
|
session-idle-timeout-minutes: 30
|
||||||
session-sweep-ms: 60000
|
session-sweep-ms: 60000
|
||||||
@@ -222,7 +228,10 @@ jarvis:
|
|||||||
connect-timeout-ms: 8000
|
connect-timeout-ms: 8000
|
||||||
read-timeout-ms: 120000
|
read-timeout-ms: 120000
|
||||||
wait-reply: true
|
wait-reply: true
|
||||||
reply-take-nth: 2
|
lock-acquire-timeout-ms: 180000
|
||||||
|
circuit-failure-threshold: 5
|
||||||
|
circuit-open-ms: 120000
|
||||||
|
# 「开」取第 2 条;「慢开」由桥接自适应第 2/3 条
|
||||||
# Ollama 大模型服务(监控健康度调试用)
|
# Ollama 大模型服务(监控健康度调试用)
|
||||||
ollama:
|
ollama:
|
||||||
base-url: http://192.168.8.34:11434
|
base-url: http://192.168.8.34:11434
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
-- 说明:菜单需在「系统管理-菜单管理」中自行新增,组件路径示例:
|
-- 说明:菜单需在「系统管理-菜单管理」中自行新增,组件路径示例:
|
||||||
-- 配置中心 system/goofish/erpOpenConfig/index
|
-- 配置中心 system/goofish/erpOpenConfig/index
|
||||||
-- 订单跟踪 system/goofish/erpGoofishOrder/index
|
-- 订单跟踪 system/goofish/erpGoofishOrder/index
|
||||||
|
-- 变更日志(跨单排查,对接 GET /jarvis/erpGoofishOrder/eventLog/list)system/goofish/erpGoofishEventLog/index
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS erp_open_config (
|
CREATE TABLE IF NOT EXISTS erp_open_config (
|
||||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||||
@@ -87,6 +88,8 @@ CREATE TABLE IF NOT EXISTS erp_goofish_order_event_log (
|
|||||||
-- 父菜单:系统管理下新增目录「闲管家ERP」
|
-- 父菜单:系统管理下新增目录「闲管家ERP」
|
||||||
-- 子菜单1:组件 system/goofish/erpOpenConfig/index 权限前缀 jarvis:erpOpenConfig
|
-- 子菜单1:组件 system/goofish/erpOpenConfig/index 权限前缀 jarvis:erpOpenConfig
|
||||||
-- 子菜单2:组件 system/goofish/erpGoofishOrder/index 权限前缀 jarvis:erpGoofishOrder
|
-- 子菜单2:组件 system/goofish/erpGoofishOrder/index 权限前缀 jarvis:erpGoofishOrder
|
||||||
|
-- 子菜单3:组件 system/goofish/erpGoofishEventLog/index 权限沿用 jarvis:erpGoofishOrder:list 即可(仅列表查询)
|
||||||
|
-- 路由地址建议与订单页同级,如订单为 …/erpGoofishOrder 则本页 …/erpGoofishEventLog(订单页「变更日志排查」按钮依赖此规则)
|
||||||
-- 按钮权限示例:
|
-- 按钮权限示例:
|
||||||
-- jarvis:erpOpenConfig:list,query,add,edit,remove
|
-- jarvis:erpOpenConfig:list,query,add,edit,remove
|
||||||
-- jarvis:erpGoofishOrder:list,query,edit
|
-- jarvis:erpGoofishOrder:list,query,edit
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ public class GoofishAsyncConfig {
|
|||||||
@Bean("goofishTaskExecutor")
|
@Bean("goofishTaskExecutor")
|
||||||
public Executor goofishTaskExecutor() {
|
public Executor goofishTaskExecutor() {
|
||||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
executor.setCorePoolSize(2);
|
// 仅用于未配置 RocketMQ 时 HTTP 回调路径的 @Async;过小易在拉单/回调并发时排队拖慢企微通知
|
||||||
executor.setMaxPoolSize(8);
|
executor.setCorePoolSize(4);
|
||||||
|
executor.setMaxPoolSize(16);
|
||||||
executor.setQueueCapacity(500);
|
executor.setQueueCapacity(500);
|
||||||
executor.setThreadNamePrefix("goofish-");
|
executor.setThreadNamePrefix("goofish-");
|
||||||
executor.initialize();
|
executor.initialize();
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class JarvisGoofishProperties {
|
|||||||
private int pullLookbackHours = 72;
|
private int pullLookbackHours = 72;
|
||||||
|
|
||||||
/** 拉单定时 cron */
|
/** 拉单定时 cron */
|
||||||
private String pullCron = "0 0/15 * * * ?";
|
private String pullCron = "0 * * * * ?";
|
||||||
|
|
||||||
/** 同步运单 + 自动发货 cron */
|
/** 同步运单 + 自动发货 cron */
|
||||||
private String autoShipCron = "0 2/10 * * * ?";
|
private String autoShipCron = "0 2/10 * * * ?";
|
||||||
@@ -58,6 +58,12 @@ public class JarvisGoofishProperties {
|
|||||||
*/
|
*/
|
||||||
private String autoShipOrderStatuses = "12";
|
private String autoShipOrderStatuses = "12";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为 true 时定时/增量列表拉单仅按 {@link #autoShipOrderStatuses} 过滤(减少调用量,但会漏掉其它状态)。
|
||||||
|
* 默认 false:按时间窗拉全状态,与本地 upsert 对齐,推送仍可用于更低延迟。
|
||||||
|
*/
|
||||||
|
private boolean pullListOnlyAutoShipStatuses = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 未在 erp_open_config 配置 express_code 时,自动发货使用的默认快递公司编码(官方列表中日日顺多为 rrs)。
|
* 未在 erp_open_config 配置 express_code 时,自动发货使用的默认快递公司编码(官方列表中日日顺多为 rrs)。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -64,4 +64,14 @@ public class ErpGoofishOrder {
|
|||||||
private String jdRemark;
|
private String jdRemark;
|
||||||
/** 联查:本地京东单收件地址 jd_order.address(闲鱼详情常不返回明文地址) */
|
/** 联查:本地京东单收件地址 jd_order.address(闲鱼详情常不返回明文地址) */
|
||||||
private String jdAddress;
|
private String jdAddress;
|
||||||
|
|
||||||
|
// --------- 以下为列表查询扩展条件(不参与 insert/update) ---------
|
||||||
|
/** 运单关键字:命中详情运单号或本地运单号(模糊) */
|
||||||
|
private String waybillKeyword;
|
||||||
|
/** 开放平台 modify_time 下限(Unix 秒,含边界) */
|
||||||
|
private Long modifyTimeBegin;
|
||||||
|
/** 开放平台 modify_time 上限(Unix 秒,含边界) */
|
||||||
|
private Long modifyTimeEnd;
|
||||||
|
/** 是否已关联京东单:1 已关联 jd_order_id 非空;0 未关联;null 不限 */
|
||||||
|
private Integer jdLinkFilter;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public class ErpGoofishOrderEventLog {
|
|||||||
private String orderNo;
|
private String orderNo;
|
||||||
/** ORDER_SYNC / LOGISTICS_SYNC / SHIP */
|
/** ORDER_SYNC / LOGISTICS_SYNC / SHIP */
|
||||||
private String eventType;
|
private String eventType;
|
||||||
/** NOTIFY、LIST、DETAIL_REFRESH、UPSERT、REDIS_WAYBILL、AUTO_SHIP 等 */
|
/** NOTIFY、LIST、DETAIL_REFRESH、JD_LOGISTICS_PUSH、REDIS_WAYBILL、AUTO_SHIP 等 */
|
||||||
private String source;
|
private String source;
|
||||||
private String message;
|
private String message;
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ruoyi.jarvis.domain;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 闲管家订单变更日志(全表检索,用于排查)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ErpGoofishOrderEventLogQuery extends BaseEntity {
|
||||||
|
|
||||||
|
private Long orderId;
|
||||||
|
private String appKey;
|
||||||
|
private String orderNo;
|
||||||
|
private String eventType;
|
||||||
|
private String source;
|
||||||
|
/** 模糊匹配 message */
|
||||||
|
private String messageKeyword;
|
||||||
|
}
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
package com.ruoyi.jarvis.domain;
|
||||||
|
|
||||||
|
import com.ruoyi.common.annotation.Excel;
|
||||||
|
import com.ruoyi.common.utils.DateUtils;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 闲鱼商品导出专用行(字段偏多,便于给 AI / 离线分析使用)
|
||||||
|
*/
|
||||||
|
public class ErpProductExportRow implements Serializable
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@Excel(name = "导出批次时间", width = 22, sort = 1)
|
||||||
|
private String exportBatchAt;
|
||||||
|
|
||||||
|
@Excel(name = "本表主键ID", sort = 2)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Excel(name = "管家商品ID", sort = 3)
|
||||||
|
private Long productId;
|
||||||
|
|
||||||
|
@Excel(name = "管家商品ID文本", width = 22, sort = 4)
|
||||||
|
private String productIdText;
|
||||||
|
|
||||||
|
@Excel(name = "商品标题", width = 45, sort = 5)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Excel(name = "主图URL", width = 55, sort = 6)
|
||||||
|
private String mainImage;
|
||||||
|
|
||||||
|
@Excel(name = "价格_分_原始整数", sort = 7)
|
||||||
|
private Long priceFen;
|
||||||
|
|
||||||
|
@Excel(name = "价格_元_可读", sort = 8)
|
||||||
|
private String priceYuan;
|
||||||
|
|
||||||
|
@Excel(name = "库存", sort = 9)
|
||||||
|
private Integer stock;
|
||||||
|
|
||||||
|
@Excel(name = "商品状态_码", sort = 10)
|
||||||
|
private Integer productStatusCode;
|
||||||
|
|
||||||
|
@Excel(name = "商品状态_说明", width = 14, sort = 11)
|
||||||
|
private String productStatusLabel;
|
||||||
|
|
||||||
|
@Excel(name = "销售状态_码", sort = 12)
|
||||||
|
private Integer saleStatusCode;
|
||||||
|
|
||||||
|
@Excel(name = "闲鱼会员名", width = 18, sort = 13)
|
||||||
|
private String userName;
|
||||||
|
|
||||||
|
@Excel(name = "ERP应用appid", width = 22, sort = 14)
|
||||||
|
private String appid;
|
||||||
|
|
||||||
|
@Excel(name = "商品链接", width = 55, sort = 15)
|
||||||
|
private String productUrl;
|
||||||
|
|
||||||
|
@Excel(name = "上架时间_unix秒", width = 18, sort = 16)
|
||||||
|
private Long onlineTimeUnix;
|
||||||
|
|
||||||
|
@Excel(name = "上架时间_可读", width = 22, sort = 17)
|
||||||
|
private String onlineTimeReadable;
|
||||||
|
|
||||||
|
@Excel(name = "下架时间_unix秒", width = 18, sort = 18)
|
||||||
|
private Long offlineTimeUnix;
|
||||||
|
|
||||||
|
@Excel(name = "下架时间_可读", width = 22, sort = 19)
|
||||||
|
private String offlineTimeReadable;
|
||||||
|
|
||||||
|
@Excel(name = "售出时间_unix秒", width = 18, sort = 20)
|
||||||
|
private Long soldTimeUnix;
|
||||||
|
|
||||||
|
@Excel(name = "售出时间_可读", width = 22, sort = 21)
|
||||||
|
private String soldTimeReadable;
|
||||||
|
|
||||||
|
@Excel(name = "闲鱼创建_unix秒", width = 18, sort = 22)
|
||||||
|
private Long createTimeXyUnix;
|
||||||
|
|
||||||
|
@Excel(name = "闲鱼创建_可读", width = 22, sort = 23)
|
||||||
|
private String createTimeXyReadable;
|
||||||
|
|
||||||
|
@Excel(name = "闲鱼更新_unix秒", width = 18, sort = 24)
|
||||||
|
private Long updateTimeXyUnix;
|
||||||
|
|
||||||
|
@Excel(name = "闲鱼更新_可读", width = 22, sort = 25)
|
||||||
|
private String updateTimeXyReadable;
|
||||||
|
|
||||||
|
@Excel(name = "备注_本表", width = 30, sort = 26)
|
||||||
|
private String remark;
|
||||||
|
|
||||||
|
@Excel(name = "库创建时间", width = 22, dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 27)
|
||||||
|
private Date dbCreateTime;
|
||||||
|
|
||||||
|
@Excel(name = "库更新时间", width = 22, dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 28)
|
||||||
|
private Date dbUpdateTime;
|
||||||
|
|
||||||
|
public static List<ErpProductExportRow> fromList(List<ErpProduct> list, String exportBatchAt)
|
||||||
|
{
|
||||||
|
List<ErpProductExportRow> rows = new ArrayList<>(list.size());
|
||||||
|
for (ErpProduct p : list)
|
||||||
|
{
|
||||||
|
rows.add(from(p, exportBatchAt));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ErpProductExportRow from(ErpProduct p, String exportBatchAt)
|
||||||
|
{
|
||||||
|
ErpProductExportRow r = new ErpProductExportRow();
|
||||||
|
r.setExportBatchAt(exportBatchAt != null ? exportBatchAt : "");
|
||||||
|
if (p == null)
|
||||||
|
{
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
r.setId(p.getId());
|
||||||
|
r.setProductId(p.getProductId());
|
||||||
|
r.setProductIdText(p.getProductId() != null ? String.valueOf(p.getProductId()) : "");
|
||||||
|
r.setTitle(p.getTitle());
|
||||||
|
r.setMainImage(p.getMainImage());
|
||||||
|
r.setPriceFen(p.getPrice());
|
||||||
|
if (p.getPrice() != null)
|
||||||
|
{
|
||||||
|
r.setPriceYuan(BigDecimal.valueOf(p.getPrice()).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).toPlainString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
r.setPriceYuan("");
|
||||||
|
}
|
||||||
|
r.setStock(p.getStock());
|
||||||
|
r.setProductStatusCode(p.getProductStatus());
|
||||||
|
r.setProductStatusLabel(productStatusLabel(p.getProductStatus()));
|
||||||
|
r.setSaleStatusCode(p.getSaleStatus());
|
||||||
|
r.setUserName(p.getUserName());
|
||||||
|
r.setAppid(p.getAppid());
|
||||||
|
r.setProductUrl(p.getProductUrl());
|
||||||
|
|
||||||
|
r.setOnlineTimeUnix(p.getOnlineTime());
|
||||||
|
r.setOnlineTimeReadable(formatUnixSeconds(p.getOnlineTime()));
|
||||||
|
r.setOfflineTimeUnix(p.getOfflineTime());
|
||||||
|
r.setOfflineTimeReadable(formatUnixSeconds(p.getOfflineTime()));
|
||||||
|
r.setSoldTimeUnix(p.getSoldTime());
|
||||||
|
r.setSoldTimeReadable(formatUnixSeconds(p.getSoldTime()));
|
||||||
|
r.setCreateTimeXyUnix(p.getCreateTimeXy());
|
||||||
|
r.setCreateTimeXyReadable(formatUnixSeconds(p.getCreateTimeXy()));
|
||||||
|
r.setUpdateTimeXyUnix(p.getUpdateTimeXy());
|
||||||
|
r.setUpdateTimeXyReadable(formatUnixSeconds(p.getUpdateTimeXy()));
|
||||||
|
|
||||||
|
r.setRemark(p.getRemark());
|
||||||
|
r.setDbCreateTime(p.getCreateTime());
|
||||||
|
r.setDbUpdateTime(p.getUpdateTime());
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatUnixSeconds(Long unixSeconds)
|
||||||
|
{
|
||||||
|
if (unixSeconds == null || unixSeconds <= 0)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date(unixSeconds * 1000L));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String productStatusLabel(Integer status)
|
||||||
|
{
|
||||||
|
if (status == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case -1:
|
||||||
|
return "删除";
|
||||||
|
case 10:
|
||||||
|
return "其它(10)";
|
||||||
|
case 21:
|
||||||
|
return "待发布";
|
||||||
|
case 22:
|
||||||
|
return "销售中";
|
||||||
|
case 23:
|
||||||
|
return "已售罄";
|
||||||
|
case 31:
|
||||||
|
return "手动下架";
|
||||||
|
case 33:
|
||||||
|
return "售出下架";
|
||||||
|
case 36:
|
||||||
|
return "自动下架";
|
||||||
|
default:
|
||||||
|
return "未知(" + status + ")";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getExportBatchAt()
|
||||||
|
{
|
||||||
|
return exportBatchAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExportBatchAt(String exportBatchAt)
|
||||||
|
{
|
||||||
|
this.exportBatchAt = exportBatchAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId()
|
||||||
|
{
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id)
|
||||||
|
{
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getProductId()
|
||||||
|
{
|
||||||
|
return productId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductId(Long productId)
|
||||||
|
{
|
||||||
|
this.productId = productId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductIdText()
|
||||||
|
{
|
||||||
|
return productIdText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductIdText(String productIdText)
|
||||||
|
{
|
||||||
|
this.productIdText = productIdText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTitle()
|
||||||
|
{
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(String title)
|
||||||
|
{
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMainImage()
|
||||||
|
{
|
||||||
|
return mainImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMainImage(String mainImage)
|
||||||
|
{
|
||||||
|
this.mainImage = mainImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getPriceFen()
|
||||||
|
{
|
||||||
|
return priceFen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriceFen(Long priceFen)
|
||||||
|
{
|
||||||
|
this.priceFen = priceFen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPriceYuan()
|
||||||
|
{
|
||||||
|
return priceYuan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPriceYuan(String priceYuan)
|
||||||
|
{
|
||||||
|
this.priceYuan = priceYuan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getStock()
|
||||||
|
{
|
||||||
|
return stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setStock(Integer stock)
|
||||||
|
{
|
||||||
|
this.stock = stock;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getProductStatusCode()
|
||||||
|
{
|
||||||
|
return productStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductStatusCode(Integer productStatusCode)
|
||||||
|
{
|
||||||
|
this.productStatusCode = productStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductStatusLabel()
|
||||||
|
{
|
||||||
|
return productStatusLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductStatusLabel(String productStatusLabel)
|
||||||
|
{
|
||||||
|
this.productStatusLabel = productStatusLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getSaleStatusCode()
|
||||||
|
{
|
||||||
|
return saleStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSaleStatusCode(Integer saleStatusCode)
|
||||||
|
{
|
||||||
|
this.saleStatusCode = saleStatusCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserName()
|
||||||
|
{
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserName(String userName)
|
||||||
|
{
|
||||||
|
this.userName = userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAppid()
|
||||||
|
{
|
||||||
|
return appid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAppid(String appid)
|
||||||
|
{
|
||||||
|
this.appid = appid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProductUrl()
|
||||||
|
{
|
||||||
|
return productUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setProductUrl(String productUrl)
|
||||||
|
{
|
||||||
|
this.productUrl = productUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOnlineTimeUnix()
|
||||||
|
{
|
||||||
|
return onlineTimeUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlineTimeUnix(Long onlineTimeUnix)
|
||||||
|
{
|
||||||
|
this.onlineTimeUnix = onlineTimeUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOnlineTimeReadable()
|
||||||
|
{
|
||||||
|
return onlineTimeReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOnlineTimeReadable(String onlineTimeReadable)
|
||||||
|
{
|
||||||
|
this.onlineTimeReadable = onlineTimeReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getOfflineTimeUnix()
|
||||||
|
{
|
||||||
|
return offlineTimeUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineTimeUnix(Long offlineTimeUnix)
|
||||||
|
{
|
||||||
|
this.offlineTimeUnix = offlineTimeUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getOfflineTimeReadable()
|
||||||
|
{
|
||||||
|
return offlineTimeReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOfflineTimeReadable(String offlineTimeReadable)
|
||||||
|
{
|
||||||
|
this.offlineTimeReadable = offlineTimeReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSoldTimeUnix()
|
||||||
|
{
|
||||||
|
return soldTimeUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoldTimeUnix(Long soldTimeUnix)
|
||||||
|
{
|
||||||
|
this.soldTimeUnix = soldTimeUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSoldTimeReadable()
|
||||||
|
{
|
||||||
|
return soldTimeReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSoldTimeReadable(String soldTimeReadable)
|
||||||
|
{
|
||||||
|
this.soldTimeReadable = soldTimeReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCreateTimeXyUnix()
|
||||||
|
{
|
||||||
|
return createTimeXyUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateTimeXyUnix(Long createTimeXyUnix)
|
||||||
|
{
|
||||||
|
this.createTimeXyUnix = createTimeXyUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCreateTimeXyReadable()
|
||||||
|
{
|
||||||
|
return createTimeXyReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreateTimeXyReadable(String createTimeXyReadable)
|
||||||
|
{
|
||||||
|
this.createTimeXyReadable = createTimeXyReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUpdateTimeXyUnix()
|
||||||
|
{
|
||||||
|
return updateTimeXyUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdateTimeXyUnix(Long updateTimeXyUnix)
|
||||||
|
{
|
||||||
|
this.updateTimeXyUnix = updateTimeXyUnix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUpdateTimeXyReadable()
|
||||||
|
{
|
||||||
|
return updateTimeXyReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUpdateTimeXyReadable(String updateTimeXyReadable)
|
||||||
|
{
|
||||||
|
this.updateTimeXyReadable = updateTimeXyReadable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRemark()
|
||||||
|
{
|
||||||
|
return remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRemark(String remark)
|
||||||
|
{
|
||||||
|
this.remark = remark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getDbCreateTime()
|
||||||
|
{
|
||||||
|
return dbCreateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDbCreateTime(Date dbCreateTime)
|
||||||
|
{
|
||||||
|
this.dbCreateTime = dbCreateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getDbUpdateTime()
|
||||||
|
{
|
||||||
|
return dbUpdateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDbUpdateTime(Date dbUpdateTime)
|
||||||
|
{
|
||||||
|
this.dbUpdateTime = dbUpdateTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,10 @@ public class JDOrder extends BaseEntity {
|
|||||||
@Excel(name = "型号")
|
@Excel(name = "型号")
|
||||||
private String modelNumber;
|
private String modelNumber;
|
||||||
|
|
||||||
|
/** 列表筛选:型号不含此子串(对应 SQL NOT LIKE %值%),不入库 */
|
||||||
|
@Transient
|
||||||
|
private String modelNumberExclude;
|
||||||
|
|
||||||
/** 链接 */
|
/** 链接 */
|
||||||
@Excel(name = "链接")
|
@Excel(name = "链接")
|
||||||
private String link;
|
private String link;
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class TencentDocBatchPushRecord extends BaseEntity {
|
|||||||
/** 错误数量 */
|
/** 错误数量 */
|
||||||
private Integer errorCount;
|
private Integer errorCount;
|
||||||
|
|
||||||
/** 状态:RUNNING-执行中,SUCCESS-成功,PARTIAL-部分成功,FAILED-失败 */
|
/** 状态:RUNNING-执行中,SUCCESS-成功,PARTIAL-部分成功,FAILED-失败,INTERRUPTED-已中断(超时/未正常结束) */
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
/** 结果消息 */
|
/** 结果消息 */
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.jarvis.domain.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快捷录单页:型号下拉项及该型号最近一次落库单的付款 / 后返
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class QuickRecordModelOption {
|
||||||
|
|
||||||
|
private String modelNumber;
|
||||||
|
|
||||||
|
/** 最近一次订单的下单付款金额 */
|
||||||
|
private Double lastPaymentAmount;
|
||||||
|
|
||||||
|
/** 最近一次订单的后返金额 */
|
||||||
|
private Double lastRebateAmount;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.jarvis.mapper;
|
package com.ruoyi.jarvis.mapper;
|
||||||
|
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||||
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -10,4 +11,6 @@ public interface ErpGoofishOrderEventLogMapper {
|
|||||||
int insert(ErpGoofishOrderEventLog row);
|
int insert(ErpGoofishOrderEventLog row);
|
||||||
|
|
||||||
List<ErpGoofishOrderEventLog> selectByOrderId(@Param("orderId") Long orderId);
|
List<ErpGoofishOrderEventLog> selectByOrderId(@Param("orderId") Long orderId);
|
||||||
|
|
||||||
|
List<ErpGoofishOrderEventLog> selectLogList(ErpGoofishOrderEventLogQuery query);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.jarvis.mapper;
|
package com.ruoyi.jarvis.mapper;
|
||||||
|
|
||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
|
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,6 +62,11 @@ public interface JDOrderMapper {
|
|||||||
* @return 订单列表
|
* @return 订单列表
|
||||||
*/
|
*/
|
||||||
List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD();
|
List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每个型号取其主键最大的一条订单的付款 / 后返(用于快捷录单下拉回填)
|
||||||
|
*/
|
||||||
|
List<QuickRecordModelOption> selectQuickRecordModelOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.mapper;
|
|||||||
import com.ruoyi.jarvis.domain.TencentDocBatchPushRecord;
|
import com.ruoyi.jarvis.domain.TencentDocBatchPushRecord;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,5 +42,11 @@ public interface TencentDocBatchPushRecordMapper {
|
|||||||
*/
|
*/
|
||||||
TencentDocBatchPushRecord selectLastSuccessRecord(@Param("fileId") String fileId,
|
TencentDocBatchPushRecord selectLastSuccessRecord(@Param("fileId") String fileId,
|
||||||
@Param("sheetId") String sheetId);
|
@Param("sheetId") String sheetId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仍为 RUNNING 且开始时间早于指定时间的批次(用于超时归档)
|
||||||
|
*/
|
||||||
|
List<TencentDocBatchPushRecord> selectRunningRecordsBefore(@Param("fileId") String fileId,
|
||||||
|
@Param("beforeTime") Date beforeTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,15 @@ public interface WeComShareLinkLogisticsJobMapper {
|
|||||||
List<WeComShareLinkLogisticsJob> selectJobsNeedingQueueReconcile(@Param("limit") int limit);
|
List<WeComShareLinkLogisticsJob> selectJobsNeedingQueueReconcile(@Param("limit") int limit);
|
||||||
|
|
||||||
int deleteByJobKey(@Param("jobKey") String jobKey);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.ruoyi.jarvis.mq;
|
|||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.ruoyi.jarvis.dto.GoofishNotifyMessage;
|
import com.ruoyi.jarvis.dto.GoofishNotifyMessage;
|
||||||
import com.ruoyi.jarvis.service.goofish.GoofishNotifyAsyncFacade;
|
import com.ruoyi.jarvis.service.goofish.GoofishOrderPipeline;
|
||||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
@@ -12,7 +12,10 @@ import org.springframework.stereotype.Component;
|
|||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 闲管家订单推送异步消费(需配置 rocketmq.name-server)
|
* 闲管家订单推送消费(需配置 rocketmq.name-server)。
|
||||||
|
* <p>
|
||||||
|
* 在此线程内同步执行 {@link GoofishOrderPipeline#runFullPipeline},避免再投递 {@code @Async} 线程池造成
|
||||||
|
* 「MQ 堆积 + goofishTaskExecutor 排队」的双重延迟;回调 HTTP 已在 {@link com.ruoyi.jarvis.service.impl.ErpGoofishOrderServiceImpl#publishOrProcessNotify} 中快速返回。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
@ConditionalOnProperty(name = "rocketmq.name-server")
|
@ConditionalOnProperty(name = "rocketmq.name-server")
|
||||||
@@ -24,7 +27,7 @@ import javax.annotation.Resource;
|
|||||||
public class GoofishOrderNotifyConsumer implements RocketMQListener<String> {
|
public class GoofishOrderNotifyConsumer implements RocketMQListener<String> {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private GoofishNotifyAsyncFacade goofishNotifyAsyncFacade;
|
private GoofishOrderPipeline goofishOrderPipeline;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMessage(String message) {
|
public void onMessage(String message) {
|
||||||
@@ -36,6 +39,6 @@ public class GoofishOrderNotifyConsumer implements RocketMQListener<String> {
|
|||||||
if (body == null) {
|
if (body == null) {
|
||||||
body = new JSONObject();
|
body = new JSONObject();
|
||||||
}
|
}
|
||||||
goofishNotifyAsyncFacade.afterNotify(m.getAppid(), body);
|
goofishOrderPipeline.runFullPipeline(m.getAppid(), body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.service;
|
|||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||||
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -41,6 +42,15 @@ public interface IErpGoofishOrderService {
|
|||||||
*/
|
*/
|
||||||
void notifyJdWaybillReady(Long jdOrderId);
|
void notifyJdWaybillReady(Long jdOrderId);
|
||||||
|
|
||||||
|
/** 京东物流服务在写 Redis / 企微货主推送后、触发闲鱼同步前记一笔(source=JD_LOGISTICS_PUSH)。 */
|
||||||
|
void traceJdLogisticsPushForGoofish(Long jdOrderId, String waybillNo, String summary);
|
||||||
|
|
||||||
|
/** 是否存在关联本京东单的闲管家订单(用于物流企微走闲鱼自建应用)。 */
|
||||||
|
boolean hasLinkedGoofishOrder(Long jdOrderId);
|
||||||
|
|
||||||
/** 订单状态 / 物流 / 发货 变更日志(新→旧) */
|
/** 订单状态 / 物流 / 发货 变更日志(新→旧) */
|
||||||
List<ErpGoofishOrderEventLog> listEventLogsByOrderId(Long orderId);
|
List<ErpGoofishOrderEventLog> listEventLogsByOrderId(Long orderId);
|
||||||
|
|
||||||
|
/** 全表分页检索(排查用,配合 PageHelper) */
|
||||||
|
List<ErpGoofishOrderEventLog> selectEventLogList(ErpGoofishOrderEventLogQuery query);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,13 @@ public interface IInstructionService {
|
|||||||
* @return 执行结果文本列表(可能为单条或多条)
|
* @return 执行结果文本列表(可能为单条或多条)
|
||||||
*/
|
*/
|
||||||
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole);
|
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行文本指令(支持传入企微成员 UserID,用于「京」统计按绑定联盟过滤)
|
||||||
|
* @param wecomUserId 企业微信成员 UserID;控制台等非企微入口传 null(按全局规则统计)
|
||||||
|
*/
|
||||||
|
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole, String wecomUserId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取历史消息记录
|
* 获取历史消息记录
|
||||||
* @param type 消息类型:request(请求) 或 response(响应)
|
* @param type 消息类型:request(请求) 或 response(响应)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.jarvis.service;
|
package com.ruoyi.jarvis.service;
|
||||||
|
|
||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
|
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,6 +49,9 @@ public interface IJDOrderService {
|
|||||||
|
|
||||||
/** 查询分销标记为F或PDD且有物流链接的订单列表 */
|
/** 查询分销标记为F或PDD且有物流链接的订单列表 */
|
||||||
java.util.List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD();
|
java.util.List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD();
|
||||||
|
|
||||||
|
/** 快捷录单:型号及最近一次单的付款 / 后返 */
|
||||||
|
List<QuickRecordModelOption> selectQuickRecordModelOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,14 @@ public interface ILogisticsService {
|
|||||||
* @return 健康状态信息,包含是否健康、状态描述等
|
* @return 健康状态信息,包含是否健康、状态描述等
|
||||||
*/
|
*/
|
||||||
HealthCheckResult checkHealth();
|
HealthCheckResult checkHealth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造调用物流解析服务的完整 GET URL(路径与编码与 {@link #fetchLogisticsAndPush} 一致)。
|
||||||
|
* 配置多个 {@code jarvis.server.logistics.base-urls} 时按轮询选取 base,便于内网多实例并行。
|
||||||
|
*
|
||||||
|
* @param logisticsLink 原始物流追踪链接(未编码)
|
||||||
|
*/
|
||||||
|
String buildFetchLogisticsRequestUrl(String logisticsLink);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 健康检测结果
|
* 健康检测结果
|
||||||
|
|||||||
@@ -41,5 +41,10 @@ public interface ITencentDocBatchPushService {
|
|||||||
* 获取推送状态和倒计时信息
|
* 获取推送状态和倒计时信息
|
||||||
*/
|
*/
|
||||||
Map<String, Object> getPushStatusAndCountdown();
|
Map<String, Object> getPushStatusAndCountdown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将长时间仍处于 RUNNING 的批次归档为 INTERRUPTED(并可选发企微告警,见实现类配置)
|
||||||
|
*/
|
||||||
|
void reconcileStaleRunningRecords(String fileId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
|
|||||||
public interface IWeComInboundService {
|
public interface IWeComInboundService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 首条进入被动回复;其余由控制器异步调 wxSend /wecom/active-push。
|
* 长文本按企微上限拆成多段(每段 ≤2048 UTF-8 字节):首段被动回复,后续段由控制器异步调 wxSend /wecom/active-push。
|
||||||
*/
|
*/
|
||||||
WeComInboundResult handleInbound(WeComInboundRequest request);
|
WeComInboundResult handleInbound(WeComInboundRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,4 +16,10 @@ public interface IWeComShareLinkLogisticsJobService {
|
|||||||
* jobKey 固定为 tracebf{traceId},可重复执行跳过已存在项。
|
* jobKey 固定为 tracebf{traceId},可重复执行跳过已存在项。
|
||||||
*/
|
*/
|
||||||
Map<String, Object> backfillImportedFromInboundTrace();
|
Map<String, Object> backfillImportedFromInboundTrace();
|
||||||
|
|
||||||
|
List<WeComShareLinkLogisticsJob> selectRecentForInstruction(String remarkKeyword, int days, int limit);
|
||||||
|
|
||||||
|
int deleteByJobKey(String jobKey);
|
||||||
|
|
||||||
|
int deleteByRemarkAndTrackingUrl(String remark, String trackingUrl);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ package com.ruoyi.jarvis.service;
|
|||||||
*/
|
*/
|
||||||
public interface IWxSendService {
|
public interface IWxSendService {
|
||||||
/**
|
/**
|
||||||
* 检查微信推送服务健康状态
|
* 检查微信推送服务健康状态(会真实下发一条测试消息,仅用于服务监控页「手动测试」)
|
||||||
* @return 健康状态信息,包含是否健康、状态描述等
|
* @return 健康状态信息,包含是否健康、状态描述等
|
||||||
*/
|
*/
|
||||||
HealthCheckResult checkHealth();
|
HealthCheckResult checkHealth();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已配置的微信推送健康检查 URL(展示用,不发起请求)
|
||||||
|
*/
|
||||||
|
String getHealthCheckServiceUrl();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 健康检测结果
|
* 健康检测结果
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ public class GoofishOrderChangeLogger {
|
|||||||
public static final String TYPE_LOGISTICS = "LOGISTICS_SYNC";
|
public static final String TYPE_LOGISTICS = "LOGISTICS_SYNC";
|
||||||
public static final String TYPE_SHIP = "SHIP";
|
public static final String TYPE_SHIP = "SHIP";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 京东物流扫描:货主通知已由 LogisticsServiceImpl 走闲鱼自建应用或 PDD;此处仅落库,避免再调 wxSend 重复推送。
|
||||||
|
*/
|
||||||
|
public static final String SOURCE_JD_LOGISTICS_PUSH = "JD_LOGISTICS_PUSH";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 写入本地运单:与后续详情运单合并、发货成功通知重复度高,仅落库便于对账,不推企微。
|
||||||
|
*/
|
||||||
|
public static final String SOURCE_REDIS_WAYBILL = "REDIS_WAYBILL";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper;
|
private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper;
|
||||||
|
|
||||||
@@ -35,6 +45,14 @@ public class GoofishOrderChangeLogger {
|
|||||||
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||||
|
|
||||||
public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message) {
|
public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message) {
|
||||||
|
append(orderId, appKey, orderNo, eventType, source, message, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param notifyWecom 为 false 时仅写事件日志,不推企微(用于过滤纯对齐类状态)。
|
||||||
|
*/
|
||||||
|
public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message,
|
||||||
|
boolean notifyWecom) {
|
||||||
if (orderId == null || StringUtils.isEmpty(message)) {
|
if (orderId == null || StringUtils.isEmpty(message)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -54,6 +72,12 @@ public class GoofishOrderChangeLogger {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log.info("[goofish-order-event] orderId={} orderNo={} type={} source={} {}", orderId, orderNo, eventType, source, msg);
|
log.info("[goofish-order-event] orderId={} orderNo={} type={} source={} {}", orderId, orderNo, eventType, source, msg);
|
||||||
|
if (SOURCE_JD_LOGISTICS_PUSH.equals(source) || SOURCE_REDIS_WAYBILL.equals(source)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!notifyWecom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
wxSendGoofishNotifyClient.notifyGoofishEvent(orderNo, eventType, source, msg);
|
wxSendGoofishNotifyClient.notifyGoofishEvent(orderNo, eventType, source, msg);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -74,16 +98,21 @@ public class GoofishOrderChangeLogger {
|
|||||||
RowSnap b = RowSnap.from(beforeSnap);
|
RowSnap b = RowSnap.from(beforeSnap);
|
||||||
RowSnap a = RowSnap.from(afterRow);
|
RowSnap a = RowSnap.from(afterRow);
|
||||||
|
|
||||||
|
boolean refundDiff = !Objects.equals(b.refundStatus, a.refundStatus);
|
||||||
|
boolean orderDiff = !Objects.equals(b.orderStatus, a.orderStatus);
|
||||||
|
|
||||||
List<String> orderParts = new ArrayList<>();
|
List<String> orderParts = new ArrayList<>();
|
||||||
if (!Objects.equals(b.orderStatus, a.orderStatus)) {
|
if (orderDiff) {
|
||||||
orderParts.add("order_status " + str(b.orderStatus) + "→" + str(a.orderStatus));
|
orderParts.add("订单状态 " + GoofishStatusLabels.orderStatusChangeForNotify(b.orderStatus, a.orderStatus));
|
||||||
}
|
}
|
||||||
if (!Objects.equals(b.refundStatus, a.refundStatus)) {
|
if (refundDiff) {
|
||||||
orderParts.add("refund_status " + str(b.refundStatus) + "→" + str(a.refundStatus));
|
orderParts.add("退款状态 " + GoofishStatusLabels.refundStatusChange(b.refundStatus, a.refundStatus));
|
||||||
}
|
}
|
||||||
if (!orderParts.isEmpty()) {
|
if (!orderParts.isEmpty()) {
|
||||||
|
boolean notifyWecom = refundDiff || (orderDiff
|
||||||
|
&& GoofishStatusLabels.isWxNotifiableOrderStatusChange(b.orderStatus, a.orderStatus));
|
||||||
append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_ORDER, source,
|
append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_ORDER, source,
|
||||||
String.join(";", orderParts));
|
String.join(";", orderParts), notifyWecom);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> logParts = new ArrayList<>();
|
List<String> logParts = new ArrayList<>();
|
||||||
@@ -91,10 +120,10 @@ public class GoofishOrderChangeLogger {
|
|||||||
logParts.add("平台运单 " + str(b.detailWaybillNo) + "→" + str(a.detailWaybillNo));
|
logParts.add("平台运单 " + str(b.detailWaybillNo) + "→" + str(a.detailWaybillNo));
|
||||||
}
|
}
|
||||||
if (!eqStr(b.detailExpressCode, a.detailExpressCode)) {
|
if (!eqStr(b.detailExpressCode, a.detailExpressCode)) {
|
||||||
logParts.add("express_code " + str(b.detailExpressCode) + "→" + str(a.detailExpressCode));
|
logParts.add("快递编码 " + str(b.detailExpressCode) + "→" + str(a.detailExpressCode));
|
||||||
}
|
}
|
||||||
if (!eqStr(b.detailExpressName, a.detailExpressName)) {
|
if (!eqStr(b.detailExpressName, a.detailExpressName)) {
|
||||||
logParts.add("express_name " + str(b.detailExpressName) + "→" + str(a.detailExpressName));
|
logParts.add("快递名称 " + str(b.detailExpressName) + "→" + str(a.detailExpressName));
|
||||||
}
|
}
|
||||||
if (!eqStr(b.localWaybillNo, a.localWaybillNo)) {
|
if (!eqStr(b.localWaybillNo, a.localWaybillNo)) {
|
||||||
logParts.add("本地运单 " + str(b.localWaybillNo) + "→" + str(a.localWaybillNo));
|
logParts.add("本地运单 " + str(b.localWaybillNo) + "→" + str(a.localWaybillNo));
|
||||||
|
|||||||
@@ -73,7 +73,11 @@ public class GoofishOrderPipeline {
|
|||||||
ErpGoofishOrder row = upsertFromNotify(appKey, item, lastNotifyJson, "LIST_UPSERT");
|
ErpGoofishOrder row = upsertFromNotify(appKey, item, lastNotifyJson, "LIST_UPSERT");
|
||||||
tryLinkJdOrder(row);
|
tryLinkJdOrder(row);
|
||||||
mergeSummaryFromOrderDetailShape(row, item, "LIST");
|
mergeSummaryFromOrderDetailShape(row, item, "LIST");
|
||||||
refreshDetail(row);
|
// 仅待发货等需判物流/发货的状态拉详情;其它状态由推送回调更新,减轻开放平台与本地压力
|
||||||
|
List<Integer> awaitingShip = resolveAutoShipOrderStatuses();
|
||||||
|
if (row.getOrderStatus() != null && awaitingShip.contains(row.getOrderStatus())) {
|
||||||
|
refreshDetail(row);
|
||||||
|
}
|
||||||
syncWaybillFromRedis(row);
|
syncWaybillFromRedis(row);
|
||||||
tryAutoShip(row);
|
tryAutoShip(row);
|
||||||
}
|
}
|
||||||
@@ -139,15 +143,27 @@ public class GoofishOrderPipeline {
|
|||||||
if (existingBeforeUpdate == null) {
|
if (existingBeforeUpdate == null) {
|
||||||
goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(),
|
goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_ORDER, upsertSource,
|
GoofishOrderChangeLogger.TYPE_ORDER, upsertSource,
|
||||||
"新订单入库 order_status=" + loaded.getOrderStatus() + " refund_status=" + loaded.getRefundStatus());
|
"新订单入库,订单状态 " + GoofishStatusLabels.orderStatusHumanForNotify(loaded.getOrderStatus())
|
||||||
|
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(loaded.getRefundStatus()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!Objects.equals(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus())
|
boolean refundDiff = !Objects.equals(existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus());
|
||||||
|| !Objects.equals(existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus())) {
|
boolean orderDiff = !Objects.equals(existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus());
|
||||||
|
if (orderDiff || refundDiff) {
|
||||||
|
List<String> parts = new ArrayList<>(2);
|
||||||
|
if (orderDiff) {
|
||||||
|
parts.add("订单状态 " + GoofishStatusLabels.orderStatusChangeForNotify(
|
||||||
|
existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()));
|
||||||
|
}
|
||||||
|
if (refundDiff) {
|
||||||
|
parts.add("退款状态 " + GoofishStatusLabels.refundStatusChange(
|
||||||
|
existingBeforeUpdate.getRefundStatus(), upsertPayload.getRefundStatus()));
|
||||||
|
}
|
||||||
|
boolean notifyWecom = refundDiff || (orderDiff && GoofishStatusLabels.isWxNotifiableOrderStatusChange(
|
||||||
|
existingBeforeUpdate.getOrderStatus(), upsertPayload.getOrderStatus()));
|
||||||
goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(),
|
goofishOrderChangeLogger.append(loaded.getId(), loaded.getAppKey(), loaded.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_ORDER, upsertSource,
|
GoofishOrderChangeLogger.TYPE_ORDER, upsertSource,
|
||||||
"order_status " + existingBeforeUpdate.getOrderStatus() + "→" + upsertPayload.getOrderStatus()
|
String.join(";", parts), notifyWecom);
|
||||||
+ ";refund_status " + existingBeforeUpdate.getRefundStatus() + "→" + upsertPayload.getRefundStatus());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,8 +435,8 @@ public class GoofishOrderPipeline {
|
|||||||
row.setLocalWaybillNo(wb.trim());
|
row.setLocalWaybillNo(wb.trim());
|
||||||
if (goofishOrderChangeLogger != null) {
|
if (goofishOrderChangeLogger != null) {
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_LOGISTICS, "REDIS_WAYBILL",
|
GoofishOrderChangeLogger.TYPE_LOGISTICS, GoofishOrderChangeLogger.SOURCE_REDIS_WAYBILL,
|
||||||
"本地运单 " + (prev == null || prev.isEmpty() ? "null" : prev) + "→" + wb.trim());
|
"本地运单 " + (prev == null || prev.isEmpty() ? "无" : prev) + "→" + wb.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,7 +501,7 @@ public class GoofishOrderPipeline {
|
|||||||
if (goofishOrderChangeLogger != null) {
|
if (goofishOrderChangeLogger != null) {
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
||||||
"发货失败(缺地址) " + (row.getShipError() != null ? row.getShipError() : ""));
|
"发货失败(缺地址):" + (row.getShipError() != null ? row.getShipError() : ""));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -526,7 +542,9 @@ public class GoofishOrderPipeline {
|
|||||||
if (goofishOrderChangeLogger != null) {
|
if (goofishOrderChangeLogger != null) {
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
||||||
"发货成功 waybill=" + waybill.trim() + " expressCode=" + expressCode);
|
"发货成功,运单 " + waybill.trim()
|
||||||
|
+ ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus())
|
||||||
|
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus()));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
String msg = r != null ? r.getString("msg") : "unknown";
|
String msg = r != null ? r.getString("msg") : "unknown";
|
||||||
@@ -534,7 +552,7 @@ public class GoofishOrderPipeline {
|
|||||||
if (goofishOrderChangeLogger != null) {
|
if (goofishOrderChangeLogger != null) {
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
||||||
"发货失败 " + (row.getShipError() != null ? row.getShipError() : msg));
|
"发货失败:" + (row.getShipError() != null ? row.getShipError() : msg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -542,7 +560,7 @@ public class GoofishOrderPipeline {
|
|||||||
if (goofishOrderChangeLogger != null) {
|
if (goofishOrderChangeLogger != null) {
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
||||||
"发货异常 " + (row.getShipError() != null ? row.getShipError() : ex.getMessage()));
|
"发货异常:" + (row.getShipError() != null ? row.getShipError() : ex.getMessage()));
|
||||||
}
|
}
|
||||||
log.warn("闲管家发货异常 orderNo={}", row.getOrderNo(), ex);
|
log.warn("闲管家发货异常 orderNo={}", row.getOrderNo(), ex);
|
||||||
}
|
}
|
||||||
@@ -903,6 +921,36 @@ public class GoofishOrderPipeline {
|
|||||||
log.warn("闲管家拉单: pull-max-pages 与 pull-page-size 乘积超过 10000,已收敛 maxPages={}", maxPages);
|
log.warn("闲管家拉单: pull-max-pages 与 pull-page-size 乘积超过 10000,已收敛 maxPages={}", maxPages);
|
||||||
}
|
}
|
||||||
int saved = 0;
|
int saved = 0;
|
||||||
|
List<Integer> listStatusFilters = goofishProperties.isPullListOnlyAutoShipStatuses()
|
||||||
|
? resolveAutoShipOrderStatuses()
|
||||||
|
: null;
|
||||||
|
if (listStatusFilters != null && listStatusFilters.isEmpty()) {
|
||||||
|
listStatusFilters = null;
|
||||||
|
}
|
||||||
|
if (listStatusFilters == null) {
|
||||||
|
saved += pullForAppKeyUpdateTimeRangeOnceForStatuses(appKey, cred, authorizeIds, updateTimeStartSec,
|
||||||
|
updateTimeEndSec, pageSize, maxPages, null);
|
||||||
|
} else {
|
||||||
|
for (Integer st : listStatusFilters) {
|
||||||
|
saved += pullForAppKeyUpdateTimeRangeOnceForStatuses(appKey, cred, authorizeIds, updateTimeStartSec,
|
||||||
|
updateTimeEndSec, pageSize, maxPages, st);
|
||||||
|
try {
|
||||||
|
Thread.sleep(100);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param orderStatusFilter null 表示列表请求不带 order_status(拉全状态)
|
||||||
|
*/
|
||||||
|
private int pullForAppKeyUpdateTimeRangeOnceForStatuses(String appKey, IERPAccount cred, List<Long> authorizeIds,
|
||||||
|
long updateTimeStartSec, long updateTimeEndSec, int pageSize, int maxPages, Integer orderStatusFilter) {
|
||||||
|
int saved = 0;
|
||||||
for (Long aid : authorizeIds) {
|
for (Long aid : authorizeIds) {
|
||||||
int page = 1;
|
int page = 1;
|
||||||
while (page <= maxPages) {
|
while (page <= maxPages) {
|
||||||
@@ -910,6 +958,9 @@ public class GoofishOrderPipeline {
|
|||||||
q.setAuthorizeId(aid);
|
q.setAuthorizeId(aid);
|
||||||
q.setUpdateTime(updateTimeStartSec, updateTimeEndSec);
|
q.setUpdateTime(updateTimeStartSec, updateTimeEndSec);
|
||||||
q.setPage(page, pageSize);
|
q.setPage(page, pageSize);
|
||||||
|
if (orderStatusFilter != null) {
|
||||||
|
q.setOrderStatus(orderStatusFilter);
|
||||||
|
}
|
||||||
String resp;
|
String resp;
|
||||||
try {
|
try {
|
||||||
resp = q.getResponseBody();
|
resp = q.getResponseBody();
|
||||||
@@ -936,8 +987,8 @@ public class GoofishOrderPipeline {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (page == maxPages) {
|
if (page == maxPages) {
|
||||||
log.warn("闲管家拉单已达最大页数 appKey={} aid={} 区间[{},{}];若订单更多请缩小 pull-time-chunk-seconds",
|
log.warn("闲管家拉单已达最大页数 appKey={} aid={} 区间[{},{}] orderStatus={};若订单更多请缩小 pull-time-chunk-seconds",
|
||||||
appKey, aid, updateTimeStartSec, updateTimeEndSec);
|
appKey, aid, updateTimeStartSec, updateTimeEndSec, orderStatusFilter);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
page++;
|
page++;
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package com.ruoyi.jarvis.service.goofish;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 闲管家开放平台:订单状态、退款状态中文说明(与 Apifox 订单列表 schema 一致)
|
||||||
|
*/
|
||||||
|
public final class GoofishStatusLabels {
|
||||||
|
|
||||||
|
private static final Map<Integer, String> ORDER = new HashMap<>();
|
||||||
|
private static final Map<Integer, String> REFUND = new HashMap<>();
|
||||||
|
|
||||||
|
static {
|
||||||
|
ORDER.put(11, "待付款");
|
||||||
|
ORDER.put(12, "待发货");
|
||||||
|
ORDER.put(21, "已发货");
|
||||||
|
ORDER.put(22, "已完成");
|
||||||
|
ORDER.put(23, "已退款");
|
||||||
|
ORDER.put(24, "已关闭");
|
||||||
|
REFUND.put(0, "未申请退款");
|
||||||
|
REFUND.put(1, "待商家处理");
|
||||||
|
REFUND.put(2, "待买家退货");
|
||||||
|
REFUND.put(3, "待商家收货");
|
||||||
|
REFUND.put(4, "退款关闭");
|
||||||
|
REFUND.put(5, "退款成功");
|
||||||
|
REFUND.put(6, "已拒绝退款");
|
||||||
|
REFUND.put(8, "待确认退货地址");
|
||||||
|
}
|
||||||
|
|
||||||
|
private GoofishStatusLabels() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅中文;未知时返回 null */
|
||||||
|
public static String orderStatusLabel(Integer s) {
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ORDER.get(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 仅中文;未知时返回 null */
|
||||||
|
public static String refundStatusLabel(Integer s) {
|
||||||
|
if (s == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return REFUND.get(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于日志/企微:优先中文,未知时带码便于排查
|
||||||
|
*/
|
||||||
|
public static String orderStatusHuman(Integer s) {
|
||||||
|
String t = orderStatusLabel(s);
|
||||||
|
if (t != null) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
if (s == null) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
return "未识别状态(" + s + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String refundStatusHuman(Integer s) {
|
||||||
|
String t = refundStatusLabel(s);
|
||||||
|
if (t != null) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
if (s == null) {
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
return "未识别状态(" + s + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 变化摘要:A→B,两边均为人话
|
||||||
|
*/
|
||||||
|
public static String orderStatusChange(Integer from, Integer to) {
|
||||||
|
return orderStatusHuman(from) + " → " + orderStatusHuman(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快照/首行展示:待发阶段注明已付款语义(开放平台码 12 即待发货)。
|
||||||
|
*/
|
||||||
|
public static String orderStatusHumanForNotify(Integer s) {
|
||||||
|
if (Objects.equals(s, 12)) {
|
||||||
|
return "待发货(已付款)";
|
||||||
|
}
|
||||||
|
return orderStatusHuman(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企微通知用变化文案:付款完成单独写「已付款(待发货)」。
|
||||||
|
*/
|
||||||
|
public static String orderStatusChangeForNotify(Integer from, Integer to) {
|
||||||
|
if (Objects.equals(from, 11) && Objects.equals(to, 12)) {
|
||||||
|
return orderStatusHuman(11) + " → 已付款(待发货)";
|
||||||
|
}
|
||||||
|
return orderStatusHuman(from) + " → " + orderStatusHumanForNotify(to);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否与「付款、待发、在途、终态退款/完成/关闭」相关,从而值得推企微(排除已由 SHIP 覆盖的待发→已发)。
|
||||||
|
*/
|
||||||
|
public static boolean isWxNotifiableOrderStatusChange(Integer from, Integer to) {
|
||||||
|
if (Objects.equals(from, to)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Objects.equals(from, 12) && Objects.equals(to, 21)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int[] anchors = {11, 12, 21, 22, 23, 24};
|
||||||
|
for (int code : anchors) {
|
||||||
|
if (Objects.equals(from, code) || Objects.equals(to, code)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String refundStatusChange(Integer from, Integer to) {
|
||||||
|
return refundStatusHuman(from) + " → " + refundStatusHuman(to);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
|
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||||
|
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||||
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
||||||
import com.ruoyi.jarvis.dto.GoofishNotifyMessage;
|
import com.ruoyi.jarvis.dto.GoofishNotifyMessage;
|
||||||
import com.ruoyi.jarvis.mapper.ErpGoofishOrderEventLogMapper;
|
import com.ruoyi.jarvis.mapper.ErpGoofishOrderEventLogMapper;
|
||||||
@@ -12,6 +13,7 @@ import com.ruoyi.jarvis.mapper.ErpGoofishOrderMapper;
|
|||||||
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
|
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
|
||||||
import com.ruoyi.jarvis.service.IErpOpenConfigService;
|
import com.ruoyi.jarvis.service.IErpOpenConfigService;
|
||||||
import com.ruoyi.jarvis.service.goofish.GoofishNotifyAsyncFacade;
|
import com.ruoyi.jarvis.service.goofish.GoofishNotifyAsyncFacade;
|
||||||
|
import com.ruoyi.jarvis.service.goofish.GoofishOrderChangeLogger;
|
||||||
import com.ruoyi.jarvis.service.goofish.GoofishOrderPipeline;
|
import com.ruoyi.jarvis.service.goofish.GoofishOrderPipeline;
|
||||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
@@ -46,6 +48,9 @@ public class ErpGoofishOrderServiceImpl implements IErpGoofishOrderService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IErpOpenConfigService erpOpenConfigService;
|
private IErpOpenConfigService erpOpenConfigService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private GoofishOrderChangeLogger goofishOrderChangeLogger;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void publishOrProcessNotify(String appid, Long timestamp, JSONObject body) {
|
public void publishOrProcessNotify(String appid, Long timestamp, JSONObject body) {
|
||||||
RocketMQTemplate mq = rocketMQTemplate.getIfAvailable();
|
RocketMQTemplate mq = rocketMQTemplate.getIfAvailable();
|
||||||
@@ -166,6 +171,43 @@ public class ErpGoofishOrderServiceImpl implements IErpGoofishOrderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasLinkedGoofishOrder(Long jdOrderId) {
|
||||||
|
if (jdOrderId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ErpGoofishOrder query = new ErpGoofishOrder();
|
||||||
|
query.setJdOrderId(jdOrderId);
|
||||||
|
List<ErpGoofishOrder> list = erpGoofishOrderMapper.selectList(query);
|
||||||
|
return list != null && !list.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void traceJdLogisticsPushForGoofish(Long jdOrderId, String waybillNo, String summary) {
|
||||||
|
if (jdOrderId == null || goofishOrderChangeLogger == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ErpGoofishOrder query = new ErpGoofishOrder();
|
||||||
|
query.setJdOrderId(jdOrderId);
|
||||||
|
List<ErpGoofishOrder> list = erpGoofishOrderMapper.selectList(query);
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String wb = waybillNo != null ? waybillNo.trim() : "";
|
||||||
|
String sum = summary != null ? summary : "";
|
||||||
|
String msg = "京东订单 " + jdOrderId + ",运单 " + wb + ";" + sum;
|
||||||
|
if (msg.length() > 1000) {
|
||||||
|
msg = msg.substring(0, 999) + "…";
|
||||||
|
}
|
||||||
|
for (ErpGoofishOrder row : list) {
|
||||||
|
if (row.getId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
|
GoofishOrderChangeLogger.TYPE_LOGISTICS, GoofishOrderChangeLogger.SOURCE_JD_LOGISTICS_PUSH, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ErpGoofishOrderEventLog> listEventLogsByOrderId(Long orderId) {
|
public List<ErpGoofishOrderEventLog> listEventLogsByOrderId(Long orderId) {
|
||||||
if (orderId == null) {
|
if (orderId == null) {
|
||||||
@@ -174,4 +216,13 @@ public class ErpGoofishOrderServiceImpl implements IErpGoofishOrderService {
|
|||||||
List<ErpGoofishOrderEventLog> list = erpGoofishOrderEventLogMapper.selectByOrderId(orderId);
|
List<ErpGoofishOrderEventLog> list = erpGoofishOrderEventLogMapper.selectByOrderId(orderId);
|
||||||
return list != null ? list : Collections.emptyList();
|
return list != null ? list : Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ErpGoofishOrderEventLog> selectEventLogList(ErpGoofishOrderEventLogQuery query) {
|
||||||
|
if (query == null) {
|
||||||
|
query = new ErpGoofishOrderEventLogQuery();
|
||||||
|
}
|
||||||
|
List<ErpGoofishOrderEventLog> list = erpGoofishOrderEventLogMapper.selectLogList(query);
|
||||||
|
return list != null ? list : Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package com.ruoyi.jarvis.service.impl;
|
|||||||
|
|
||||||
import com.ruoyi.jarvis.domain.OrderRows;
|
import com.ruoyi.jarvis.domain.OrderRows;
|
||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
|
import com.ruoyi.jarvis.domain.SuperAdmin;
|
||||||
|
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||||
import com.ruoyi.jarvis.service.IInstructionService;
|
import com.ruoyi.jarvis.service.IInstructionService;
|
||||||
import com.ruoyi.jarvis.service.IOrderRowsService;
|
import com.ruoyi.jarvis.service.IOrderRowsService;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||||
import com.ruoyi.jarvis.service.IProductJdConfigService;
|
import com.ruoyi.jarvis.service.IProductJdConfigService;
|
||||||
import com.ruoyi.jarvis.service.IPhoneReplaceConfigService;
|
import com.ruoyi.jarvis.service.IPhoneReplaceConfigService;
|
||||||
import com.ruoyi.jarvis.service.SuperAdminService;
|
import com.ruoyi.jarvis.service.SuperAdminService;
|
||||||
|
import com.ruoyi.jarvis.service.IWeComShareLinkLogisticsJobService;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -51,12 +54,23 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
@Resource
|
@Resource
|
||||||
private com.ruoyi.jarvis.service.ITencentDocTokenService tencentDocTokenService;
|
private com.ruoyi.jarvis.service.ITencentDocTokenService tencentDocTokenService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private IWeComShareLinkLogisticsJobService weComShareLinkLogisticsJobService;
|
||||||
|
@Resource
|
||||||
private com.ruoyi.jarvis.config.TencentDocConfig tencentDocConfig;
|
private com.ruoyi.jarvis.config.TencentDocConfig tencentDocConfig;
|
||||||
@Resource
|
@Resource
|
||||||
private com.ruoyi.common.core.redis.RedisCache redisCache;
|
private com.ruoyi.common.core.redis.RedisCache redisCache;
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService tencentDocDelayedPushService;
|
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService tencentDocDelayedPushService;
|
||||||
|
|
||||||
|
/** 与 {@link com.ruoyi.jarvis.service.impl.WeComInboundServiceImpl#WE_COM_SUPER_USER_ID} 一致:该账号在京统计中视为全局视角 */
|
||||||
|
private static final String WE_COM_SUPER_GLOBAL_STATS_USER = "LinPingFan";
|
||||||
|
|
||||||
|
/** 与企微物流分享链解析一致 */
|
||||||
|
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 保持一致)
|
// 录单模板(与 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" + "京粉实际价格:不用填";
|
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" + "京粉实际价格:不用填";
|
||||||
|
|
||||||
@@ -72,6 +86,11 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> execute(String command, boolean forceGenerate, boolean isFromConsole) {
|
public List<String> execute(String command, boolean forceGenerate, boolean isFromConsole) {
|
||||||
|
return execute(command, forceGenerate, isFromConsole, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> execute(String command, boolean forceGenerate, boolean isFromConsole, String wecomUserId) {
|
||||||
// 存储接收的消息到Redis队列
|
// 存储接收的消息到Redis队列
|
||||||
storeMessageToRedis("instruction:request", command);
|
storeMessageToRedis("instruction:request", command);
|
||||||
|
|
||||||
@@ -89,7 +108,7 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
|
|
||||||
// 一级命令分流:京系(统计/订单)、录单/慢单、转链/礼金…
|
// 一级命令分流:京系(统计/订单)、录单/慢单、转链/礼金…
|
||||||
if (input.startsWith("京") || menuKeywords().contains(input)) {
|
if (input.startsWith("京") || menuKeywords().contains(input)) {
|
||||||
result = Collections.singletonList(handleJingFen(input.replaceFirst("^京", "")));
|
result = Collections.singletonList(handleJingFen(input.replaceFirst("^京", ""), wecomUserId));
|
||||||
}
|
}
|
||||||
// TF/H/生/拼多多 生成类指令
|
// TF/H/生/拼多多 生成类指令
|
||||||
else if (input.startsWith("TF")) {
|
else if (input.startsWith("TF")) {
|
||||||
@@ -120,6 +139,77 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 「京」统计/订单所用京粉订单数据源:企微成员按超级管理员绑定 unionId;全局视角排除「不参与订单统计」联盟。
|
||||||
|
*/
|
||||||
|
private static final class JingStatsScope {
|
||||||
|
private final List<OrderRows> rows;
|
||||||
|
/** 可为 null:禁止访问统计(不参与统计等) */
|
||||||
|
private final String denyMessage;
|
||||||
|
/** 回复前缀,标明当前统计归属 */
|
||||||
|
private final String replyPrefix;
|
||||||
|
|
||||||
|
private JingStatsScope(List<OrderRows> rows, String denyMessage, String replyPrefix) {
|
||||||
|
this.rows = rows != null ? rows : Collections.emptyList();
|
||||||
|
this.denyMessage = denyMessage;
|
||||||
|
this.replyPrefix = replyPrefix != null ? replyPrefix : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
static JingStatsScope denied(String msg) {
|
||||||
|
return new JingStatsScope(Collections.emptyList(), msg, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static JingStatsScope ok(List<OrderRows> rows, String replyPrefix) {
|
||||||
|
return new JingStatsScope(rows, null, replyPrefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Long> buildExcludeUnionIdsForStats() {
|
||||||
|
List<Long> excludeUnionIds = new ArrayList<>();
|
||||||
|
List<SuperAdmin> superAdminList = superAdminService.selectSuperAdminList(null);
|
||||||
|
if (superAdminList != null) {
|
||||||
|
for (SuperAdmin admin : superAdminList) {
|
||||||
|
if (admin.getIsCount() != null && admin.getIsCount() == 0 && admin.getUnionId() != null && !admin.getUnionId().trim().isEmpty()) {
|
||||||
|
try {
|
||||||
|
excludeUnionIds.add(Long.parseLong(admin.getUnionId().trim()));
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return excludeUnionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JingStatsScope resolveJingStatsScope(String wecomUserId) {
|
||||||
|
if (wecomUserId == null || wecomUserId.trim().isEmpty()
|
||||||
|
|| WE_COM_SUPER_GLOBAL_STATS_USER.equals(wecomUserId.trim())) {
|
||||||
|
List<OrderRows> all = orderRowsService.selectOrderRowsListWithFilter(new OrderRows(), null, null, buildExcludeUnionIdsForStats());
|
||||||
|
return JingStatsScope.ok(all, "");
|
||||||
|
}
|
||||||
|
String wxUser = wecomUserId.trim();
|
||||||
|
SuperAdmin sa = superAdminService.selectSuperAdminByWecomUserId(wxUser);
|
||||||
|
if (sa == null) {
|
||||||
|
return JingStatsScope.ok(Collections.emptyList(), "");
|
||||||
|
}
|
||||||
|
if (sa.getIsCount() != null && sa.getIsCount() == 0) {
|
||||||
|
return JingStatsScope.denied("「京统计」\n\n当前企微账号在后台「超级管理员」中标记为不参与订单统计,无法使用统计与订单类京指令。\n如需开通请联系管理员。");
|
||||||
|
}
|
||||||
|
if (sa.getUnionId() == null || sa.getUnionId().trim().isEmpty()) {
|
||||||
|
return JingStatsScope.denied("「京统计」\n\n当前企微账号未绑定联盟ID,无法匹配京粉订单。\n请在后台「超级管理员」中维护该账号对应行的联盟ID。");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
long uid = Long.parseLong(sa.getUnionId().trim());
|
||||||
|
OrderRows probe = new OrderRows();
|
||||||
|
probe.setUnionId(uid);
|
||||||
|
List<OrderRows> scoped = orderRowsService.selectOrderRowsListWithFilter(probe, null, null, Collections.emptyList());
|
||||||
|
String namePart = sa.getName() != null && !sa.getName().trim().isEmpty() ? sa.getName().trim() : "未命名";
|
||||||
|
String prefix = "【联盟 " + sa.getUnionId().trim() + " · " + namePart + "】\n";
|
||||||
|
return JingStatsScope.ok(scoped, prefix);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return JingStatsScope.denied("「京统计」\n\n联盟ID格式不正确,请联系管理员检查「超级管理员」配置。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将消息存储到Redis队列,最多保留100条
|
* 将消息存储到Redis队列,最多保留100条
|
||||||
* @param key Redis键
|
* @param key Redis键
|
||||||
@@ -192,43 +282,55 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
return new HashSet<>(Arrays.asList("菜单", "今日统计", "昨日统计", "三日统计", "七日统计", "一个月统计", "两个月统计", "三个月统计", "这个月统计", "上个月统计", "今日订单", "昨日订单", "七日订单", "总统计"));
|
return new HashSet<>(Arrays.asList("菜单", "今日统计", "昨日统计", "三日统计", "七日统计", "一个月统计", "两个月统计", "三个月统计", "这个月统计", "上个月统计", "今日订单", "昨日订单", "七日订单", "总统计"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private String handleJingFen(String cmd) {
|
private String handleJingFen(String cmd, String wecomUserId) {
|
||||||
String action = cmd.trim();
|
String action = cmd.trim();
|
||||||
if (action.isEmpty() || action.equals("菜单")) {
|
if (action.isEmpty() || action.equals("菜单")) {
|
||||||
return jingMenu();
|
return jingMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取出所有订单(排除被删除/无效:这里沿用 OrderRowsService 的常规查询,必要时可增加过滤参数)
|
if (action.startsWith("外物列表")) {
|
||||||
List<OrderRows> all = orderRowsService.selectOrderRowsList(new OrderRows());
|
String kw = action.substring("外物列表".length()).trim();
|
||||||
if (all == null) all = Collections.emptyList();
|
return textExternalShareLinkLogisticsList(kw);
|
||||||
|
}
|
||||||
|
if (action.startsWith("外物删")) {
|
||||||
|
String rest = action.substring("外物删".length()).trim();
|
||||||
|
return textExternalShareLinkLogisticsDelete(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
JingStatsScope scope = resolveJingStatsScope(wecomUserId);
|
||||||
|
if (scope.denyMessage != null) {
|
||||||
|
return scope.denyMessage;
|
||||||
|
}
|
||||||
|
List<OrderRows> all = scope.rows;
|
||||||
|
String header = scope.replyPrefix;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "今日统计":
|
case "今日统计":
|
||||||
return statsText(filterByDays(all, 0), "今日统计");
|
return header + statsText(filterByDays(all, 0), "今日统计");
|
||||||
case "昨日统计":
|
case "昨日统计":
|
||||||
return statsText(filterYesterday(all), "昨日统计");
|
return header + statsText(filterYesterday(all), "昨日统计");
|
||||||
case "三日统计":
|
case "三日统计":
|
||||||
return statsText(filterByRange(all, 3), "三日统计");
|
return header + statsText(filterByRange(all, 3), "三日统计");
|
||||||
case "七日统计":
|
case "七日统计":
|
||||||
return statsText(filterByRange(all, 7), "七日统计");
|
return header + statsText(filterByRange(all, 7), "七日统计");
|
||||||
case "一个月统计":
|
case "一个月统计":
|
||||||
return statsText(filterByRange(all, 30), "一个月统计");
|
return header + statsText(filterByRange(all, 30), "一个月统计");
|
||||||
case "两个月统计":
|
case "两个月统计":
|
||||||
return statsText(filterByRange(all, 60), "两个月统计");
|
return header + statsText(filterByRange(all, 60), "两个月统计");
|
||||||
case "三个月统计":
|
case "三个月统计":
|
||||||
return statsText(filterByRange(all, 90), "三个月统计");
|
return header + statsText(filterByRange(all, 90), "三个月统计");
|
||||||
case "这个月统计":
|
case "这个月统计":
|
||||||
return statsText(filterThisMonth(all), "这个月统计");
|
return header + statsText(filterThisMonth(all), "这个月统计");
|
||||||
case "上个月统计":
|
case "上个月统计":
|
||||||
return statsText(filterLastMonth(all), "上个月统计");
|
return header + statsText(filterLastMonth(all), "上个月统计");
|
||||||
case "总统计":
|
case "总统计":
|
||||||
return statsText(all, "总统计");
|
return header + statsText(all, "总统计");
|
||||||
case "今日订单":
|
case "今日订单":
|
||||||
return listOrders(filterByDays(all, 0), "今日订单");
|
return header + listOrders(filterByDays(all, 0), "今日订单");
|
||||||
case "昨日订单":
|
case "昨日订单":
|
||||||
return listOrders(filterYesterday(all), "昨日订单");
|
return header + listOrders(filterYesterday(all), "昨日订单");
|
||||||
case "七日订单":
|
case "七日订单":
|
||||||
return listOrders(filterByRange(all, 7), "七日订单");
|
return header + listOrders(filterByRange(all, 7), "七日订单");
|
||||||
default:
|
default:
|
||||||
// 高级命令:违规N、SKU、搜索、JF… 此处按需扩展
|
// 高级命令:违规N、SKU、搜索、JF… 此处按需扩展
|
||||||
if (action.startsWith("高级")) {
|
if (action.startsWith("高级")) {
|
||||||
@@ -478,11 +580,11 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
// ==================== 追加:按下单人分组统计 ====================
|
// ==================== 追加:按下单人分组统计 ====================
|
||||||
outputs.add("\n━━━━━━━ 按下单人统计 ━━━━━━━");
|
outputs.add("\n━━━━━━━ 按下单人统计 ━━━━━━━");
|
||||||
|
|
||||||
// 按下单人分组(过滤掉拍错退款)
|
// 按下单人「前缀」分组:取第一个 "-" 之前为同组(如 凡-林、凡-淑玲 → 凡;陈文慧-林、陈文慧-666 → 陈文慧);无 "-" 则按完整下单人
|
||||||
Map<String, List<JDOrder>> byBuyer = filtered.stream()
|
Map<String, List<JDOrder>> byBuyer = filtered.stream()
|
||||||
.filter(o -> o.getStatus() == null || !"拍错退款".equals(o.getStatus()))
|
.filter(o -> o.getStatus() == null || !"拍错退款".equals(o.getStatus()))
|
||||||
.filter(o -> o.getBuyer() != null && !o.getBuyer().isEmpty())
|
.filter(o -> o.getBuyer() != null && !o.getBuyer().isEmpty())
|
||||||
.collect(Collectors.groupingBy(JDOrder::getBuyer));
|
.collect(Collectors.groupingBy(o -> buyerGroupKey(o.getBuyer())));
|
||||||
|
|
||||||
List<Map.Entry<String, List<JDOrder>>> buyerEntries = new ArrayList<>(byBuyer.entrySet());
|
List<Map.Entry<String, List<JDOrder>>> buyerEntries = new ArrayList<>(byBuyer.entrySet());
|
||||||
buyerEntries.sort(Comparator.comparing(en -> en.getKey() == null ? "" : en.getKey()));
|
buyerEntries.sort(Comparator.comparing(en -> en.getKey() == null ? "" : en.getKey()));
|
||||||
@@ -2128,16 +2230,207 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
}).collect(Collectors.toList());
|
}).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() {
|
private String jingMenu() {
|
||||||
return "「京粉 · 菜单」\n\n"
|
return "「京粉 · 菜单」\n\n"
|
||||||
+ "企微/机器人前请加「京」,例如:京今日统计\n\n"
|
+ "企微/机器人前请加「京」,例如:京今日统计\n"
|
||||||
|
+ "说明:企微内统计仅含当前账号在「超级管理员」绑定的联盟ID;标记为不参与订单统计的联盟不会在全局汇总中出现(与后台京粉订单列表统计一致)。\n\n"
|
||||||
+ "—— 统计 ——\n"
|
+ "—— 统计 ——\n"
|
||||||
+ "今日统计、昨日统计、三日统计、七日统计\n"
|
+ "今日统计、昨日统计、三日统计、七日统计\n"
|
||||||
+ "一个月统计、两个月统计、三个月统计\n"
|
+ "一个月统计、两个月统计、三个月统计\n"
|
||||||
+ "这个月统计、上个月统计、总统计\n\n"
|
+ "这个月统计、上个月统计、总统计\n\n"
|
||||||
+ "—— 订单 ——\n"
|
+ "—— 订单 ——\n"
|
||||||
+ "今日订单、昨日订单、七日订单\n\n"
|
+ "今日订单、昨日订单、七日订单\n\n"
|
||||||
|
+ "—— 外部分享链物流 ——\n"
|
||||||
|
+ "京外物列表 [关键词]、京外物删(见列表说明)\n\n"
|
||||||
+ "发「京」单独或「京菜单」可再次打开本列表。";
|
+ "发「京」单独或「京菜单」可再次打开本列表。";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2148,6 +2441,7 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
+ "· 京今日统计 / 京昨日统计 / 京七日统计 …\n"
|
+ "· 京今日统计 / 京昨日统计 / 京七日统计 …\n"
|
||||||
+ "· 京今日订单 / 京昨日订单 / 京七日订单\n"
|
+ "· 京今日订单 / 京昨日订单 / 京七日订单\n"
|
||||||
+ "· 慢搜 关键词、慢查 关键词(录单库模糊查询)\n"
|
+ "· 慢搜 关键词、慢查 关键词(录单库模糊查询)\n"
|
||||||
|
+ "· 京外物列表 / 京外物删 — 企微 3.cn 分享链登记查询与删除\n"
|
||||||
+ "· 录单20250101-20250107 或 录单昨日|三日|七日(导出)\n\n"
|
+ "· 录单20250101-20250107 或 录单昨日|三日|七日(导出)\n\n"
|
||||||
+ "说明:转链、礼金等请使用系统内「一键转链」页面。";
|
+ "说明:转链、礼金等请使用系统内「一键转链」页面。";
|
||||||
}
|
}
|
||||||
@@ -2225,6 +2519,25 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 慢单「按下单人统计」分组键:第一个 "-" 前的前缀视为同一对象;无 "-" 时用整段下单人。
|
||||||
|
*/
|
||||||
|
private static String buyerGroupKey(String buyer) {
|
||||||
|
if (buyer == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String t = buyer.trim();
|
||||||
|
if (t.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
int idx = t.indexOf('-');
|
||||||
|
if (idx > 0) {
|
||||||
|
String prefix = t.substring(0, idx).trim();
|
||||||
|
return prefix.isEmpty() ? t : prefix;
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean contains(Object field, String kwLower) {
|
private boolean contains(Object field, String kwLower) {
|
||||||
if (field == null) return false;
|
if (field == null) return false;
|
||||||
return String.valueOf(field).toLowerCase(Locale.ROOT).contains(kwLower);
|
return String.valueOf(field).toLowerCase(Locale.ROOT).contains(kwLower);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.ruoyi.jarvis.service.impl;
|
package com.ruoyi.jarvis.service.impl;
|
||||||
|
|
||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
|
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||||
import com.ruoyi.jarvis.mapper.JDOrderMapper;
|
import com.ruoyi.jarvis.mapper.JDOrderMapper;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderProfitService;
|
import com.ruoyi.jarvis.service.IJDOrderProfitService;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||||
@@ -81,6 +82,11 @@ public class JDOrderServiceImpl implements IJDOrderService {
|
|||||||
public List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD() {
|
public List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD() {
|
||||||
return jdOrderMapper.selectJDOrderListByDistributionMarkFOrPDD();
|
return jdOrderMapper.selectJDOrderListByDistributionMarkFOrPDD();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<QuickRecordModelOption> selectQuickRecordModelOptions() {
|
||||||
|
return jdOrderMapper.selectQuickRecordModelOptions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ import javax.annotation.Resource;
|
|||||||
import javax.annotation.PostConstruct;
|
import javax.annotation.PostConstruct;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import org.springframework.util.DigestUtils;
|
import org.springframework.util.DigestUtils;
|
||||||
|
|
||||||
@@ -58,6 +61,10 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
|
|
||||||
@Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}")
|
@Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}")
|
||||||
private String logisticsBaseUrl;
|
private String logisticsBaseUrl;
|
||||||
|
|
||||||
|
/** 逗号分隔的多个解析服务 base,非空时优先于此列表轮询;例如 http://10.0.0.1:5001,http://10.0.0.2:5001 */
|
||||||
|
@Value("${jarvis.server.logistics.base-urls:}")
|
||||||
|
private String logisticsBaseUrlsRaw;
|
||||||
|
|
||||||
@Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}")
|
@Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}")
|
||||||
private String logisticsFetchPath;
|
private String logisticsFetchPath;
|
||||||
@@ -71,8 +78,10 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
@Resource
|
@Resource
|
||||||
private WeComShareLinkLogisticsJobMapper weComShareLinkLogisticsJobMapper;
|
private WeComShareLinkLogisticsJobMapper weComShareLinkLogisticsJobMapper;
|
||||||
|
|
||||||
private String externalApiUrlTemplate;
|
/** 已规范化(无尾部斜杠)的物流解析服务 base 列表,至少一项 */
|
||||||
private String healthCheckUrl;
|
private List<String> logisticsServiceBases = Collections.emptyList();
|
||||||
|
private String logisticsServiceBasesSummary = "";
|
||||||
|
private final AtomicInteger logisticsBaseRoundRobin = new AtomicInteger(0);
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
@@ -85,13 +94,82 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private IErpGoofishOrderService erpGoofishOrderService;
|
private IErpGoofishOrderService erpGoofishOrderService;
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
public void init() {
|
public void init() {
|
||||||
externalApiUrlTemplate = logisticsBaseUrl + logisticsFetchPath + "?tracking_url=";
|
List<String> list = new ArrayList<>();
|
||||||
healthCheckUrl = logisticsBaseUrl + logisticsHealthPath;
|
if (StringUtils.hasText(logisticsBaseUrlsRaw)) {
|
||||||
logger.info("物流服务地址已初始化: {}", externalApiUrlTemplate);
|
for (String part : logisticsBaseUrlsRaw.split(",")) {
|
||||||
logger.info("物流服务健康检查地址已初始化: {}", healthCheckUrl);
|
String n = normalizeLogisticsBaseUrl(part);
|
||||||
|
if (StringUtils.hasText(n)) {
|
||||||
|
list.add(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
list.add(normalizeLogisticsBaseUrl(logisticsBaseUrl));
|
||||||
|
}
|
||||||
|
logisticsServiceBases = Collections.unmodifiableList(list);
|
||||||
|
logisticsServiceBasesSummary = String.join(", ", logisticsServiceBases);
|
||||||
|
logger.info("物流服务实例 {} 个(轮询): {}", logisticsServiceBases.size(), logisticsServiceBasesSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeLogisticsBaseUrl(String base) {
|
||||||
|
if (!StringUtils.hasText(base)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String t = base.trim();
|
||||||
|
while (t.endsWith("/")) {
|
||||||
|
t = t.substring(0, t.length() - 1);
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String pickLogisticsBaseUrl() {
|
||||||
|
int n = logisticsServiceBases.size();
|
||||||
|
if (n == 0) {
|
||||||
|
return normalizeLogisticsBaseUrl(logisticsBaseUrl);
|
||||||
|
}
|
||||||
|
if (n == 1) {
|
||||||
|
return logisticsServiceBases.get(0);
|
||||||
|
}
|
||||||
|
int idx = Math.floorMod(logisticsBaseRoundRobin.getAndIncrement(), n);
|
||||||
|
return logisticsServiceBases.get(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String buildFetchLogisticsRequestUrl(String logisticsLink) {
|
||||||
|
String base = pickLogisticsBaseUrl();
|
||||||
|
try {
|
||||||
|
return base + logisticsFetchPath + "?tracking_url=" + URLEncoder.encode(logisticsLink, "UTF-8");
|
||||||
|
} catch (java.io.UnsupportedEncodingException e) {
|
||||||
|
throw new IllegalStateException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 与原先单 URL 健康检查语义一致:JSON 字段命中则通过;非 JSON 时仅当正文含 ok/healthy/success 子串则通过。
|
||||||
|
*/
|
||||||
|
private boolean isHealthResponseBodyHealthy(String healthResult) {
|
||||||
|
if (healthResult == null || healthResult.trim().isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JSONObject response = JSON.parseObject(healthResult);
|
||||||
|
if (response != null) {
|
||||||
|
String status = response.getString("status");
|
||||||
|
Boolean healthy = response.getBoolean("healthy");
|
||||||
|
Integer code = response.getInteger("code");
|
||||||
|
if ("ok".equalsIgnoreCase(status) || "healthy".equalsIgnoreCase(status)
|
||||||
|
|| Boolean.TRUE.equals(healthy) || (code != null && code == 200)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (Exception e) {
|
||||||
|
String lowerResult = healthResult.toLowerCase();
|
||||||
|
return lowerResult.contains("ok") || lowerResult.contains("healthy") || lowerResult.contains("success");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -105,46 +183,26 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ILogisticsService.HealthCheckResult checkHealth() {
|
public ILogisticsService.HealthCheckResult checkHealth() {
|
||||||
try {
|
List<String> errors = new ArrayList<>();
|
||||||
logger.debug("开始检查物流服务健康状态 - URL: {}", healthCheckUrl);
|
for (String base : logisticsServiceBases) {
|
||||||
String healthResult = HttpUtils.sendGet(healthCheckUrl);
|
String url = base + logisticsHealthPath;
|
||||||
|
|
||||||
if (healthResult == null || healthResult.trim().isEmpty()) {
|
|
||||||
logger.warn("物流服务健康检查返回空结果");
|
|
||||||
return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查返回空结果", healthCheckUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试解析JSON响应
|
|
||||||
try {
|
try {
|
||||||
JSONObject response = JSON.parseObject(healthResult);
|
logger.debug("开始检查物流服务健康状态 - URL: {}", url);
|
||||||
if (response != null) {
|
String healthResult = HttpUtils.sendGet(url);
|
||||||
// 检查常见的健康状态字段
|
if (isHealthResponseBodyHealthy(healthResult)) {
|
||||||
String status = response.getString("status");
|
logger.debug("物流服务健康检查通过 - {}", url);
|
||||||
Boolean healthy = response.getBoolean("healthy");
|
return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常: " + url,
|
||||||
Integer code = response.getInteger("code");
|
logisticsServiceBasesSummary);
|
||||||
|
|
||||||
if ("ok".equalsIgnoreCase(status) || "healthy".equalsIgnoreCase(status) ||
|
|
||||||
Boolean.TRUE.equals(healthy) || (code != null && code == 200)) {
|
|
||||||
logger.debug("物流服务健康检查通过");
|
|
||||||
return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
logger.warn("物流服务健康检查失败 - URL: {} 响应: {}", url, healthResult);
|
||||||
|
errors.add(url + " -> " + (healthResult == null || healthResult.isEmpty() ? "空响应" : "状态异常"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 如果不是JSON格式,检查是否包含成功标识
|
logger.error("物流服务健康检查异常 - URL: {}, 错误: {}", url, e.getMessage(), e);
|
||||||
String lowerResult = healthResult.toLowerCase();
|
errors.add(url + " -> " + e.getMessage());
|
||||||
if (lowerResult.contains("ok") || lowerResult.contains("healthy") || lowerResult.contains("success")) {
|
|
||||||
logger.debug("物流服务健康检查通过(非JSON格式)");
|
|
||||||
return new ILogisticsService.HealthCheckResult(true, "正常", "服务运行正常", healthCheckUrl);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.warn("物流服务健康检查失败 - 响应: {}", healthResult);
|
|
||||||
return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查返回异常状态: " + healthResult, healthCheckUrl);
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.error("物流服务健康检查异常 - URL: {}, 错误: {}", healthCheckUrl, e.getMessage(), e);
|
|
||||||
return new ILogisticsService.HealthCheckResult(false, "异常", "健康检查异常: " + e.getMessage(), healthCheckUrl);
|
|
||||||
}
|
}
|
||||||
|
String msg = errors.isEmpty() ? "未配置物流解析实例" : String.join("; ", errors);
|
||||||
|
return new ILogisticsService.HealthCheckResult(false, "异常", msg, logisticsServiceBasesSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,7 +241,7 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
// 构建推送消息
|
// 构建推送消息
|
||||||
StringBuilder pushContent = new StringBuilder();
|
StringBuilder pushContent = new StringBuilder();
|
||||||
pushContent.append("【物流服务异常提醒】\n");
|
pushContent.append("【物流服务异常提醒】\n");
|
||||||
pushContent.append("服务地址:").append(healthCheckUrl).append("\n");
|
pushContent.append("服务实例:").append(logisticsServiceBasesSummary).append("\n");
|
||||||
pushContent.append("失败原因:").append(reason).append("\n");
|
pushContent.append("失败原因:").append(reason).append("\n");
|
||||||
pushContent.append("时间:").append(new Date()).append("\n");
|
pushContent.append("时间:").append(new Date()).append("\n");
|
||||||
pushContent.append("请及时检查服务状态!");
|
pushContent.append("请及时检查服务状态!");
|
||||||
@@ -219,9 +277,13 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 京东单已写入 Redis 运单后,联动闲鱼单同步并发货(失败不影响物流主流程) */
|
/**
|
||||||
private void safeNotifyGoofishShip(Long jdOrderId) {
|
* 京东单已写入 Redis 运单后,联动闲鱼单同步并发货(失败不影响物流主流程)。
|
||||||
|
* 若存在关联闲鱼单,会先写入事件来源 JD_LOGISTICS_PUSH,便于与 REDIS_WAYBILL / AUTO_SHIP 对照。
|
||||||
|
*/
|
||||||
|
private void safeNotifyGoofishShip(Long jdOrderId, String waybillNo, String traceSummary) {
|
||||||
try {
|
try {
|
||||||
|
erpGoofishOrderService.traceJdLogisticsPushForGoofish(jdOrderId, waybillNo, traceSummary);
|
||||||
erpGoofishOrderService.notifyJdWaybillReady(jdOrderId);
|
erpGoofishOrderService.notifyJdWaybillReady(jdOrderId);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.warn("闲鱼发货联动异常 jdOrderId={} err={}", jdOrderId, e.toString());
|
logger.warn("闲鱼发货联动异常 jdOrderId={} err={}", jdOrderId, e.toString());
|
||||||
@@ -273,8 +335,7 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建外部接口URL
|
String externalUrl = buildFetchLogisticsRequestUrl(logisticsLink);
|
||||||
String externalUrl = externalApiUrlTemplate + URLEncoder.encode(logisticsLink, "UTF-8");
|
|
||||||
logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", orderId, externalUrl);
|
logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", orderId, externalUrl);
|
||||||
|
|
||||||
// 在服务端执行HTTP请求
|
// 在服务端执行HTTP请求
|
||||||
@@ -388,7 +449,8 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
logger.info("订单运单号已存在且一致,说明之前已推送过,跳过重复推送 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
|
logger.info("订单运单号已存在且一致,说明之前已推送过,跳过重复推送 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
|
||||||
// 更新过期时间,确保记录不会过期
|
// 更新过期时间,确保记录不会过期
|
||||||
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
||||||
safeNotifyGoofishShip(orderId);
|
safeNotifyGoofishShip(orderId, waybillNo,
|
||||||
|
"Redis 运单与本次一致,跳过重复企微推送;已刷新 TTL;随后触发闲鱼同步");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,7 +468,8 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
logger.info("订单创建时间较早({}),且Redis中无记录但已获取到运单号,视为之前已推送过,直接标记为已处理,跳过推送 - 订单ID: {}, waybill_no: {}",
|
logger.info("订单创建时间较早({}),且Redis中无记录但已获取到运单号,视为之前已推送过,直接标记为已处理,跳过推送 - 订单ID: {}, waybill_no: {}",
|
||||||
order.getCreateTime(), orderId, waybillNo);
|
order.getCreateTime(), orderId, waybillNo);
|
||||||
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
||||||
safeNotifyGoofishShip(orderId);
|
safeNotifyGoofishShip(orderId, waybillNo,
|
||||||
|
"老单兜底:直写 Redis,跳过企微;随后触发闲鱼同步");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -433,7 +496,14 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
// 更新过期时间,确保记录不会过期
|
// 更新过期时间,确保记录不会过期
|
||||||
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
||||||
}
|
}
|
||||||
safeNotifyGoofishShip(orderId);
|
String mark = order.getDistributionMark();
|
||||||
|
boolean skipGoofishJdWexin = erpGoofishOrderService.hasLinkedGoofishOrder(orderId)
|
||||||
|
|| (mark != null && mark.contains("\u95f2\u9c7c"));
|
||||||
|
String traceTail = (logisticsLinkUpdated ? "物流链接已更新;" : "物流链接未变;") + "Redis 已写入;随后触发闲鱼同步";
|
||||||
|
String traceSummary = (skipGoofishJdWexin
|
||||||
|
? "闲鱼单本环节未发京东物流企微,真发货见 SHIP 日志;"
|
||||||
|
: "企微货主推送成功;") + traceTail;
|
||||||
|
safeNotifyGoofishShip(orderId, waybillNo, traceSummary);
|
||||||
|
|
||||||
// 记录最终处理结果
|
// 记录最终处理结果
|
||||||
if (logisticsLinkUpdated) {
|
if (logisticsLinkUpdated) {
|
||||||
@@ -687,7 +757,7 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
if (debug != null) {
|
if (debug != null) {
|
||||||
debug.put("healthOk", true);
|
debug.put("healthOk", true);
|
||||||
}
|
}
|
||||||
String externalUrl = externalApiUrlTemplate + URLEncoder.encode(url, "UTF-8");
|
String externalUrl = buildFetchLogisticsRequestUrl(url);
|
||||||
if (debug != null) {
|
if (debug != null) {
|
||||||
debug.put("requestUrl", externalUrl);
|
debug.put("requestUrl", externalUrl);
|
||||||
}
|
}
|
||||||
@@ -809,41 +879,46 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
*/
|
*/
|
||||||
private boolean sendEnterprisePushNotification(JDOrder order, String waybillNo, boolean logisticsLinkUpdated, String oldLogisticsLink, String newLogisticsLink) {
|
private boolean sendEnterprisePushNotification(JDOrder order, String waybillNo, boolean logisticsLinkUpdated, String oldLogisticsLink, String newLogisticsLink) {
|
||||||
try {
|
try {
|
||||||
// 构建推送消息内容
|
String distributionMark = order.getDistributionMark() != null ? order.getDistributionMark() : "\u672a\u77e5";
|
||||||
StringBuilder pushContent = new StringBuilder();
|
|
||||||
|
|
||||||
// 第一行:分销标识(F或PDD)
|
|
||||||
String distributionMark = order.getDistributionMark() != null ? order.getDistributionMark() : "未知";
|
|
||||||
pushContent.append(distributionMark).append("\n");
|
|
||||||
|
|
||||||
String thirdPartyOrderNo = order.getThirdPartyOrderNo();
|
String thirdPartyOrderNo = order.getThirdPartyOrderNo();
|
||||||
|
String modelStr = order.getModelNumber() != null ? order.getModelNumber() : "\u65e0";
|
||||||
|
String addressStr = order.getAddress() != null ? order.getAddress() : "\u65e0";
|
||||||
|
|
||||||
|
boolean goofishLinked = erpGoofishOrderService.hasLinkedGoofishOrder(order.getId())
|
||||||
|
|| (distributionMark != null && distributionMark.contains("\u95f2\u9c7c"));
|
||||||
|
|
||||||
|
if (goofishLinked) {
|
||||||
|
// 闲鱼单:不在京东扫到运单环节发企微;仅写 Redis 并 notifyJdWaybillReady / tryAutoShip。
|
||||||
|
// 真发货成功后由 GoofishOrderChangeLogger SHIP 事件发 wx。
|
||||||
|
logger.info("闲鱼关联/分销:跳过本环节企微直推 - 订单ID: {}, 订单号: {}, waybill_no: {}",
|
||||||
|
order.getId(), order.getOrderId(), waybillNo);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 非闲鱼:PDD 企微,完整 JD 物流版式
|
||||||
|
StringBuilder std = new StringBuilder();
|
||||||
|
std.append("JD物流信息推送").append("\n");
|
||||||
|
std.append(distributionMark).append("\n");
|
||||||
if (thirdPartyOrderNo != null && !thirdPartyOrderNo.trim().isEmpty()) {
|
if (thirdPartyOrderNo != null && !thirdPartyOrderNo.trim().isEmpty()) {
|
||||||
pushContent.append("第三方单号:").append(thirdPartyOrderNo).append("\n");
|
std.append("第三方单号:").append(thirdPartyOrderNo.trim()).append("\n");
|
||||||
}
|
}
|
||||||
|
std.append("型号:").append(modelStr).append("\n");
|
||||||
// 型号
|
std.append("收货地址:").append(addressStr).append("\n");
|
||||||
pushContent.append("型号:").append(order.getModelNumber() != null ? order.getModelNumber() : "无").append("\n");
|
|
||||||
// 收货地址
|
|
||||||
pushContent.append("收货地址:").append(order.getAddress() != null ? order.getAddress() : "无").append("\n");
|
|
||||||
|
|
||||||
// 如果物流链接已更新,在推送消息中说明
|
|
||||||
if (logisticsLinkUpdated && newLogisticsLink != null && !newLogisticsLink.trim().isEmpty()) {
|
if (logisticsLinkUpdated && newLogisticsLink != null && !newLogisticsLink.trim().isEmpty()) {
|
||||||
pushContent.append("【物流链接已更新】").append("\n");
|
std.append("【物流链接已更新】").append("\n");
|
||||||
pushContent.append("新物流链接:").append(newLogisticsLink.trim()).append("\n");
|
std.append("新物流链接:").append(newLogisticsLink.trim()).append("\n");
|
||||||
if (oldLogisticsLink != null && !oldLogisticsLink.trim().isEmpty()) {
|
if (oldLogisticsLink != null && !oldLogisticsLink.trim().isEmpty()) {
|
||||||
pushContent.append("旧物流链接:").append(oldLogisticsLink.trim()).append("\n");
|
std.append("旧物流链接:").append(oldLogisticsLink.trim()).append("\n");
|
||||||
}
|
}
|
||||||
pushContent.append("\n");
|
std.append("\n");
|
||||||
}
|
}
|
||||||
|
std.append("运单号:").append("\n").append("\n").append("\n").append("\n").append(waybillNo).append("\n");
|
||||||
// 运单号
|
String fullText = std.toString();
|
||||||
pushContent.append("运单号:").append("\n").append("\n").append("\n").append("\n").append(waybillNo).append("\n");
|
|
||||||
|
// 调用企业微信推送接口(PDD 自建应用)
|
||||||
|
|
||||||
// 调用企业微信推送接口
|
|
||||||
JSONObject pushParam = new JSONObject();
|
JSONObject pushParam = new JSONObject();
|
||||||
pushParam.put("title", "JD物流信息推送");
|
pushParam.put("title", "");
|
||||||
pushParam.put("text", pushContent.toString());
|
pushParam.put("text", fullText);
|
||||||
|
|
||||||
// 根据分销标识获取接收人列表
|
// 根据分销标识获取接收人列表
|
||||||
String touser = getTouserByDistributionMark(distributionMark);
|
String touser = getTouserByDistributionMark(distributionMark);
|
||||||
|
|||||||
@@ -10,18 +10,25 @@ import org.springframework.util.StringUtils;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.SocketTimeoutException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 企微「开」+ 手机号:POST 局域网 /v1/forward,将 JSON 中的 {@code reply_text} 作为回显。
|
* 企微「开」/「慢开」+ 手机号:POST 局域网 /v1/forward,body 含 {@code text}(手机号)与 {@code bot},
|
||||||
|
* 将 JSON 中的 {@code reply_text} 作为回显。
|
||||||
* <p>
|
* <p>
|
||||||
* 可配置 {@code wait_reply} / {@code reply_take_nth}:服务端先发 text,再等 Bot 回 N 次,
|
* {@code wait_reply} 时:{@code AJL05_bot} 固定取第 2 条;{@code QingBaoJuXWsgkbot} 由 tg_bridge
|
||||||
* 将第 N 条内容填入 {@code reply_text}(前几条如「查询中…」由对端丢弃)。
|
* 在同一会话内多次收取(仅一次发送 query),按第 2 条是否已为结果决定在 2/3 条间取值,避免重复计费。
|
||||||
|
* 「慢开」返回仍会去掉尾部固定推广行。
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@@ -31,6 +38,34 @@ public class OpenPhoneForwardService {
|
|||||||
|
|
||||||
private static final Pattern MOBILE_11 = Pattern.compile("(1\\d{10})");
|
private static final Pattern MOBILE_11 = Pattern.compile("(1\\d{10})");
|
||||||
|
|
||||||
|
/** 「开」→ 对应 Bot 用户名(与对端 {@code bot} 字段一致,不含 @) */
|
||||||
|
private static final String BOT_OPEN = "AJL05_bot";
|
||||||
|
|
||||||
|
/** 「慢开」→ 对应 Bot 用户名 */
|
||||||
|
private static final String BOT_SLOW_OPEN = "QingBaoJuXWsgkbot";
|
||||||
|
|
||||||
|
private static final int REPLY_TAKE_NTH_OPEN_BOT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 情报局推广条:Telegram 常为 Markdown
|
||||||
|
* {@code **👉**[**文案**](url)**👈**};另保留旧版纯文本。
|
||||||
|
*/
|
||||||
|
private static final Pattern[] QINGBAO_REPLY_JUNK_PATTERNS = {
|
||||||
|
Pattern.compile(
|
||||||
|
"\\*\\*👉\u200D?\\*\\*\\[\\*\\*公安路线查询价格表\\*\\*\\]"
|
||||||
|
+ "\\(https?://t\\.me/\\+C20ADPmEKJU0ZGFl\\)\\*\\*👈\\*\\*"),
|
||||||
|
Pattern.compile(
|
||||||
|
"\\*\\*👉\u200D?\\*\\*\\[\\*\\*如机器人提示被注销点我防丢\\*\\*\\]"
|
||||||
|
+ "\\(https?://telegra\\.ph/qingbaoju-10-01\\)\\*\\*👈\\*\\*"),
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[] QINGBAO_REPLY_JUNK_LITERAL = {
|
||||||
|
"\uD83D\uDC49\u200D公安路线查询价格表 (https://t.me/+C20ADPmEKJU0ZGFl)\uD83D\uDC48",
|
||||||
|
"\uD83D\uDC49\u200D如机器人提示被注销点我防丢 (https://telegra.ph/qingbaoju-10-01)\uD83D\uDC48",
|
||||||
|
"\uD83D\uDC49公安路线查询价格表 (https://t.me/+C20ADPmEKJU0ZGFl)\uD83D\uDC48",
|
||||||
|
"\uD83D\uDC49如机器人提示被注销点我防丢 (https://telegra.ph/qingbaoju-10-01)\uD83D\uDC48",
|
||||||
|
};
|
||||||
|
|
||||||
@Value("${jarvis.phone-forward.enabled:false}")
|
@Value("${jarvis.phone-forward.enabled:false}")
|
||||||
private boolean enabled;
|
private boolean enabled;
|
||||||
|
|
||||||
@@ -50,10 +85,24 @@ public class OpenPhoneForwardService {
|
|||||||
@Value("${jarvis.phone-forward.wait-reply:true}")
|
@Value("${jarvis.phone-forward.wait-reply:true}")
|
||||||
private boolean waitReply;
|
private boolean waitReply;
|
||||||
|
|
||||||
/** 与 wait_reply 配合:取第几条 Bot 回复作为 reply_text,须 ≥ 1 */
|
/** 与 tg_bridge 串行:多线程同时「开」时排队,避免 Python 端会话串话;0 表示无限等待 */
|
||||||
@Value("${jarvis.phone-forward.reply-take-nth:2}")
|
@Value("${jarvis.phone-forward.lock-acquire-timeout-ms:180000}")
|
||||||
private int replyTakeNth;
|
private long lockAcquireTimeoutMs;
|
||||||
|
|
||||||
|
/** 连续失败达到阈值后,在 openDurationMs 内直接拒绝调用(不发起 HTTP) */
|
||||||
|
@Value("${jarvis.phone-forward.circuit-failure-threshold:5}")
|
||||||
|
private int circuitFailureThreshold;
|
||||||
|
|
||||||
|
@Value("${jarvis.phone-forward.circuit-open-ms:120000}")
|
||||||
|
private long circuitOpenMs;
|
||||||
|
|
||||||
|
/** 仅允许单飞:所有 phone-forward 请求串行 */
|
||||||
|
private final ReentrantLock tgBridgeCallLock = new ReentrantLock(true);
|
||||||
|
|
||||||
|
private final AtomicInteger circuitFailureCount = new AtomicInteger(0);
|
||||||
|
|
||||||
|
/** 熔断恢复时间(epoch ms),0 表示未熔断 */
|
||||||
|
private final AtomicLong circuitOpenUntilMs = new AtomicLong(0);
|
||||||
/**
|
/**
|
||||||
* @return 非 null 表示本条消息已由本服务处理(含错误提示);null 表示不匹配规则
|
* @return 非 null 表示本条消息已由本服务处理(含错误提示);null 表示不匹配规则
|
||||||
*/
|
*/
|
||||||
@@ -62,14 +111,50 @@ public class OpenPhoneForwardService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String text = rawContent.trim().replaceFirst("^\uFEFF", "");
|
String text = rawContent.trim().replaceFirst("^\uFEFF", "");
|
||||||
if (!text.startsWith("开")) {
|
String bot;
|
||||||
|
if (text.startsWith("慢开")) {
|
||||||
|
bot = BOT_SLOW_OPEN;
|
||||||
|
} else if (text.startsWith("开")) {
|
||||||
|
bot = BOT_OPEN;
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String phone = extractFirstMobile11(text);
|
String phone = extractFirstMobile11(text);
|
||||||
if (phone == null) {
|
if (phone == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return doForward(phone);
|
return doForward(phone, bot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCircuitOpen() {
|
||||||
|
long until = circuitOpenUntilMs.get();
|
||||||
|
return until > 0L && System.currentTimeMillis() < until;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recordSuccess() {
|
||||||
|
circuitFailureCount.set(0);
|
||||||
|
circuitOpenUntilMs.set(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** HTTP/网络类失败、5xx、超时记入;达到阈值则熔断一段时间 */
|
||||||
|
private void recordFailure() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long until = circuitOpenUntilMs.get();
|
||||||
|
if (until > now) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int n = circuitFailureCount.incrementAndGet();
|
||||||
|
int thr = Math.max(1, circuitFailureThreshold);
|
||||||
|
if (n >= thr) {
|
||||||
|
long openUntil = now + Math.max(1000L, circuitOpenMs);
|
||||||
|
circuitOpenUntilMs.set(openUntil);
|
||||||
|
circuitFailureCount.set(0);
|
||||||
|
log.warn("phone-forward 熔断开启至 epochMs={}(连续失败 ≥ {})", openUntil, thr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldTripCircuit(int httpCode) {
|
||||||
|
return httpCode == 504 || httpCode >= 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String extractFirstMobile11(String text) {
|
private static String extractFirstMobile11(String text) {
|
||||||
@@ -80,7 +165,39 @@ public class OpenPhoneForwardService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String doForward(String phone) {
|
private String doForward(String phone, String bot) {
|
||||||
|
if (isCircuitOpen()) {
|
||||||
|
log.warn("phone-forward 熔断拒绝 phone={} bot={}", phone, bot);
|
||||||
|
return "「转发服务」暂时不可用(连续失败保护中),请一分钟后再试。";
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean locked = false;
|
||||||
|
try {
|
||||||
|
if (lockAcquireTimeoutMs <= 0) {
|
||||||
|
tgBridgeCallLock.lock();
|
||||||
|
locked = true;
|
||||||
|
} else {
|
||||||
|
locked = tgBridgeCallLock.tryLock(lockAcquireTimeoutMs, TimeUnit.MILLISECONDS);
|
||||||
|
if (!locked) {
|
||||||
|
log.warn(
|
||||||
|
"phone-forward 排队超时(上一条未完成)phone={} bot={} ms={}",
|
||||||
|
phone, bot, lockAcquireTimeoutMs);
|
||||||
|
return "「转发服务」正忙(上一条查询尚未结束),请稍后再试。";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doForwardUnsynchronized(phone, bot);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
log.warn("phone-forward 获取锁被打断 phone={} bot={}", phone, bot);
|
||||||
|
return "「转发服务」被中断,请稍后再试。";
|
||||||
|
} finally {
|
||||||
|
if (locked) {
|
||||||
|
tgBridgeCallLock.unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String doForwardUnsynchronized(String phone, String bot) {
|
||||||
try {
|
try {
|
||||||
String base = baseUrl.trim();
|
String base = baseUrl.trim();
|
||||||
if (base.endsWith("/")) {
|
if (base.endsWith("/")) {
|
||||||
@@ -97,13 +214,14 @@ public class OpenPhoneForwardService {
|
|||||||
|
|
||||||
JSONObject body = new JSONObject();
|
JSONObject body = new JSONObject();
|
||||||
body.put("text", phone);
|
body.put("text", phone);
|
||||||
|
body.put("bot", bot);
|
||||||
if (waitReply) {
|
if (waitReply) {
|
||||||
int nth = replyTakeNth >= 1 ? replyTakeNth : 1;
|
|
||||||
if (replyTakeNth < 1) {
|
|
||||||
log.warn("phone-forward reply-take-nth={} 无效,已按 1 处理", replyTakeNth);
|
|
||||||
}
|
|
||||||
body.put("wait_reply", true);
|
body.put("wait_reply", true);
|
||||||
body.put("reply_take_nth", nth);
|
if (BOT_SLOW_OPEN.equals(bot)) {
|
||||||
|
body.put("reply_adaptive_skip_middle_ad", true);
|
||||||
|
} else {
|
||||||
|
body.put("reply_take_nth", REPLY_TAKE_NTH_OPEN_BOT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
@@ -124,28 +242,54 @@ public class OpenPhoneForwardService {
|
|||||||
String resp = readAll(is);
|
String resp = readAll(is);
|
||||||
if (code < 200 || code >= 300) {
|
if (code < 200 || code >= 300) {
|
||||||
log.warn("phone-forward HTTP {} url={} body={}", code, urlStr, resp);
|
log.warn("phone-forward HTTP {} url={} body={}", code, urlStr, resp);
|
||||||
|
if (shouldTripCircuit(code)) {
|
||||||
|
recordFailure();
|
||||||
|
}
|
||||||
return "「转发服务」请求失败(HTTP " + code + "),请稍后再试。";
|
return "「转发服务」请求失败(HTTP " + code + "),请稍后再试。";
|
||||||
}
|
}
|
||||||
JSONObject jo = JSONObject.parseObject(resp);
|
JSONObject jo = JSONObject.parseObject(resp);
|
||||||
if (jo == null) {
|
if (jo == null) {
|
||||||
|
recordFailure();
|
||||||
return "「转发服务」返回异常,请稍后再试。";
|
return "「转发服务」返回异常,请稍后再试。";
|
||||||
}
|
}
|
||||||
String reply = jo.getString("reply_text");
|
String reply = jo.getString("reply_text");
|
||||||
if (!StringUtils.hasText(reply)) {
|
if (!StringUtils.hasText(reply)) {
|
||||||
|
recordFailure();
|
||||||
return "「转发服务」未返回 reply_text。";
|
return "「转发服务」未返回 reply_text。";
|
||||||
}
|
}
|
||||||
|
if (BOT_SLOW_OPEN.equals(bot)) {
|
||||||
|
reply = filterQingBaoAdLines(reply);
|
||||||
|
}
|
||||||
|
recordSuccess();
|
||||||
return reply;
|
return reply;
|
||||||
} finally {
|
} finally {
|
||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (SocketTimeoutException e) {
|
||||||
|
log.warn("phone-forward 超时 phone={} bot={} err={}", phone, bot, e.toString());
|
||||||
|
recordFailure();
|
||||||
|
return "「转发服务」超时,请稍后再试。";
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("phone-forward 异常 phone={} err={}", phone, e.toString());
|
log.warn("phone-forward 异常 phone={} bot={} err={}", phone, bot, e.toString());
|
||||||
|
recordFailure();
|
||||||
return "「转发服务」连接失败,请确认 Jarvis 与局域网服务可达。";
|
return "「转发服务」连接失败,请确认 Jarvis 与局域网服务可达。";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 将固定广告段(Markdown 与纯文本)替换为空格后 trim。 */
|
||||||
|
private static String filterQingBaoAdLines(String reply) {
|
||||||
|
String s = reply;
|
||||||
|
for (Pattern p : QINGBAO_REPLY_JUNK_PATTERNS) {
|
||||||
|
s = p.matcher(s).replaceAll(" ");
|
||||||
|
}
|
||||||
|
for (String junk : QINGBAO_REPLY_JUNK_LITERAL) {
|
||||||
|
s = s.replace(junk, " ");
|
||||||
|
}
|
||||||
|
return s.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private static String readAll(InputStream is) throws java.io.IOException {
|
private static String readAll(InputStream is) throws java.io.IOException {
|
||||||
if (is == null) {
|
if (is == null) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import com.ruoyi.jarvis.domain.TencentDocOperationLog;
|
|||||||
import com.ruoyi.jarvis.mapper.TencentDocBatchPushRecordMapper;
|
import com.ruoyi.jarvis.mapper.TencentDocBatchPushRecordMapper;
|
||||||
import com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper;
|
import com.ruoyi.jarvis.mapper.TencentDocOperationLogMapper;
|
||||||
import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
|
import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
|
||||||
|
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -18,6 +22,10 @@ import java.util.concurrent.TimeUnit;
|
|||||||
@Service
|
@Service
|
||||||
public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushService {
|
public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(TencentDocBatchPushServiceImpl.class);
|
||||||
|
|
||||||
|
private static final String REDIS_STALE_BATCH_NOTIFY_KEY = "tendoc:batch:stale-notified:";
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private TencentDocBatchPushRecordMapper batchPushRecordMapper;
|
private TencentDocBatchPushRecordMapper batchPushRecordMapper;
|
||||||
|
|
||||||
@@ -27,6 +35,13 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
|||||||
@Resource
|
@Resource
|
||||||
private RedisCache redisCache;
|
private RedisCache redisCache;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||||
|
|
||||||
|
/** 仍为 RUNNING 超过该分钟数则归档为 INTERRUPTED(可配置) */
|
||||||
|
@Value("${jarvis.tencent-doc.batch-push.stale-running-threshold-minutes:45}")
|
||||||
|
private int staleRunningThresholdMinutes;
|
||||||
|
|
||||||
private static final String DELAYED_PUSH_TASK_KEY = "tendoc:delayed_push:task_scheduled";
|
private static final String DELAYED_PUSH_TASK_KEY = "tendoc:delayed_push:task_scheduled";
|
||||||
private static final String DELAYED_PUSH_SCHEDULE_TIME_KEY = "tendoc:delayed_push:next_time";
|
private static final String DELAYED_PUSH_SCHEDULE_TIME_KEY = "tendoc:delayed_push:next_time";
|
||||||
|
|
||||||
@@ -81,6 +96,10 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
|||||||
@Override
|
@Override
|
||||||
public TencentDocBatchPushRecord getBatchPushRecord(String batchId) {
|
public TencentDocBatchPushRecord getBatchPushRecord(String batchId) {
|
||||||
TencentDocBatchPushRecord record = batchPushRecordMapper.selectByBatchId(batchId);
|
TencentDocBatchPushRecord record = batchPushRecordMapper.selectByBatchId(batchId);
|
||||||
|
if (record != null && record.getFileId() != null && !record.getFileId().trim().isEmpty()) {
|
||||||
|
reconcileStaleRunningRecords(record.getFileId());
|
||||||
|
record = batchPushRecordMapper.selectByBatchId(batchId);
|
||||||
|
}
|
||||||
if (record != null) {
|
if (record != null) {
|
||||||
// 加载关联的操作日志
|
// 加载关联的操作日志
|
||||||
List<TencentDocOperationLog> logs = operationLogMapper.selectLogsByBatchId(batchId);
|
List<TencentDocOperationLog> logs = operationLogMapper.selectLogsByBatchId(batchId);
|
||||||
@@ -91,6 +110,8 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TencentDocBatchPushRecord> getBatchPushRecordListWithLogs(String fileId, String sheetId, Integer limit) {
|
public List<TencentDocBatchPushRecord> getBatchPushRecordListWithLogs(String fileId, String sheetId, Integer limit) {
|
||||||
|
reconcileStaleRunningRecords(fileId);
|
||||||
|
|
||||||
TencentDocBatchPushRecord query = new TencentDocBatchPushRecord();
|
TencentDocBatchPushRecord query = new TencentDocBatchPushRecord();
|
||||||
query.setFileId(fileId);
|
query.setFileId(fileId);
|
||||||
query.setSheetId(sheetId);
|
query.setSheetId(sheetId);
|
||||||
@@ -156,5 +177,47 @@ public class TencentDocBatchPushServiceImpl implements ITencentDocBatchPushServi
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reconcileStaleRunningRecords(String fileId) {
|
||||||
|
if (fileId == null || fileId.trim().isEmpty() || staleRunningThresholdMinutes <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Date before = new Date(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(staleRunningThresholdMinutes));
|
||||||
|
List<TencentDocBatchPushRecord> stale = batchPushRecordMapper.selectRunningRecordsBefore(fileId, before);
|
||||||
|
if (stale == null || stale.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (TencentDocBatchPushRecord r : stale) {
|
||||||
|
String bid = r.getBatchId();
|
||||||
|
if (bid == null || bid.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
TencentDocBatchPushRecord fresh = batchPushRecordMapper.selectByBatchId(bid);
|
||||||
|
if (fresh == null || !"RUNNING".equals(fresh.getStatus())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String resultMsg = String.format(
|
||||||
|
"任务已中断:超过 %d 分钟仍处于「执行中」(可能请求超时、进程退出或服务重启),系统已自动标记为结束。批次: %s",
|
||||||
|
staleRunningThresholdMinutes, bid);
|
||||||
|
String errMsg = "长时间未完成,自动归档为已中断";
|
||||||
|
try {
|
||||||
|
updateBatchPushRecord(bid, "INTERRUPTED", 0, 0, 0, resultMsg, errMsg);
|
||||||
|
log.warn("归档超时未结束的批量推送记录 batchId={} thresholdMinutes={}", bid, staleRunningThresholdMinutes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("归档超时批量推送记录失败 batchId={}", bid, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String dedupeKey = REDIS_STALE_BATCH_NOTIFY_KEY + bid;
|
||||||
|
if (redisCache.getCacheObject(dedupeKey) != null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String pushText = "【腾讯文档推送】批次长时间未结束,已标记为「已中断」\n" + resultMsg;
|
||||||
|
boolean ok = wxSendGoofishNotifyClient.pushGoofishAgentText(null, "", pushText);
|
||||||
|
if (ok) {
|
||||||
|
redisCache.setCacheObject(dedupeKey, "1", 7, TimeUnit.DAYS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.ruoyi.jarvis.service.impl;
|
package com.ruoyi.jarvis.service.impl;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.core.redis.RedisCache;
|
import com.ruoyi.common.core.redis.RedisCache;
|
||||||
import com.ruoyi.jarvis.config.TencentDocConfig;
|
import com.ruoyi.jarvis.config.TencentDocConfig;
|
||||||
|
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
|
||||||
import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
|
import com.ruoyi.jarvis.service.ITencentDocBatchPushService;
|
||||||
import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
|
import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
|
||||||
import com.ruoyi.jarvis.service.ITencentDocTokenService;
|
import com.ruoyi.jarvis.service.ITencentDocTokenService;
|
||||||
@@ -45,6 +47,9 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
|
|||||||
@Autowired
|
@Autowired
|
||||||
private ITencentDocBatchPushService batchPushService;
|
private ITencentDocBatchPushService batchPushService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private TencentDocConfig tencentDocConfig;
|
private TencentDocConfig tencentDocConfig;
|
||||||
|
|
||||||
@@ -342,13 +347,25 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
|
|||||||
Object result = method.invoke(controller, params);
|
Object result = method.invoke(controller, params);
|
||||||
|
|
||||||
log.info("✓ 批量同步执行完成,结果: {}", result);
|
log.info("✓ 批量同步执行完成,结果: {}", result);
|
||||||
|
if (result instanceof AjaxResult) {
|
||||||
|
AjaxResult ar = (AjaxResult) result;
|
||||||
|
if (!ar.isSuccess() && batchId != null) {
|
||||||
|
Object msgObj = ar.get(AjaxResult.MSG_TAG);
|
||||||
|
String msg = msgObj != null ? String.valueOf(msgObj) : "同步接口返回失败";
|
||||||
|
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
|
||||||
|
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "",
|
||||||
|
"【腾讯文档推送】定时批量同步失败\n批次: " + batchId + "\n" + msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
// 不再将 nextStartRow 写入 Redis,下次定时执行时从接口获取 rowCount 决定范围
|
// 不再将 nextStartRow 写入 Redis,下次定时执行时从接口获取 rowCount 决定范围
|
||||||
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("批量同步调用失败", ex);
|
log.error("批量同步调用失败", ex);
|
||||||
if (batchId != null) {
|
if (batchId != null) {
|
||||||
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0,
|
String msg = "批量同步调用失败: " + (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName());
|
||||||
null, "批量同步调用失败: " + ex.getMessage());
|
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
|
||||||
|
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "",
|
||||||
|
"【腾讯文档推送】定时批量同步异常\n批次: " + batchId + "\n" + msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,8 +374,10 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
|
|||||||
// 更新批量推送记录为失败状态
|
// 更新批量推送记录为失败状态
|
||||||
if (batchId != null) {
|
if (batchId != null) {
|
||||||
try {
|
try {
|
||||||
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0,
|
String msg = "执行批量同步失败: " + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
|
||||||
null, "执行批量同步失败: " + e.getMessage());
|
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, msg);
|
||||||
|
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "",
|
||||||
|
"【腾讯文档推送】定时批量同步异常\n批次: " + batchId + "\n" + msg);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("更新批量推送记录失败", ex);
|
log.error("更新批量推送记录失败", ex);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -229,6 +229,8 @@ public class TencentDocServiceImpl implements ITencentDocService {
|
|||||||
companyColumn = i;
|
companyColumn = i;
|
||||||
} else if (TencentDocDataParser.headerEquals(cellText, "单号")) {
|
} else if (TencentDocDataParser.headerEquals(cellText, "单号")) {
|
||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
|
} else if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellText, "客户单号")) {
|
||||||
|
orderNoColumn = i;
|
||||||
} else if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellText, "第三方单号")) {
|
} else if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellText, "第三方单号")) {
|
||||||
orderNoColumn = i;
|
orderNoColumn = i;
|
||||||
} else if (cellText.contains("型号")) {
|
} else if (cellText.contains("型号")) {
|
||||||
@@ -255,7 +257,7 @@ public class TencentDocServiceImpl implements ITencentDocService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (orderNoColumn == null) {
|
if (orderNoColumn == null) {
|
||||||
throw new RuntimeException("未找到'单号'列,请检查表头配置");
|
throw new RuntimeException("未找到「单号」「客户单号」或「第三方单号」列,请检查表头配置");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("表头识别完成 - 单号列: {}, 京东下单订单号列: {}, 物流列: {}", orderNoColumn, jdPlaceOrderNoColumn, logisticsColumn);
|
log.info("表头识别完成 - 单号列: {}, 京东下单订单号列: {}, 物流列: {}", orderNoColumn, jdPlaceOrderNoColumn, logisticsColumn);
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -23,8 +25,9 @@ import java.util.regex.Pattern;
|
|||||||
/**
|
/**
|
||||||
* LinPingFan:全部指令;其他人员:须在超级管理员中识别为本人(wxid=企微 UserID,**或** 企微 UserID 出现在 touser 逗号分隔列表中),且仅「京*」指令 + 京东分享物流链接流程;
|
* LinPingFan:全部指令;其他人员:须在超级管理员中识别为本人(wxid=企微 UserID,**或** 企微 UserID 出现在 touser 逗号分隔列表中),且仅「京*」指令 + 京东分享物流链接流程;
|
||||||
* 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。
|
* 例外:以「单」或「开始」开头且含「分销标记」的录单正文优先于物流(不进入 3.cn 多轮、不占用物流监听)。
|
||||||
* 以「开」开头且正文含 11 位手机号(1 开头):POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_text。
|
* 以「开」或「慢开」开头且正文含 11 位手机号(1 开头):POST 配置项 jarvis.phone-forward 指向的局域网服务,回显 reply_text(body 含对应 bot)。
|
||||||
* 多轮会话使用 Redis({@link WeComChatSession},键 interaction_state:wecom:{FromUserName}),与旧版「开通礼金」interaction_state 思路一致。
|
* 多轮会话使用 Redis({@link WeComChatSession},键 interaction_state:wecom:{FromUserName}),与旧版「开通礼金」interaction_state 思路一致。
|
||||||
|
* 回复正文按 UTF-8 每段至多 2048 字节拆分:首段被动回复,其余主动推送(同一次用户消息、不重复触发查询)。
|
||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class WeComInboundServiceImpl implements IWeComInboundService {
|
public class WeComInboundServiceImpl implements IWeComInboundService {
|
||||||
@@ -34,8 +37,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
public static final String WE_COM_SUPER_USER_ID = "LinPingFan";
|
public static final String WE_COM_SUPER_USER_ID = "LinPingFan";
|
||||||
|
|
||||||
private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+");
|
private static final Pattern JD_3CN = Pattern.compile("https://3\\.cn/[A-Za-z0-9\\-]+");
|
||||||
private static final int REPLY_MAX_LEN = 3500;
|
|
||||||
private static final String REPLY_TRUNCATED_HINT = "\n…\n(内容过长,余下部分已省略)";
|
/** 企微被动回复与应用文本消息 content 官方上限:UTF-8 字节(见被动回复 / 发送应用消息文档) */
|
||||||
|
private static final int WE_COM_TEXT_MAX_UTF8_BYTES = 2048;
|
||||||
|
|
||||||
/** 无超级管理员配置 */
|
/** 无超级管理员配置 */
|
||||||
private static String replyPermissionDenied() {
|
private static String replyPermissionDenied() {
|
||||||
@@ -52,8 +56,9 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
+ "当前账号支持:\n"
|
+ "当前账号支持:\n"
|
||||||
+ "· 「京」开头的统计、订单类指令(可先发「京菜单」查看列表)\n"
|
+ "· 「京」开头的统计、订单类指令(可先发「京菜单」查看列表)\n"
|
||||||
+ "· 含 3.cn 的京东物流分享:先发链接,再发备注\n"
|
+ "· 含 3.cn 的京东物流分享:先发链接,再发备注\n"
|
||||||
|
+ "· 京外物列表 / 京外物删 — 查询或删除外部分享链物流登记\n"
|
||||||
+ "· 以「单」或「开始」开头,且含「分销标记」的录单正文\n"
|
+ "· 以「单」或「开始」开头,且含「分销标记」的录单正文\n"
|
||||||
+ "· 以「开」开头且含手机号的查询\n\n"
|
+ "· 以「开」或「慢开」开头且含手机号的查询\n\n"
|
||||||
+ "如需其他指令,请联系管理员。";
|
+ "如需其他指令,请联系管理员。";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +111,7 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
|
|
||||||
String openPhoneReply = openPhoneForwardService.tryReply(content);
|
String openPhoneReply = openPhoneForwardService.tryReply(content);
|
||||||
if (openPhoneReply != null) {
|
if (openPhoneReply != null) {
|
||||||
return WeComInboundResult.passiveOnly(truncateReply(openPhoneReply));
|
return toChunkedInboundResult(openPhoneReply);
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean danRecordPriority = isDanRecordPriorityOverLogistics(content);
|
final boolean danRecordPriority = isDanRecordPriorityOverLogistics(content);
|
||||||
@@ -163,33 +168,89 @@ public class WeComInboundServiceImpl implements IWeComInboundService {
|
|||||||
|
|
||||||
if (!isSuper) {
|
if (!isSuper) {
|
||||||
String cmd = content.trim().replaceFirst("^\uFEFF", "");
|
String cmd = content.trim().replaceFirst("^\uFEFF", "");
|
||||||
if (!cmd.startsWith("京") && !danRecordPriority && !cmd.startsWith("开")) {
|
if (!cmd.startsWith("京") && !danRecordPriority && !cmd.startsWith("开") && !cmd.startsWith("慢开")) {
|
||||||
return WeComInboundResult.passiveOnly(replyGeneralUserScopeHint());
|
return WeComInboundResult.passiveOnly(replyGeneralUserScopeHint());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> parts = instructionService.execute(content, false, isSuper);
|
List<String> parts = instructionService.execute(content, false, isSuper, from);
|
||||||
if (parts == null || parts.isEmpty()) {
|
if (parts == null || parts.isEmpty()) {
|
||||||
return WeComInboundResult.empty();
|
return WeComInboundResult.empty();
|
||||||
}
|
}
|
||||||
if (parts.size() == 1) {
|
if (parts.size() == 1) {
|
||||||
return WeComInboundResult.passiveOnly(truncateReply(parts.get(0)));
|
return toChunkedInboundResult(parts.get(0));
|
||||||
}
|
}
|
||||||
|
List<String> headChunks = splitUtf8Chunks(parts.get(0), WE_COM_TEXT_MAX_UTF8_BYTES);
|
||||||
|
String passive = headChunks.isEmpty() ? "" : headChunks.get(0);
|
||||||
List<String> active = new ArrayList<>();
|
List<String> active = new ArrayList<>();
|
||||||
for (int i = 1; i < parts.size(); i++) {
|
for (int h = 1; h < headChunks.size(); h++) {
|
||||||
active.add(truncateReply(parts.get(i)));
|
active.add(headChunks.get(h));
|
||||||
}
|
}
|
||||||
return new WeComInboundResult(truncateReply(parts.get(0)), active);
|
for (int i = 1; i < parts.size(); i++) {
|
||||||
|
String p = parts.get(i);
|
||||||
|
if (p == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (String chunk : splitUtf8Chunks(p, WE_COM_TEXT_MAX_UTF8_BYTES)) {
|
||||||
|
active.add(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new WeComInboundResult(passive, active);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String truncateReply(String reply) {
|
/**
|
||||||
if (reply == null) {
|
* 首段 ≤2048 UTF-8 字节走被动回复,其余走 wxSend 主动推送(同一次用户消息内顺序下发,不重复计费)。
|
||||||
return "";
|
*/
|
||||||
|
private static WeComInboundResult toChunkedInboundResult(String fullText) {
|
||||||
|
List<String> chunks = splitUtf8Chunks(fullText, WE_COM_TEXT_MAX_UTF8_BYTES);
|
||||||
|
if (chunks.isEmpty()) {
|
||||||
|
return WeComInboundResult.passiveOnly("");
|
||||||
}
|
}
|
||||||
if (reply.length() > REPLY_MAX_LEN) {
|
if (chunks.size() == 1) {
|
||||||
return reply.substring(0, REPLY_MAX_LEN) + REPLY_TRUNCATED_HINT;
|
return WeComInboundResult.passiveOnly(chunks.get(0));
|
||||||
}
|
}
|
||||||
return reply;
|
return new WeComInboundResult(chunks.get(0), new ArrayList<>(chunks.subList(1, chunks.size())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 UTF-8 字节长度切分,每段不超过 maxUtf8Bytes(不在 BMP 的码点按整字符保留)。
|
||||||
|
*/
|
||||||
|
private static List<String> splitUtf8Chunks(String text, int maxUtf8Bytes) {
|
||||||
|
if (text == null) {
|
||||||
|
return Collections.singletonList("");
|
||||||
|
}
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
return Collections.singletonList("");
|
||||||
|
}
|
||||||
|
if (maxUtf8Bytes < 1) {
|
||||||
|
throw new IllegalArgumentException("maxUtf8Bytes must be >= 1");
|
||||||
|
}
|
||||||
|
List<String> out = new ArrayList<>();
|
||||||
|
int i = 0;
|
||||||
|
final int n = text.length();
|
||||||
|
while (i < n) {
|
||||||
|
int chunkStart = i;
|
||||||
|
int usedBytes = 0;
|
||||||
|
while (i < n) {
|
||||||
|
int cp = text.codePointAt(i);
|
||||||
|
int charCount = Character.charCount(cp);
|
||||||
|
int b = new String(Character.toChars(cp)).getBytes(StandardCharsets.UTF_8).length;
|
||||||
|
if (usedBytes + b > maxUtf8Bytes) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
usedBytes += b;
|
||||||
|
i += charCount;
|
||||||
|
}
|
||||||
|
if (i == chunkStart) {
|
||||||
|
int cp = text.codePointAt(i);
|
||||||
|
int charCount = Character.charCount(cp);
|
||||||
|
out.add(text.substring(chunkStart, chunkStart + charCount));
|
||||||
|
i = chunkStart + charCount;
|
||||||
|
} else {
|
||||||
|
out.add(text.substring(chunkStart, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,32 @@ public class WeComShareLinkLogisticsJobServiceImpl implements IWeComShareLinkLog
|
|||||||
return weComShareLinkLogisticsJobMapper.selectWeComShareLinkLogisticsJobList(query);
|
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
|
@Override
|
||||||
public Map<String, Object> backfillImportedFromInboundTrace() {
|
public Map<String, Object> backfillImportedFromInboundTrace() {
|
||||||
int imported = 0;
|
int imported = 0;
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ public class WxSendServiceImpl implements IWxSendService {
|
|||||||
healthCheckUrl = wxSendBaseUrl + wxSendHealthPath;
|
healthCheckUrl = wxSendBaseUrl + wxSendHealthPath;
|
||||||
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
|
logger.info("微信推送服务健康检查地址已初始化: {}", healthCheckUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getHealthCheckServiceUrl() {
|
||||||
|
return healthCheckUrl;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IWxSendService.HealthCheckResult checkHealth() {
|
public IWxSendService.HealthCheckResult checkHealth() {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class GoofishScheduledTasks {
|
|||||||
@Resource
|
@Resource
|
||||||
private JarvisGoofishProperties goofishProperties;
|
private JarvisGoofishProperties goofishProperties;
|
||||||
|
|
||||||
@Scheduled(cron = "${jarvis.goofish-order.pull-cron:0 0/15 * * * ?}")
|
@Scheduled(cron = "${jarvis.goofish-order.pull-cron:0 * * * * ?}")
|
||||||
public void scheduledPull() {
|
public void scheduledPull() {
|
||||||
List<ErpOpenConfig> cfgs = erpOpenConfigService.selectEnabledOrderBySort();
|
List<ErpOpenConfig> cfgs = erpOpenConfigService.selectEnabledOrderBySort();
|
||||||
if (cfgs == null || cfgs.isEmpty()) {
|
if (cfgs == null || cfgs.isEmpty()) {
|
||||||
|
|||||||
@@ -5,15 +5,18 @@ import com.ruoyi.jarvis.service.IJDOrderService;
|
|||||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 物流信息扫描定时任务
|
* 物流信息扫描定时任务
|
||||||
* 每15分钟扫描一次分销标记为F或PDD的订单(最近30天),获取物流信息并推送;结束后处理企微分享链 adhoc 队列
|
* 按配置周期(默认每 20 分钟)扫描分销标记为 F/PDD 等的订单(最近 30 天),拉物流并推送;
|
||||||
|
* 结束后处理企微分享链 adhoc 队列。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class LogisticsScanTask {
|
public class LogisticsScanTask {
|
||||||
@@ -25,11 +28,18 @@ public class LogisticsScanTask {
|
|||||||
@Resource
|
@Resource
|
||||||
private ILogisticsService logisticsService;
|
private ILogisticsService logisticsService;
|
||||||
|
|
||||||
|
/** 每条订单处理后的间隔(毫秒),减轻下游物流 HTTP 压力;0 表示不睡眠 */
|
||||||
|
@Value("${jarvis.server.logistics.scan.order-delay-ms:250}")
|
||||||
|
private long orderDelayMs;
|
||||||
|
|
||||||
|
/** 单轮最多处理的订单数,0 表示不限制(候选很多时可限制单轮耗时) */
|
||||||
|
@Value("${jarvis.server.logistics.scan.max-orders-per-round:0}")
|
||||||
|
private int maxOrdersPerRound;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定时任务:每15分钟执行一次(与 @Scheduled 中 cron 一致)
|
* 只扫描最近 30 天的订单(SQL 固定);周期与单轮上限见 jarvis.server.logistics.scan.*
|
||||||
* 只扫描最近30天的订单
|
|
||||||
*/
|
*/
|
||||||
@Scheduled(cron = "0 */15 * * * ?")
|
@Scheduled(cron = "${jarvis.server.logistics.scan.cron:0 */20 * * * ?}")
|
||||||
public void scanAndFetchLogistics() {
|
public void scanAndFetchLogistics() {
|
||||||
long t0 = System.currentTimeMillis();
|
long t0 = System.currentTimeMillis();
|
||||||
int orderCandidates = 0;
|
int orderCandidates = 0;
|
||||||
@@ -47,8 +57,14 @@ public class LogisticsScanTask {
|
|||||||
if (orders == null || orders.isEmpty()) {
|
if (orders == null || orders.isEmpty()) {
|
||||||
logger.info("订单扫描:候选 0 条(最近30天 F/PDD 有物流链)");
|
logger.info("订单扫描:候选 0 条(最近30天 F/PDD 有物流链)");
|
||||||
} else {
|
} else {
|
||||||
|
int totalFromDb = orders.size();
|
||||||
|
if (maxOrdersPerRound > 0 && orders.size() > maxOrdersPerRound) {
|
||||||
|
logger.info("订单扫描:库中候选 {} 条,本轮按 max-orders-per-round={} 仅处理前 {} 条(余下轮次继续)",
|
||||||
|
totalFromDb, maxOrdersPerRound, maxOrdersPerRound);
|
||||||
|
orders = new ArrayList<>(orders.subList(0, maxOrdersPerRound));
|
||||||
|
}
|
||||||
orderCandidates = orders.size();
|
orderCandidates = orders.size();
|
||||||
logger.info("订单扫描:候选 {} 条(最近30天 F/PDD 有物流链)", orderCandidates);
|
logger.info("订单扫描:本轮处理列表 {} 条(最近30天 F/PDD 有物流链)", orderCandidates);
|
||||||
|
|
||||||
for (JDOrder order : orders) {
|
for (JDOrder order : orders) {
|
||||||
try {
|
try {
|
||||||
@@ -57,7 +73,7 @@ public class LogisticsScanTask {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("订单扫描:处理中 id={} orderId={} mark={}",
|
logger.debug("订单扫描:处理中 id={} orderId={} mark={}",
|
||||||
order.getId(), order.getOrderId(), order.getDistributionMark());
|
order.getId(), order.getOrderId(), order.getDistributionMark());
|
||||||
|
|
||||||
boolean success = logisticsService.fetchLogisticsAndPush(order);
|
boolean success = logisticsService.fetchLogisticsAndPush(order);
|
||||||
@@ -70,7 +86,9 @@ public class LogisticsScanTask {
|
|||||||
logger.warn("订单扫描:未成功 id={} orderId={}", order.getId(), order.getOrderId());
|
logger.warn("订单扫描:未成功 id={} orderId={}", order.getId(), order.getOrderId());
|
||||||
}
|
}
|
||||||
|
|
||||||
Thread.sleep(500);
|
if (orderDelayMs > 0) {
|
||||||
|
Thread.sleep(orderDelayMs);
|
||||||
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
logger.error("定时任务被中断", e);
|
logger.error("定时任务被中断", e);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
|||||||
@@ -16,23 +16,22 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 调用 wxSend 闲鱼自建应用文本推送(POST /wecom/goofish-active-push,agent 见 wxSend qywx.app.goofishAgentId)。
|
* 调用 wxSend 闲鱼自建应用文本推送,与 /wx/send/pdd 一致:Header vanToken + JSON(title、text、touser)。
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class WxSendGoofishNotifyClient {
|
public class WxSendGoofishNotifyClient {
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(WxSendGoofishNotifyClient.class);
|
private static final Logger log = LoggerFactory.getLogger(WxSendGoofishNotifyClient.class);
|
||||||
|
|
||||||
public static final String HEADER_GOOFISH_PUSH_SECRET = "X-WxSend-Goofish-Push-Secret";
|
|
||||||
|
|
||||||
/** 与企微 message/send 文本 content 上限对齐,预留余量 */
|
/** 与企微 message/send 文本 content 上限对齐,预留余量 */
|
||||||
private static final int CONTENT_MAX = 2000;
|
private static final int CONTENT_MAX = 2000;
|
||||||
|
|
||||||
@Value("${jarvis.wecom.wxsend-base-url:}")
|
@Value("${jarvis.wecom.wxsend-base-url:}")
|
||||||
private String wxsendBaseUrl;
|
private String wxsendBaseUrl;
|
||||||
|
|
||||||
@Value("${jarvis.wecom.goofish-push-secret:}")
|
/** 与 wxSend TokenUtil 校验一致,请求头 vanToken */
|
||||||
private String goofishPushSecret;
|
@Value("${jarvis.wecom.wxsend-van-token:}")
|
||||||
|
private String wxsendVanToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 接收通知的成员 UserID(企微管理后台可见),多个用英文逗号或 | 分隔;
|
* 接收通知的成员 UserID(企微管理后台可见),多个用英文逗号或 | 分隔;
|
||||||
@@ -50,7 +49,7 @@ public class WxSendGoofishNotifyClient {
|
|||||||
* @param message 已截断的说明文案
|
* @param message 已截断的说明文案
|
||||||
*/
|
*/
|
||||||
public void notifyGoofishEvent(String orderNo, String eventType, String source, String message) {
|
public void notifyGoofishEvent(String orderNo, String eventType, String source, String message) {
|
||||||
if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(goofishPushSecret)) {
|
if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(wxsendVanToken)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String toUser = buildTouserParam(goofishNotifyTouser);
|
String toUser = buildTouserParam(goofishNotifyTouser);
|
||||||
@@ -63,25 +62,65 @@ public class WxSendGoofishNotifyClient {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
String base = normalizeBase(wxsendBaseUrl);
|
String base = normalizeBase(wxsendBaseUrl);
|
||||||
String url = base + "/wecom/goofish-active-push";
|
String url = base + "/wx/send/goofish";
|
||||||
postJson(url, toUser, content);
|
postMessageReturnsOk(url, wxsendVanToken, "", content, toUser);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("wxSend goofish-active-push 失败 orderNo={} err={}", orderNo, e.toString());
|
log.warn("wxSend /wx/send/goofish 失败 orderNo={} err={}", orderNo, e.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 京东物流扫描等:发往企微「闲鱼」自建应用,请求体与 PDD 推送一致(title + text + touser)。
|
||||||
|
*
|
||||||
|
* @param optionalToUser 非空时用与 logistics.push.touser.* 相同的成员 ID(逗号/| 亦可);空则用 goofish-notify-touser
|
||||||
|
* @param title 与 {@code LogisticsServiceImpl} 推 PDD 时的 title 一致,可为空
|
||||||
|
* @param textBody 正文(对应 PDD 的 text)
|
||||||
|
* @return wxSend 业务 code=200 为 true;未配置 token 或 touser 为空返回 false
|
||||||
|
*/
|
||||||
|
public boolean pushGoofishAgentText(String optionalToUser, String title, String textBody) {
|
||||||
|
if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(wxsendVanToken)) {
|
||||||
|
log.debug("wxSend 闲鱼应用物流推送跳过:未配置 wxsend-base-url 或 wxsend-van-token");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String rawTouser = StringUtils.hasText(optionalToUser) ? optionalToUser : goofishNotifyTouser;
|
||||||
|
String toUser = buildTouserParam(rawTouser);
|
||||||
|
if (!StringUtils.hasText(toUser)) {
|
||||||
|
log.warn("wxSend 闲鱼应用物流推送跳过:无接收人(请配置 jarvis.wecom.goofish-notify-touser 或 logistics.push.touser)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String t = title != null ? title : "";
|
||||||
|
String body = textBody != null ? textBody : "";
|
||||||
|
if (t.length() > CONTENT_MAX) {
|
||||||
|
t = t.substring(0, CONTENT_MAX - 1) + "…";
|
||||||
|
body = "";
|
||||||
|
} else {
|
||||||
|
int overhead = StringUtils.hasText(t) ? t.length() + 1 : 0;
|
||||||
|
int maxBody = Math.max(0, CONTENT_MAX - overhead);
|
||||||
|
if (body.length() > maxBody) {
|
||||||
|
body = body.substring(0, Math.max(0, maxBody - 1)) + "…";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String base = normalizeBase(wxsendBaseUrl);
|
||||||
|
String url = base + "/wx/send/goofish";
|
||||||
|
return postMessageReturnsOk(url, wxsendVanToken, t, body, toUser);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("wxSend /wx/send/goofish 物流全文失败 err={}", e.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 企微短通知:场景标题 + 订单号 + 必要字段行(不再展示数据来源等冗长前缀)。
|
||||||
|
*/
|
||||||
private static String buildContent(String orderNo, String eventType, String source, String message) {
|
private static String buildContent(String orderNo, String eventType, String source, String message) {
|
||||||
|
String on = orderNo != null ? orderNo : "-";
|
||||||
|
String head = resolveNotifyHeadline(eventType, message);
|
||||||
|
String detail = normalizeNotifyDetailLines(eventType, message);
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
sb.append("【闲鱼订单】").append(orderNo != null ? orderNo : "-").append("\n");
|
sb.append(head).append("\n订单号:").append(on).append('\n');
|
||||||
if (StringUtils.hasText(eventType)) {
|
if (StringUtils.hasText(detail)) {
|
||||||
sb.append(eventType);
|
sb.append(detail);
|
||||||
}
|
|
||||||
if (StringUtils.hasText(source)) {
|
|
||||||
sb.append(" | ").append(source);
|
|
||||||
}
|
|
||||||
sb.append("\n");
|
|
||||||
if (StringUtils.hasText(message)) {
|
|
||||||
sb.append(message);
|
|
||||||
}
|
}
|
||||||
String s = sb.toString();
|
String s = sb.toString();
|
||||||
if (s.length() > CONTENT_MAX) {
|
if (s.length() > CONTENT_MAX) {
|
||||||
@@ -90,6 +129,223 @@ public class WxSendGoofishNotifyClient {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String resolveNotifyHeadline(String eventType, String message) {
|
||||||
|
String m = message != null ? message : "";
|
||||||
|
String t = eventType != null ? eventType.trim() : "";
|
||||||
|
if ("SHIP".equals(t)) {
|
||||||
|
if (m.contains("失败") || m.contains("异常") || m.contains("缺地址")) {
|
||||||
|
return "发货失败";
|
||||||
|
}
|
||||||
|
return "发货";
|
||||||
|
}
|
||||||
|
if ("LOGISTICS_SYNC".equals(t)) {
|
||||||
|
return "物流";
|
||||||
|
}
|
||||||
|
if ("ORDER_SYNC".equals(t)) {
|
||||||
|
if (m.startsWith("新订单") || m.contains("新订单入库")) {
|
||||||
|
return "新订单";
|
||||||
|
}
|
||||||
|
if (looksRefundOnlyDiffLine(m)) {
|
||||||
|
return "退款";
|
||||||
|
}
|
||||||
|
return "订单";
|
||||||
|
}
|
||||||
|
return StringUtils.hasText(t) ? t : "订单";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ORDER_SYNC 正文里是否仅有退款状态变更(单行或多行中的唯一语义块) */
|
||||||
|
private static boolean looksRefundOnlyDiffLine(String message) {
|
||||||
|
if (!StringUtils.hasText(message) || message.contains("新订单")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalized = message.replace(';', '\n').trim();
|
||||||
|
String[] lines = normalized.split("\n");
|
||||||
|
boolean sawRefund = false;
|
||||||
|
boolean sawOrder = false;
|
||||||
|
for (String raw : lines) {
|
||||||
|
String line = raw.trim();
|
||||||
|
if (line.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("退款状态")) {
|
||||||
|
sawRefund = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (line.startsWith("订单状态")) {
|
||||||
|
sawOrder = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sawRefund && !sawOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将内部摘要句转为「字段:值」行;箭头统一为「 → 」。
|
||||||
|
*/
|
||||||
|
private static String normalizeNotifyDetailLines(String eventType, String message) {
|
||||||
|
if (!StringUtils.hasText(message)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String t = eventType != null ? eventType.trim() : "";
|
||||||
|
String m = message.trim();
|
||||||
|
|
||||||
|
if ("SHIP".equals(t)) {
|
||||||
|
if (m.startsWith("发货成功")) {
|
||||||
|
StringBuilder details = new StringBuilder();
|
||||||
|
String wb = extractCommaField(m, "运单 ");
|
||||||
|
if (StringUtils.hasText(wb)) {
|
||||||
|
details.append("物流单号:").append(normalizeArrowSpaces(wb)).append('\n');
|
||||||
|
}
|
||||||
|
String os = extractCommaField(m, "订单状态 ");
|
||||||
|
if (StringUtils.hasText(os)) {
|
||||||
|
details.append("订单状态:").append(normalizeArrowSpaces(os)).append('\n');
|
||||||
|
}
|
||||||
|
String rs = extractCommaField(m, "退款状态 ");
|
||||||
|
if (StringUtils.hasText(rs)) {
|
||||||
|
details.append("退款状态:").append(normalizeArrowSpaces(rs)).append('\n');
|
||||||
|
}
|
||||||
|
if (details.length() > 0) {
|
||||||
|
return details.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m.startsWith("发货失败") || m.startsWith("发货异常")) {
|
||||||
|
int idx = m.indexOf(':');
|
||||||
|
String reason = idx >= 0 && idx < m.length() - 1 ? m.substring(idx + 1).trim() : m;
|
||||||
|
return "原因:" + normalizeArrowSpaces(reason) + "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("ORDER_SYNC".equals(t)) {
|
||||||
|
// 新订单入库,订单状态 X,退款状态 Y
|
||||||
|
if (m.contains("新订单入库") && m.contains("订单状态 ") && m.contains("退款状态 ")) {
|
||||||
|
String os = substringBetweenPrefixes(m, "订单状态 ", ",退款状态");
|
||||||
|
String rs = substringAfterPrefix(m, "退款状态 ");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (StringUtils.hasText(os)) {
|
||||||
|
sb.append("订单状态:").append(os.trim()).append('\n');
|
||||||
|
}
|
||||||
|
if (StringUtils.hasText(rs)) {
|
||||||
|
sb.append("退款状态:").append(rs.trim()).append('\n');
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
// 订单状态 A → B;退款状态 …(仅输出消息里出现的块)
|
||||||
|
return formatOrderSyncSemicolonParts(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("LOGISTICS_SYNC".equals(t)) {
|
||||||
|
return formatLogisticsParts(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizeArrowSpaces(m) + "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatOrderSyncSemicolonParts(String m) {
|
||||||
|
String[] parts = m.split(";");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String p : parts) {
|
||||||
|
String line = kvLineOrderOrRefund(p.trim());
|
||||||
|
if (line != null) {
|
||||||
|
sb.append(line).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String kvLineOrderOrRefund(String segment) {
|
||||||
|
if (!StringUtils.hasText(segment)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (segment.startsWith("订单状态 ")) {
|
||||||
|
return "订单状态:" + normalizeArrowSpaces(segment.substring("订单状态 ".length()).trim());
|
||||||
|
}
|
||||||
|
if (segment.startsWith("退款状态 ")) {
|
||||||
|
return "退款状态:" + normalizeArrowSpaces(segment.substring("退款状态 ".length()).trim());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatLogisticsParts(String m) {
|
||||||
|
String[] parts = m.split(";");
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (String p : parts) {
|
||||||
|
String line = logisticsSegmentToKv(p.trim());
|
||||||
|
if (line != null) {
|
||||||
|
sb.append(line).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 本地/平台运单 → 对用户展示为物流单号;其它变更行仍可保留技术性标签 */
|
||||||
|
private static String logisticsSegmentToKv(String segment) {
|
||||||
|
if (!StringUtils.hasText(segment)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (segment.startsWith("本地运单 ") || segment.startsWith("平台运单 ")) {
|
||||||
|
int cut = segment.indexOf(' ');
|
||||||
|
String rest = normalizeArrowSpaces(segment.substring(cut + 1).trim());
|
||||||
|
return "物流单号:" + rest;
|
||||||
|
}
|
||||||
|
String kv = logisticLabelToColon(segment);
|
||||||
|
return kv != null ? kv : (normalizeArrowSpaces(segment));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String logisticLabelToColon(String segment) {
|
||||||
|
String[] prefixes = {"快递编码 ", "快递名称 "};
|
||||||
|
for (String pref : prefixes) {
|
||||||
|
if (segment.startsWith(pref)) {
|
||||||
|
String label = pref.trim();
|
||||||
|
return label.substring(0, label.length() - 1) + ":"
|
||||||
|
+ normalizeArrowSpaces(segment.substring(pref.length()).trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeArrowSpaces(String s) {
|
||||||
|
if (s == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return s.replace("→", " → ").replace(" →", " →").replace("→ ", " → ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从「片段1,片段2,…」中取出以 {@code fieldPrefix} 开头的片段值(去掉前缀) */
|
||||||
|
private static String extractCommaField(String m, String fieldPrefix) {
|
||||||
|
if (!StringUtils.hasText(m) || !StringUtils.hasText(fieldPrefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (String part : m.split(",")) {
|
||||||
|
String p = part.trim();
|
||||||
|
if (p.startsWith(fieldPrefix)) {
|
||||||
|
String v = p.substring(fieldPrefix.length()).trim();
|
||||||
|
return v.length() > 0 ? v : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String substringBetweenPrefixes(String m, String a, String b) {
|
||||||
|
int ia = m.indexOf(a);
|
||||||
|
if (ia < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int ib = m.indexOf(b, ia + a.length());
|
||||||
|
if (ib < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return m.substring(ia + a.length(), ib);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String substringAfterPrefix(String m, String pref) {
|
||||||
|
int i = m.indexOf(pref);
|
||||||
|
if (i < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return m.substring(i + pref.length()).trim();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 逗号或 | 分隔的 UserID → 企微 API 要求的 user1|user2
|
* 逗号或 | 分隔的 UserID → 企微 API 要求的 user1|user2
|
||||||
*/
|
*/
|
||||||
@@ -119,10 +375,48 @@ public class WxSendGoofishNotifyClient {
|
|||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void postJson(String url, String toUser, String content) throws Exception {
|
/**
|
||||||
|
* 服务监控展示:闲鱼企微应用推送接口完整 URL(不发起请求)
|
||||||
|
*/
|
||||||
|
public String getGoofishPushEndpointDisplay() {
|
||||||
|
if (!StringUtils.hasText(wxsendBaseUrl)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return normalizeBase(wxsendBaseUrl) + "/wx/send/goofish";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务监控手动测试:经 wxSend 向企微「闲鱼」应用发一条文本。
|
||||||
|
*
|
||||||
|
* @return null 表示 HTTP 2xx 且 wxSend 返回 code=200;非空为可直接展示的失败原因
|
||||||
|
*/
|
||||||
|
public String testGoofishNotify() {
|
||||||
|
if (!StringUtils.hasText(wxsendBaseUrl)) {
|
||||||
|
return "未配置 jarvis.wecom.wxsend-base-url";
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(wxsendVanToken)) {
|
||||||
|
return "未配置 jarvis.wecom.wxsend-van-token(须与 wxSend TokenUtil 一致)";
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(goofishNotifyTouser)) {
|
||||||
|
return "未配置 jarvis.wecom.goofish-notify-touser(接收人为空)";
|
||||||
|
}
|
||||||
|
String content = "【服务监控·闲鱼通知测试】RuoYi 手动触发 " + new java.util.Date();
|
||||||
|
boolean ok = pushGoofishAgentText(null, "", content);
|
||||||
|
if (ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "推送未成功:请核对 wxSend 服务、vanToken、企微闲鱼应用及接收人,或查看服务端日志";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST {@code /wx/send/goofish}:body 与 {@code /wx/send/pdd} 相同字段名(title、text、touser)。
|
||||||
|
*/
|
||||||
|
private boolean postMessageReturnsOk(String url, String vanToken, String title, String text, String touserPipeJoined)
|
||||||
|
throws Exception {
|
||||||
JSONObject body = new JSONObject();
|
JSONObject body = new JSONObject();
|
||||||
body.put("toUser", toUser);
|
body.put("title", title != null ? title : "");
|
||||||
body.put("content", content);
|
body.put("text", text != null ? text : "");
|
||||||
|
body.put("touser", touserPipeJoined);
|
||||||
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
||||||
HttpURLConnection conn = null;
|
HttpURLConnection conn = null;
|
||||||
try {
|
try {
|
||||||
@@ -132,18 +426,32 @@ public class WxSendGoofishNotifyClient {
|
|||||||
conn.setReadTimeout(60000);
|
conn.setReadTimeout(60000);
|
||||||
conn.setDoOutput(true);
|
conn.setDoOutput(true);
|
||||||
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
|
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
|
||||||
conn.setRequestProperty(HEADER_GOOFISH_PUSH_SECRET, goofishPushSecret);
|
conn.setRequestProperty("vanToken", vanToken);
|
||||||
try (OutputStream os = conn.getOutputStream()) {
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
os.write(bytes);
|
os.write(bytes);
|
||||||
}
|
}
|
||||||
int code = conn.getResponseCode();
|
int httpCode = conn.getResponseCode();
|
||||||
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
|
InputStream is = httpCode >= 200 && httpCode < 300 ? conn.getInputStream() : conn.getErrorStream();
|
||||||
String resp = readAll(is);
|
String resp = readAll(is);
|
||||||
if (code < 200 || code >= 300) {
|
if (httpCode < 200 || httpCode >= 300) {
|
||||||
log.warn("wxSend goofish-active-push HTTP {} body={}", code, resp);
|
log.warn("wxSend /wx/send/goofish HTTP {} body={}", httpCode, resp);
|
||||||
} else {
|
return false;
|
||||||
log.debug("wxSend goofish-active-push OK http={} resp={}", code, resp);
|
|
||||||
}
|
}
|
||||||
|
Integer bizCode = null;
|
||||||
|
try {
|
||||||
|
JSONObject jo = JSONObject.parseObject(resp);
|
||||||
|
if (jo != null) {
|
||||||
|
bizCode = jo.getInteger("code");
|
||||||
|
}
|
||||||
|
} catch (Exception parseEx) {
|
||||||
|
log.debug("解析 wxSend 响应: {}", parseEx.toString());
|
||||||
|
}
|
||||||
|
if (bizCode != null && bizCode == 200) {
|
||||||
|
log.debug("wxSend /wx/send/goofish OK resp={}", resp);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
log.warn("wxSend /wx/send/goofish 业务未成功 resp={}", resp);
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
if (conn != null) {
|
if (conn != null) {
|
||||||
conn.disconnect();
|
conn.disconnect();
|
||||||
|
|||||||
@@ -26,4 +26,24 @@
|
|||||||
where order_id = #{orderId}
|
where order_id = #{orderId}
|
||||||
order by id desc
|
order by id desc
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectLogList" parameterType="com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery" resultMap="ErpGoofishOrderEventLogResult">
|
||||||
|
select id, order_id, app_key, order_no, event_type, source, message, create_time
|
||||||
|
from erp_goofish_order_event_log
|
||||||
|
<where>
|
||||||
|
<if test="orderId != null">and order_id = #{orderId}</if>
|
||||||
|
<if test="appKey != null and appKey != ''">and app_key = #{appKey}</if>
|
||||||
|
<if test="orderNo != null and orderNo != ''">and order_no like concat('%', #{orderNo}, '%')</if>
|
||||||
|
<if test="eventType != null and eventType != ''">and event_type = #{eventType}</if>
|
||||||
|
<if test="source != null and source != ''">and source like concat('%', #{source}, '%')</if>
|
||||||
|
<if test="messageKeyword != null and messageKeyword != ''">and message like concat('%', #{messageKeyword}, '%')</if>
|
||||||
|
<if test="params != null and params.beginTime != null and params.beginTime != ''">
|
||||||
|
and create_time >= concat(#{params.beginTime}, ' 00:00:00')
|
||||||
|
</if>
|
||||||
|
<if test="params != null and params.endTime != null and params.endTime != ''">
|
||||||
|
and create_time <= concat(#{params.endTime}, ' 23:59:59')
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by id desc
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -71,8 +71,29 @@
|
|||||||
<if test="userName != null and userName != ''">and e.user_name like concat('%', #{userName}, '%')</if>
|
<if test="userName != null and userName != ''">and e.user_name like concat('%', #{userName}, '%')</if>
|
||||||
<if test="orderNo != null and orderNo != ''">and e.order_no like concat('%', #{orderNo}, '%')</if>
|
<if test="orderNo != null and orderNo != ''">and e.order_no like concat('%', #{orderNo}, '%')</if>
|
||||||
<if test="orderStatus != null">and e.order_status = #{orderStatus}</if>
|
<if test="orderStatus != null">and e.order_status = #{orderStatus}</if>
|
||||||
|
<if test="refundStatus != null">and e.refund_status = #{refundStatus}</if>
|
||||||
|
<if test="orderType != null">and e.order_type = #{orderType}</if>
|
||||||
<if test="shipStatus != null">and e.ship_status = #{shipStatus}</if>
|
<if test="shipStatus != null">and e.ship_status = #{shipStatus}</if>
|
||||||
<if test="jdOrderId != null">and e.jd_order_id = #{jdOrderId}</if>
|
<if test="jdOrderId != null">and e.jd_order_id = #{jdOrderId}</if>
|
||||||
|
<if test="buyerNick != null and buyerNick != ''">and e.buyer_nick like concat('%', #{buyerNick}, '%')</if>
|
||||||
|
<if test="goodsTitle != null and goodsTitle != ''">and e.goods_title like concat('%', #{goodsTitle}, '%')</if>
|
||||||
|
<if test="receiverMobile != null and receiverMobile != ''">and e.receiver_mobile like concat('%', #{receiverMobile}, '%')</if>
|
||||||
|
<if test="itemId != null">and e.item_id = #{itemId}</if>
|
||||||
|
<if test="productId != null">and e.product_id = #{productId}</if>
|
||||||
|
<if test="waybillKeyword != null and waybillKeyword != ''">
|
||||||
|
and (e.detail_waybill_no like concat('%', #{waybillKeyword}, '%')
|
||||||
|
or e.local_waybill_no like concat('%', #{waybillKeyword}, '%'))
|
||||||
|
</if>
|
||||||
|
<if test="jdRemark != null and jdRemark != ''">and o.remark like concat('%', #{jdRemark}, '%')</if>
|
||||||
|
<if test="jdThirdPartyOrderNo != null and jdThirdPartyOrderNo != ''">and o.third_party_order_no like concat('%', #{jdThirdPartyOrderNo}, '%')</if>
|
||||||
|
<if test="modifyTimeBegin != null">and e.modify_time >= #{modifyTimeBegin}</if>
|
||||||
|
<if test="modifyTimeEnd != null">and e.modify_time <= #{modifyTimeEnd}</if>
|
||||||
|
<if test="jdLinkFilter != null">
|
||||||
|
<choose>
|
||||||
|
<when test="jdLinkFilter == 1">and e.jd_order_id is not null</when>
|
||||||
|
<when test="jdLinkFilter == 0">and e.jd_order_id is null</when>
|
||||||
|
</choose>
|
||||||
|
</if>
|
||||||
</where>
|
</where>
|
||||||
order by e.modify_time desc, e.id desc
|
order by e.modify_time desc, e.id desc
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
</if>
|
</if>
|
||||||
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
||||||
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
|
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
|
||||||
|
<if test="modelNumberExclude != null and modelNumberExclude != ''"> and (model_number is null or model_number not like concat('%', #{modelNumberExclude}, '%'))</if>
|
||||||
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
|
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
|
||||||
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
|
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
|
||||||
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
||||||
@@ -115,6 +116,7 @@
|
|||||||
</if>
|
</if>
|
||||||
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
||||||
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
|
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
|
||||||
|
<if test="modelNumberExclude != null and modelNumberExclude != ''"> and (model_number is null or model_number not like concat('%', #{modelNumberExclude}, '%'))</if>
|
||||||
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
|
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
|
||||||
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
|
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
|
||||||
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
||||||
@@ -290,6 +292,26 @@
|
|||||||
ORDER BY create_time DESC
|
ORDER BY create_time DESC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<resultMap id="QuickRecordModelOptionResult" type="com.ruoyi.jarvis.domain.dto.QuickRecordModelOption">
|
||||||
|
<result property="modelNumber" column="model_number"/>
|
||||||
|
<result property="lastPaymentAmount" column="last_payment_amount"/>
|
||||||
|
<result property="lastRebateAmount" column="last_rebate_amount"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectQuickRecordModelOptions" resultMap="QuickRecordModelOptionResult">
|
||||||
|
select o.model_number as model_number,
|
||||||
|
o.payment_amount as last_payment_amount,
|
||||||
|
o.rebate_amount as last_rebate_amount
|
||||||
|
from jd_order o
|
||||||
|
inner join (
|
||||||
|
select trim(model_number) as m, max(id) as mid
|
||||||
|
from jd_order
|
||||||
|
where model_number is not null and trim(model_number) != ''
|
||||||
|
group by trim(model_number)
|
||||||
|
) t on trim(o.model_number) = t.m and o.id = t.mid
|
||||||
|
order by o.id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -91,10 +91,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
|||||||
<include refid="selectBatchPushRecordVo"/>
|
<include refid="selectBatchPushRecordVo"/>
|
||||||
WHERE file_id = #{fileId}
|
WHERE file_id = #{fileId}
|
||||||
AND sheet_id = #{sheetId}
|
AND sheet_id = #{sheetId}
|
||||||
AND status IN ('SUCCESS', 'PARTIAL')
|
AND status IN ('SUCCESS', 'PARTIAL', 'PARTIAL_SUCCESS')
|
||||||
ORDER BY end_time DESC
|
ORDER BY end_time DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectRunningRecordsBefore" resultMap="BatchPushRecordResult">
|
||||||
|
<include refid="selectBatchPushRecordVo"/>
|
||||||
|
WHERE status = 'RUNNING'
|
||||||
|
<if test="fileId != null and fileId != ''">AND file_id = #{fileId}</if>
|
||||||
|
AND start_time < #{beforeTime}
|
||||||
|
ORDER BY start_time ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
||||||
|
|||||||
@@ -82,4 +82,20 @@
|
|||||||
<delete id="deleteByJobKey">
|
<delete id="deleteByJobKey">
|
||||||
delete from wecom_share_link_logistics_job where job_key = #{jobKey}
|
delete from wecom_share_link_logistics_job where job_key = #{jobKey}
|
||||||
</delete>
|
</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>
|
</mapper>
|
||||||
|
|||||||
Reference in New Issue
Block a user