Compare commits
8
Commits
a169a554b9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdb9a5e099 | ||
|
|
e8e1f9e26f | ||
|
|
152e6712ca | ||
|
|
459540819d | ||
|
|
4e4def7f72 | ||
|
|
a5d60fa912 | ||
|
|
39c7e16f82 | ||
|
|
3b5081ce25 |
@@ -0,0 +1,288 @@
|
|||||||
|
package com.ruoyi.jarvis.task;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.ruoyi.common.utils.Arith;
|
||||||
|
import com.ruoyi.framework.web.domain.Server;
|
||||||
|
import com.ruoyi.framework.web.domain.server.SysFile;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按服务监控同一套采样({@link Server#copyTo()})定时看 CPU 与磁盘。
|
||||||
|
* 任一超过阈值时,经 wxSend {@code /wx/send}、消息类型 QL(企微应用 1000002)连发告警。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class ServerResourceAlertTask {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(ServerResourceAlertTask.class);
|
||||||
|
|
||||||
|
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
|
|
||||||
|
/** 内存盘、容器层等不代表物理硬盘,避免误报 */
|
||||||
|
private static final Set<String> PSEUDO_FS = new HashSet<>(Arrays.asList(
|
||||||
|
"tmpfs", "devtmpfs", "overlay", "squashfs", "ramfs", "sysfs", "proc",
|
||||||
|
"cgroup", "cgroup2", "devfs", "autofs", "nsfs", "tracefs", "debugfs",
|
||||||
|
"securityfs", "pstore", "bpf", "configfs", "mqueue", "hugetlbfs",
|
||||||
|
"fusectl", "binfmt_misc"));
|
||||||
|
|
||||||
|
@Value("${jarvis.server.resource-alert.enabled:true}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
/** 与服务监控页磁盘标红线一致,超过即告警 */
|
||||||
|
@Value("${jarvis.server.resource-alert.cpu-threshold:80}")
|
||||||
|
private double cpuThreshold;
|
||||||
|
|
||||||
|
@Value("${jarvis.server.resource-alert.disk-threshold:80}")
|
||||||
|
private double diskThreshold;
|
||||||
|
|
||||||
|
@Value("${jarvis.server.resource-alert.repeat-count:3}")
|
||||||
|
private int repeatCount;
|
||||||
|
|
||||||
|
/** 持续超标时两次连发之间的最短间隔,避免每轮刷新都刷屏 */
|
||||||
|
@Value("${jarvis.server.resource-alert.cooldown-minutes:30}")
|
||||||
|
private int cooldownMinutes;
|
||||||
|
|
||||||
|
@Value("${jarvis.server.resource-alert.touser:LinPingFan}")
|
||||||
|
private String touser;
|
||||||
|
|
||||||
|
/** wxSend 消息类型名,QL 对应企微应用 1000002 */
|
||||||
|
@Value("${jarvis.server.resource-alert.message-type:QL}")
|
||||||
|
private String messageType;
|
||||||
|
|
||||||
|
@Value("${jarvis.wecom.wxsend-base-url:}")
|
||||||
|
private String wxsendBaseUrl;
|
||||||
|
|
||||||
|
@Value("${jarvis.wecom.wxsend-van-token:}")
|
||||||
|
private String vanToken;
|
||||||
|
|
||||||
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private volatile long lastAlertAtMs;
|
||||||
|
|
||||||
|
@Scheduled(cron = "${jarvis.server.resource-alert.cron:0 */5 * * * ?}")
|
||||||
|
public void refreshAndAlert() {
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!running.compareAndSet(false, true)) {
|
||||||
|
log.info("服务器资源监控上一轮尚未结束,跳过本次");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
sampleAndMaybeAlert();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("服务器资源监控定时任务异常", e);
|
||||||
|
} finally {
|
||||||
|
running.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sampleAndMaybeAlert() throws Exception {
|
||||||
|
Server server = new Server();
|
||||||
|
server.copyTo();
|
||||||
|
|
||||||
|
double cpuFree = server.getCpu().getFree();
|
||||||
|
double cpuUsage = clampPercent(Arith.sub(100D, cpuFree));
|
||||||
|
boolean cpuHot = cpuUsage > cpuThreshold;
|
||||||
|
|
||||||
|
List<String> hotDisks = new ArrayList<>();
|
||||||
|
double maxDisk = -1D;
|
||||||
|
String maxDiskDir = "";
|
||||||
|
List<SysFile> files = server.getSysFiles();
|
||||||
|
if (files != null) {
|
||||||
|
for (SysFile file : files) {
|
||||||
|
if (file == null || !isPhysicalDisk(file)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
double usage = file.getUsage();
|
||||||
|
if (usage > maxDisk) {
|
||||||
|
maxDisk = usage;
|
||||||
|
maxDiskDir = file.getDirName() == null ? "" : file.getDirName();
|
||||||
|
}
|
||||||
|
if (usage > diskThreshold) {
|
||||||
|
hotDisks.add(formatDisk(file));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean diskHot = !hotDisks.isEmpty();
|
||||||
|
|
||||||
|
String diskSummary = maxDisk < 0 ? "无物理盘" : String.format(Locale.ROOT, "%.2f%%(%s)", maxDisk, maxDiskDir);
|
||||||
|
log.info("服务器资源监控 CPU={}%(阈值{}%) 磁盘最高={}(阈值{}%)",
|
||||||
|
formatPercent(cpuUsage), formatPercent(cpuThreshold), diskSummary, formatPercent(diskThreshold));
|
||||||
|
|
||||||
|
if (!cpuHot && !diskHot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
long cooldownMs = Math.max(1, cooldownMinutes) * 60_000L;
|
||||||
|
if (lastAlertAtMs > 0 && now - lastAlertAtMs < cooldownMs) {
|
||||||
|
log.info("资源仍超标,处于告警冷却({} 分钟),本次不重复推送", cooldownMinutes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String kind = cpuHot && diskHot ? "CPU与磁盘" : (cpuHot ? "CPU" : "磁盘");
|
||||||
|
String body = buildBody(server, cpuUsage, cpuHot, hotDisks, diskHot);
|
||||||
|
int times = Math.max(1, repeatCount);
|
||||||
|
int sent = 0;
|
||||||
|
for (int i = 1; i <= times; i++) {
|
||||||
|
String title = "【资源告警 " + i + "/" + times + "】" + kind + "过高";
|
||||||
|
if (postQl(title, body)) {
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
if (i < times) {
|
||||||
|
Thread.sleep(400L);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (sent > 0) {
|
||||||
|
lastAlertAtMs = System.currentTimeMillis();
|
||||||
|
log.warn("服务器资源告警已推送 {}/{} 条,渠道 messageType={}(企微应用 1000002)", sent, times, messageType);
|
||||||
|
} else {
|
||||||
|
log.error("服务器资源已超标,但 {} 条告警均未发出,下轮将重试", times);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildBody(Server server, double cpuUsage, boolean cpuHot, List<String> hotDisks, boolean diskHot) {
|
||||||
|
String name = server.getSys() == null ? "" : nullToEmpty(server.getSys().getComputerName());
|
||||||
|
String ip = server.getSys() == null ? "" : nullToEmpty(server.getSys().getComputerIp());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("主机:").append(name);
|
||||||
|
if (StringUtils.hasText(ip)) {
|
||||||
|
sb.append("(").append(ip).append(")");
|
||||||
|
}
|
||||||
|
sb.append("\n时间:").append(TIME_FMT.format(LocalDateTime.now()));
|
||||||
|
sb.append("\nCPU:").append(formatPercent(cpuUsage)).append("%");
|
||||||
|
sb.append(cpuHot ? ",已超过阈值 " : ",未超过阈值 ");
|
||||||
|
sb.append(formatPercent(cpuThreshold)).append("%");
|
||||||
|
if (diskHot) {
|
||||||
|
sb.append("\n磁盘超标:");
|
||||||
|
for (String line : hotDisks) {
|
||||||
|
sb.append("\n- ").append(line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sb.append("\n磁盘:未超过阈值 ").append(formatPercent(diskThreshold)).append("%");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean postQl(String title, String text) {
|
||||||
|
if (!StringUtils.hasText(wxsendBaseUrl)) {
|
||||||
|
log.error("资源告警未发送:未配置 jarvis.wecom.wxsend-base-url");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(vanToken)) {
|
||||||
|
log.error("资源告警未发送:未配置 jarvis.wecom.wxsend-van-token");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String base = wxsendBaseUrl.trim();
|
||||||
|
if (base.endsWith("/")) {
|
||||||
|
base = base.substring(0, base.length() - 1);
|
||||||
|
}
|
||||||
|
String url = base + "/wx/send";
|
||||||
|
JSONObject body = new JSONObject();
|
||||||
|
body.put("title", title);
|
||||||
|
body.put("text", text);
|
||||||
|
body.put("touser", touser == null ? "" : touser.trim());
|
||||||
|
body.put("vanToken", vanToken.trim());
|
||||||
|
body.put("messageType", StringUtils.hasText(messageType) ? messageType.trim() : "QL");
|
||||||
|
|
||||||
|
HttpURLConnection conn = null;
|
||||||
|
try {
|
||||||
|
conn = (HttpURLConnection) new URL(url).openConnection();
|
||||||
|
conn.setRequestMethod("POST");
|
||||||
|
conn.setConnectTimeout(10000);
|
||||||
|
conn.setReadTimeout(20000);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
|
||||||
|
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
||||||
|
try (OutputStream os = conn.getOutputStream()) {
|
||||||
|
os.write(bytes);
|
||||||
|
}
|
||||||
|
int code = conn.getResponseCode();
|
||||||
|
InputStream is = code >= 200 && code < 300 ? conn.getInputStream() : conn.getErrorStream();
|
||||||
|
String resp = readAll(is);
|
||||||
|
boolean ok = code >= 200 && code < 300 && resp != null && resp.contains("成功");
|
||||||
|
if (!ok) {
|
||||||
|
log.error("资源告警推送失败 http={} url={} resp={}", code, url, resp);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
log.info("资源告警已发出 title={} resp={}", title, resp);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("资源告警推送异常 url={} err={}", url, e.toString(), e);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
if (conn != null) {
|
||||||
|
conn.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isPhysicalDisk(SysFile file) {
|
||||||
|
String total = file.getTotal();
|
||||||
|
if (!StringUtils.hasText(total) || !total.toUpperCase(Locale.ROOT).contains("GB")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String fsType = file.getSysTypeName();
|
||||||
|
if (!StringUtils.hasText(fsType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !PSEUDO_FS.contains(fsType.trim().toLowerCase(Locale.ROOT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatDisk(SysFile file) {
|
||||||
|
return nullToEmpty(file.getDirName())
|
||||||
|
+ " 已用 " + formatPercent(file.getUsage()) + "%"
|
||||||
|
+ "(可用 " + nullToEmpty(file.getFree())
|
||||||
|
+ " / 共 " + nullToEmpty(file.getTotal()) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double clampPercent(double value) {
|
||||||
|
if (value < 0D) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
if (value > 100D) {
|
||||||
|
return 100D;
|
||||||
|
}
|
||||||
|
return Arith.round(value, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String formatPercent(double value) {
|
||||||
|
return String.format(Locale.ROOT, "%.2f", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String nullToEmpty(String value) {
|
||||||
|
return value == null ? "" : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String readAll(InputStream is) throws java.io.IOException {
|
||||||
|
if (is == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
byte[] buf = new byte[4096];
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
int n;
|
||||||
|
while ((n = is.read(buf)) >= 0) {
|
||||||
|
sb.append(new String(buf, 0, n, StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -448,9 +448,9 @@ public class TencentDocController extends BaseController {
|
|||||||
return AjaxResult.error("未找到订单信息");
|
return AjaxResult.error("未找到订单信息");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有物流链接
|
// 检查是否有物流链接或物流单号
|
||||||
if (order.getLogisticsLink() == null || order.getLogisticsLink().trim().isEmpty()) {
|
if (!order.hasLogisticsInfo()) {
|
||||||
return AjaxResult.error("订单缺少物流链接,无法自动发货");
|
return AjaxResult.error("订单缺少物流链接或物流单号,无法自动发货");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 上传物流信息到腾讯文档
|
// 上传物流信息到腾讯文档
|
||||||
@@ -1480,8 +1480,8 @@ public class TencentDocController extends BaseController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (order.getLogisticsLink() != null && !order.getLogisticsLink().trim().isEmpty()) {
|
if (order.hasLogisticsInfo()) {
|
||||||
String logisticsLink = order.getLogisticsLink().trim();
|
String logisticsLink = order.resolveLogisticsDocValue();
|
||||||
|
|
||||||
// 从status字段中提取手机号码
|
// 从status字段中提取手机号码
|
||||||
String phone = null;
|
String phone = null;
|
||||||
@@ -1526,14 +1526,14 @@ public class TencentDocController extends BaseController {
|
|||||||
order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 ? "是" : "否");
|
order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 ? "是" : "否");
|
||||||
} else {
|
} else {
|
||||||
errorCount++;
|
errorCount++;
|
||||||
log.warn("订单物流链接为空 - 单号: {}, 行号: {}", orderNo, excelRow);
|
log.warn("订单物流链接/单号为空 - 单号: {}, 行号: {}", orderNo, excelRow);
|
||||||
|
|
||||||
// 记录错误详情
|
// 记录错误详情
|
||||||
Map<String, Object> errorLog = new java.util.HashMap<>();
|
Map<String, Object> errorLog = new java.util.HashMap<>();
|
||||||
errorLog.put("orderNo", orderNo);
|
errorLog.put("orderNo", orderNo);
|
||||||
errorLog.put("row", excelRow);
|
errorLog.put("row", excelRow);
|
||||||
errorLog.put("errorType", "物流链接为空");
|
errorLog.put("errorType", "物流链接为空");
|
||||||
errorLog.put("errorMessage", "订单物流链接为空");
|
errorLog.put("errorMessage", "订单缺少物流链接或物流单号");
|
||||||
errorLogs.add(errorLog);
|
errorLogs.add(errorLog);
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -1760,8 +1760,8 @@ public class TencentDocController extends BaseController {
|
|||||||
log.info("✓ 先清除单元格内容 - 行: {}, 列: {}", row, logisticsLinkColumn);
|
log.info("✓ 先清除单元格内容 - 行: {}, 列: {}", row, logisticsLinkColumn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 更新物流单号(超链接类型)
|
// 1. 更新物流单号(链接写超链接,纯单号写文本)
|
||||||
requests.add(buildUpdateCellRequest(sheetId, row - 1, logisticsLinkColumn, logisticsLink, true));
|
requests.add(buildUpdateCellRequest(sheetId, row - 1, logisticsLinkColumn, logisticsLink, JDOrder.isHttpUrl(logisticsLink)));
|
||||||
|
|
||||||
// 2. 更新"下单电话"列(如果存在且提取到了手机号码)
|
// 2. 更新"下单电话"列(如果存在且提取到了手机号码)
|
||||||
String phone = update.getString("phone");
|
String phone = update.getString("phone");
|
||||||
|
|||||||
+15
-1
@@ -125,6 +125,7 @@ public class JDOrderListController extends BaseController
|
|||||||
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
|
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
|
||||||
query.getParams().put("orderSearch", orderSearch.trim());
|
query.getParams().put("orderSearch", orderSearch.trim());
|
||||||
}
|
}
|
||||||
|
applyMobileQuickSearch(query, request);
|
||||||
|
|
||||||
String quickRecordSearch = request.getParameter("quickRecordSearch");
|
String quickRecordSearch = request.getParameter("quickRecordSearch");
|
||||||
if (quickRecordSearch != null && !quickRecordSearch.trim().isEmpty()) {
|
if (quickRecordSearch != null && !quickRecordSearch.trim().isEmpty()) {
|
||||||
@@ -314,10 +315,11 @@ public class JDOrderListController extends BaseController
|
|||||||
*/
|
*/
|
||||||
@Log(title = "JD订单", businessType = BusinessType.EXPORT)
|
@Log(title = "JD订单", businessType = BusinessType.EXPORT)
|
||||||
@PostMapping("/export")
|
@PostMapping("/export")
|
||||||
public void export(HttpServletResponse response, JDOrder jdOrder) throws IOException
|
public void export(HttpServletResponse response, JDOrder jdOrder, HttpServletRequest request) throws IOException
|
||||||
{
|
{
|
||||||
String fileName = "JD订单数据";
|
String fileName = "JD订单数据";
|
||||||
|
|
||||||
|
applyMobileQuickSearch(jdOrder, request);
|
||||||
applyModelNumberExcludeFilter(jdOrder);
|
applyModelNumberExcludeFilter(jdOrder);
|
||||||
|
|
||||||
List<JDOrder> list = jdOrderService.selectJDOrderList(jdOrder);
|
List<JDOrder> list = jdOrderService.selectJDOrderList(jdOrder);
|
||||||
@@ -601,6 +603,7 @@ public class JDOrderListController extends BaseController
|
|||||||
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
|
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
|
||||||
query.getParams().put("orderSearch", orderSearch.trim());
|
query.getParams().put("orderSearch", orderSearch.trim());
|
||||||
}
|
}
|
||||||
|
applyMobileQuickSearch(query, request);
|
||||||
|
|
||||||
String rebateRemarkAbnormal = request.getParameter("rebateRemarkHasAbnormal");
|
String rebateRemarkAbnormal = request.getParameter("rebateRemarkHasAbnormal");
|
||||||
if (rebateRemarkAbnormal != null && !rebateRemarkAbnormal.isEmpty()) {
|
if (rebateRemarkAbnormal != null && !rebateRemarkAbnormal.isEmpty()) {
|
||||||
@@ -742,6 +745,17 @@ public class JDOrderListController extends BaseController
|
|||||||
/**
|
/**
|
||||||
* 型号不含:支持逗号、中文逗号、空格、换行分隔的多个关键词,型号不得包含任一关键词。
|
* 型号不含:支持逗号、中文逗号、空格、换行分隔的多个关键词,型号不得包含任一关键词。
|
||||||
*/
|
*/
|
||||||
|
/** 手机端快速搜索:备注、单号、下单人、地址任一命中即可 */
|
||||||
|
private void applyMobileQuickSearch(JDOrder query, HttpServletRequest request) {
|
||||||
|
if (query == null || request == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String mobileQuickSearch = request.getParameter("mobileQuickSearch");
|
||||||
|
if (mobileQuickSearch != null && !mobileQuickSearch.trim().isEmpty()) {
|
||||||
|
query.getParams().put("mobileQuickSearch", mobileQuickSearch.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void applyModelNumberExcludeFilter(JDOrder query) {
|
private void applyModelNumberExcludeFilter(JDOrder query) {
|
||||||
if (query == null) {
|
if (query == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -219,6 +219,16 @@ jarvis:
|
|||||||
order-delay-ms: 250
|
order-delay-ms: 250
|
||||||
# 0=不限制;例如 40 可控制单轮最长耗时(余下下轮再扫)
|
# 0=不限制;例如 40 可控制单轮最长耗时(余下下轮再扫)
|
||||||
max-orders-per-round: 0
|
max-orders-per-round: 0
|
||||||
|
# 服务监控定时刷新:CPU 或物理盘使用率超过阈值时,经企微应用 1000002(wxSend messageType=QL)连发告警
|
||||||
|
resource-alert:
|
||||||
|
enabled: true
|
||||||
|
cron: "0 */5 * * * ?"
|
||||||
|
cpu-threshold: 80
|
||||||
|
disk-threshold: 80
|
||||||
|
repeat-count: 3
|
||||||
|
cooldown-minutes: 30
|
||||||
|
touser: "LinPingFan"
|
||||||
|
message-type: QL
|
||||||
# 获取评论接口服务地址(后端转发,避免前端跨域)
|
# 获取评论接口服务地址(后端转发,避免前端跨域)
|
||||||
fetch-comments:
|
fetch-comments:
|
||||||
base-url: http://192.168.8.60:5008
|
base-url: http://192.168.8.60:5008
|
||||||
@@ -322,5 +332,11 @@ jarvis:
|
|||||||
# true=仅拉 auto-ship-order-statuses(省调用,其它状态依赖推送);false=时间窗内全状态(推荐,与本地对齐)
|
# true=仅拉 auto-ship-order-statuses(省调用,其它状态依赖推送);false=时间窗内全状态(推荐,与本地对齐)
|
||||||
pull-list-only-auto-ship-statuses: false
|
pull-list-only-auto-ship-statuses: false
|
||||||
auto-ship-batch-size: 20
|
auto-ship-batch-size: 20
|
||||||
|
# 两次发货接口最小间隔(毫秒),避免连续/并发发货被平台返回「操作频繁」
|
||||||
|
ship-min-interval-ms: 2000
|
||||||
|
# 「操作频繁」、超时等可恢复错误的额外重试次数(不含首次)
|
||||||
|
ship-retry-times: 3
|
||||||
|
# 重试基础等待(毫秒),第 n 次等待 n × 该值
|
||||||
|
ship-retry-delay-ms: 2000
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -216,6 +216,16 @@ jarvis:
|
|||||||
cron: "0 */5 * * * ?"
|
cron: "0 */5 * * * ?"
|
||||||
order-delay-ms: 250
|
order-delay-ms: 250
|
||||||
max-orders-per-round: 0
|
max-orders-per-round: 0
|
||||||
|
# 服务监控定时刷新:CPU 或物理盘使用率超过阈值时,经企微应用 1000002(wxSend messageType=QL)连发告警
|
||||||
|
resource-alert:
|
||||||
|
enabled: true
|
||||||
|
cron: "0 */5 * * * ?"
|
||||||
|
cpu-threshold: 80
|
||||||
|
disk-threshold: 80
|
||||||
|
repeat-count: 3
|
||||||
|
cooldown-minutes: 30
|
||||||
|
touser: "LinPingFan"
|
||||||
|
message-type: QL
|
||||||
# 获取评论接口服务地址(后端转发)
|
# 获取评论接口服务地址(后端转发)
|
||||||
fetch-comments:
|
fetch-comments:
|
||||||
base-url: http://192.168.8.60:5008
|
base-url: http://192.168.8.60:5008
|
||||||
|
|||||||
@@ -71,4 +71,19 @@ public class JarvisGoofishProperties {
|
|||||||
|
|
||||||
/** 与 defaultShipExpressCode 配套的展示名称 */
|
/** 与 defaultShipExpressCode 配套的展示名称 */
|
||||||
private String defaultShipExpressName = "日日顺";
|
private String defaultShipExpressName = "日日顺";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 两次发货接口调用的最小间隔(毫秒)。开放平台对连续发货会返回「操作频繁」。
|
||||||
|
*/
|
||||||
|
private long shipMinIntervalMs = 2000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发货遇到「操作频繁」、超时等可恢复错误时的额外重试次数(不含首次)。
|
||||||
|
*/
|
||||||
|
private int shipRetryTimes = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发货重试基础等待(毫秒)。第 n 次重试等待 n × 该值。
|
||||||
|
*/
|
||||||
|
private long shipRetryDelayMs = 2000L;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,33 @@ public class JDOrder extends BaseEntity {
|
|||||||
@Excel(name = "物流链接")
|
@Excel(name = "物流链接")
|
||||||
private String logisticsLink;
|
private String logisticsLink;
|
||||||
|
|
||||||
|
/** 物流运单号(手工填写或扫描回写;无物流链接时也可落库) */
|
||||||
|
@Excel(name = "物流单号")
|
||||||
|
private String waybillNo;
|
||||||
|
|
||||||
|
/** 腾讯文档「物流单号」列:优先链接,否则运单号 */
|
||||||
|
public String resolveLogisticsDocValue() {
|
||||||
|
if (logisticsLink != null && !logisticsLink.trim().isEmpty()) {
|
||||||
|
return logisticsLink.trim();
|
||||||
|
}
|
||||||
|
if (waybillNo != null && !waybillNo.trim().isEmpty()) {
|
||||||
|
return waybillNo.trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasLogisticsInfo() {
|
||||||
|
return resolveLogisticsDocValue() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHttpUrl(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String t = value.trim();
|
||||||
|
return t.startsWith("http://") || t.startsWith("https://");
|
||||||
|
}
|
||||||
|
|
||||||
/** 是否已推送到腾讯文档(0-未推送,1-已推送) */
|
/** 是否已推送到腾讯文档(0-未推送,1-已推送) */
|
||||||
private Integer tencentDocPushed;
|
private Integer tencentDocPushed;
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ public interface JDOrderMapper {
|
|||||||
* 根据物流链接查询订单
|
* 根据物流链接查询订单
|
||||||
*/
|
*/
|
||||||
JDOrder selectJDOrderByLogisticsLink(String logisticsLink);
|
JDOrder selectJDOrderByLogisticsLink(String logisticsLink);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据物流运单号查询订单
|
||||||
|
*/
|
||||||
|
JDOrder selectJDOrderByWaybillNo(String waybillNo);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据第三方单号查询订单
|
* 根据第三方单号查询订单
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ public interface IJDOrderService {
|
|||||||
|
|
||||||
/** 根据物流链接查询订单 */
|
/** 根据物流链接查询订单 */
|
||||||
JDOrder selectJDOrderByLogisticsLink(String logisticsLink);
|
JDOrder selectJDOrderByLogisticsLink(String logisticsLink);
|
||||||
|
|
||||||
|
/** 根据物流运单号查询订单 */
|
||||||
|
JDOrder selectJDOrderByWaybillNo(String waybillNo);
|
||||||
|
|
||||||
/** 根据第三方单号查询订单 */
|
/** 根据第三方单号查询订单 */
|
||||||
JDOrder selectJDOrderByThirdPartyOrderNo(String thirdPartyOrderNo);
|
JDOrder selectJDOrderByThirdPartyOrderNo(String thirdPartyOrderNo);
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ public interface ILogisticsService {
|
|||||||
*/
|
*/
|
||||||
boolean fetchLogisticsAndPush(JDOrder order);
|
boolean fetchLogisticsAndPush(JDOrder order);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 录单手工填写的运单号:写入 Redis 并触发闲鱼同步(无物流链接时跳过扫描抓取)。
|
||||||
|
*/
|
||||||
|
void persistManualWaybill(JDOrder order);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 分享类京东物流短链:查运单并通过 wxts 推送到指定 touser(不依赖订单表)
|
* 分享类京东物流短链:查运单并通过 wxts 推送到指定 touser(不依赖订单表)
|
||||||
*/
|
*/
|
||||||
|
|||||||
+256
-46
@@ -29,6 +29,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
/**
|
/**
|
||||||
* 闲管家:推送/拉单后的落库、详情、关联京东单、同步运单、发货
|
* 闲管家:推送/拉单后的落库、详情、关联京东单、同步运单、发货
|
||||||
*/
|
*/
|
||||||
@@ -39,6 +40,15 @@ public class GoofishOrderPipeline {
|
|||||||
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
|
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
|
||||||
/** F-闲鱼等京东单:运单可按第三方单号=闲鱼单号写入 Redis,见 LogisticsServiceImpl 镜像写入 */
|
/** F-闲鱼等京东单:运单可按第三方单号=闲鱼单号写入 Redis,见 LogisticsServiceImpl 镜像写入 */
|
||||||
public static final String REDIS_WAYBILL_GOOFISH_ORDER_PREFIX = "logistics:waybill:goofish:";
|
public static final String REDIS_WAYBILL_GOOFISH_ORDER_PREFIX = "logistics:waybill:goofish:";
|
||||||
|
/** 同一闲鱼单发货互斥,避免定时扫描与京东运单回调同时打出发货 */
|
||||||
|
private static final String REDIS_SHIP_ORDER_LOCK_PREFIX = "goofish:ship:lock:";
|
||||||
|
/** 全局发货槽:同一时刻只允许一笔发货请求在途,结束后再保留最小间隔 */
|
||||||
|
private static final String REDIS_SHIP_GLOBAL_LOCK = "goofish:ship:global";
|
||||||
|
/** 发货扫描互斥,避免拉单后扫描与定时扫描叠在一起 */
|
||||||
|
private static final String REDIS_SHIP_BATCH_LOCK = "goofish:ship:batch";
|
||||||
|
/** 可恢复失败后的短冷却,避免同一轮扫描立刻再打一次 */
|
||||||
|
private static final String REDIS_SHIP_COOLDOWN_PREFIX = "goofish:ship:cooldown:";
|
||||||
|
private static final long SHIP_FAIL_COOLDOWN_SECONDS = 30L;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private ErpGoofishOrderMapper erpGoofishOrderMapper;
|
private ErpGoofishOrderMapper erpGoofishOrderMapper;
|
||||||
@@ -565,34 +575,7 @@ public class GoofishOrderPipeline {
|
|||||||
ship.setWaybillNo(waybill.trim());
|
ship.setWaybillNo(waybill.trim());
|
||||||
ship.setExpressCode(expressCode);
|
ship.setExpressCode(expressCode);
|
||||||
ship.setExpressName(expressName);
|
ship.setExpressName(expressName);
|
||||||
String resp = ship.getResponseBody();
|
submitShip(row, ship, expressCode, waybill.trim(), manualRetry);
|
||||||
JSONObject r = JSON.parseObject(resp);
|
|
||||||
if (r != null && r.getIntValue("code") == 0) {
|
|
||||||
ErpGoofishOrder ok = new ErpGoofishOrder();
|
|
||||||
ok.setId(row.getId());
|
|
||||||
ok.setShipStatus(1);
|
|
||||||
ok.setShipTime(DateUtils.getNowDate());
|
|
||||||
ok.setShipExpressCode(expressCode);
|
|
||||||
ok.setShipError(null);
|
|
||||||
ok.setUpdateTime(DateUtils.getNowDate());
|
|
||||||
erpGoofishOrderMapper.update(ok);
|
|
||||||
row.setShipStatus(1);
|
|
||||||
if (goofishOrderChangeLogger != null) {
|
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
|
||||||
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
|
||||||
"发货成功,运单 " + waybill.trim()
|
|
||||||
+ ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus())
|
|
||||||
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String msg = r != null ? r.getString("msg") : "unknown";
|
|
||||||
patchShipError(row, msg != null ? msg : "发货接口返回失败");
|
|
||||||
if (goofishOrderChangeLogger != null) {
|
|
||||||
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
|
||||||
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP",
|
|
||||||
"发货失败:" + (row.getShipError() != null ? row.getShipError() : msg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
patchShipError(row, ex.getMessage());
|
patchShipError(row, ex.getMessage());
|
||||||
if (goofishOrderChangeLogger != null) {
|
if (goofishOrderChangeLogger != null) {
|
||||||
@@ -604,6 +587,220 @@ public class GoofishOrderPipeline {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同一订单只允许一路发货;全局再串行调用开放平台,遇到「操作频繁」等可恢复错误按间隔重试。
|
||||||
|
*/
|
||||||
|
private void submitShip(ErpGoofishOrder row, OrderShipRequest ship, String expressCode, String waybill,
|
||||||
|
boolean manualRetry) {
|
||||||
|
if (!manualRetry && inShipCooldown(row.getId())) {
|
||||||
|
log.info("闲管家发货冷却中,跳过本轮 orderNo={}", row.getOrderNo());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String orderLock = REDIS_SHIP_ORDER_LOCK_PREFIX + row.getId();
|
||||||
|
Boolean got = stringRedisTemplate.opsForValue().setIfAbsent(orderLock, "1", 180, TimeUnit.SECONDS);
|
||||||
|
if (!Boolean.TRUE.equals(got)) {
|
||||||
|
log.info("闲管家发货跳过:该单正在发货 orderNo={}", row.getOrderNo());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ErpGoofishOrder fresh = erpGoofishOrderMapper.selectById(row.getId());
|
||||||
|
if (fresh != null && fresh.getShipStatus() != null && fresh.getShipStatus() == 1) {
|
||||||
|
row.setShipStatus(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int maxAttempts = Math.max(1, goofishProperties.getShipRetryTimes() + 1);
|
||||||
|
String lastMsg = null;
|
||||||
|
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||||
|
String resp;
|
||||||
|
try {
|
||||||
|
resp = callShipSerialized(ship);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
lastMsg = ex.getMessage() == null ? ex.toString() : ex.getMessage();
|
||||||
|
log.warn("闲管家发货调用异常 orderNo={} attempt={}/{} {}", row.getOrderNo(), attempt, maxAttempts, lastMsg);
|
||||||
|
if (attempt < maxAttempts && sleepBeforeShipRetry(attempt)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
patchShipError(row, lastMsg);
|
||||||
|
appendShipLog(row, "发货异常:" + (row.getShipError() != null ? row.getShipError() : lastMsg));
|
||||||
|
armShipCooldown(row.getId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject r;
|
||||||
|
try {
|
||||||
|
r = StringUtils.isEmpty(resp) ? null : JSON.parseObject(resp);
|
||||||
|
} catch (Exception parseEx) {
|
||||||
|
r = null;
|
||||||
|
}
|
||||||
|
if (r != null && r.getIntValue("code") == 0) {
|
||||||
|
markShipSuccess(row, expressCode, waybill);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastMsg = shipFailureMessage(r, resp);
|
||||||
|
if (isAlreadyShippedMessage(lastMsg)) {
|
||||||
|
log.info("闲管家发货接口表示已发货,按成功处理 orderNo={} msg={}", row.getOrderNo(), lastMsg);
|
||||||
|
markShipSuccess(row, expressCode, waybill);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (attempt < maxAttempts && isRetryableShipFailure(r, resp, lastMsg)) {
|
||||||
|
log.warn("闲管家发货将重试 orderNo={} attempt={}/{} msg={}", row.getOrderNo(), attempt, maxAttempts, lastMsg);
|
||||||
|
if (sleepBeforeShipRetry(attempt)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
patchShipError(row, lastMsg);
|
||||||
|
appendShipLog(row, "发货失败:" + (row.getShipError() != null ? row.getShipError() : lastMsg));
|
||||||
|
if (isRetryableShipFailure(r, resp, lastMsg)) {
|
||||||
|
armShipCooldown(row.getId());
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.delete(orderLock);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("释放闲管家发货锁失败 orderNo={} {}", row.getOrderNo(), e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局只放行一笔发货请求。调用结束后把锁缩短为最小间隔,下一笔必须等这段时间过去。
|
||||||
|
*/
|
||||||
|
private String callShipSerialized(OrderShipRequest ship) {
|
||||||
|
long intervalMs = goofishProperties.getShipMinIntervalMs();
|
||||||
|
long deadline = System.currentTimeMillis() + 90_000L;
|
||||||
|
boolean locked = false;
|
||||||
|
while (System.currentTimeMillis() < deadline) {
|
||||||
|
Boolean ok = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_SHIP_GLOBAL_LOCK, "1", 45, TimeUnit.SECONDS);
|
||||||
|
if (Boolean.TRUE.equals(ok)) {
|
||||||
|
locked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!sleepQuiet(250L)) {
|
||||||
|
throw new IllegalStateException("发货排队被中断");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!locked) {
|
||||||
|
throw new IllegalStateException("发货排队超时,避免并发触发操作频繁");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return ship.getResponseBody();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (intervalMs <= 0) {
|
||||||
|
stringRedisTemplate.delete(REDIS_SHIP_GLOBAL_LOCK);
|
||||||
|
} else {
|
||||||
|
long seconds = Math.max(1L, (intervalMs + 999L) / 1000L);
|
||||||
|
stringRedisTemplate.expire(REDIS_SHIP_GLOBAL_LOCK, seconds, TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.delete(REDIS_SHIP_GLOBAL_LOCK);
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
log.debug("释放闲管家发货全局锁失败 {}", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markShipSuccess(ErpGoofishOrder row, String expressCode, String waybill) {
|
||||||
|
ErpGoofishOrder ok = new ErpGoofishOrder();
|
||||||
|
ok.setId(row.getId());
|
||||||
|
ok.setShipStatus(1);
|
||||||
|
ok.setShipTime(DateUtils.getNowDate());
|
||||||
|
ok.setShipExpressCode(expressCode);
|
||||||
|
ok.setShipError("");
|
||||||
|
ok.setUpdateTime(DateUtils.getNowDate());
|
||||||
|
erpGoofishOrderMapper.update(ok);
|
||||||
|
row.setShipStatus(1);
|
||||||
|
row.setShipError(null);
|
||||||
|
appendShipLog(row, "发货成功,运单 " + waybill
|
||||||
|
+ ",订单状态 " + GoofishStatusLabels.orderStatusHuman(row.getOrderStatus())
|
||||||
|
+ ",退款状态 " + GoofishStatusLabels.refundStatusHuman(row.getRefundStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendShipLog(ErpGoofishOrder row, String message) {
|
||||||
|
if (goofishOrderChangeLogger == null || row == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
goofishOrderChangeLogger.append(row.getId(), row.getAppKey(), row.getOrderNo(),
|
||||||
|
GoofishOrderChangeLogger.TYPE_SHIP, "AUTO_SHIP", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String shipFailureMessage(JSONObject r, String resp) {
|
||||||
|
if (r != null && StringUtils.isNotEmpty(r.getString("msg"))) {
|
||||||
|
return r.getString("msg");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(resp)) {
|
||||||
|
return "发货接口无响应";
|
||||||
|
}
|
||||||
|
return "发货接口返回失败";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isAlreadyShippedMessage(String msg) {
|
||||||
|
return StringUtils.isNotEmpty(msg) && (msg.contains("已发货") || msg.contains("重复发货"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRetryableShipFailure(JSONObject r, String resp, String msg) {
|
||||||
|
if (r == null || StringUtils.isEmpty(resp)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return isRetryableShipMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isRetryableShipMessage(String msg) {
|
||||||
|
if (StringUtils.isEmpty(msg)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String m = msg.toLowerCase();
|
||||||
|
return m.contains("频繁") || m.contains("稍后") || m.contains("繁忙")
|
||||||
|
|| m.contains("限流") || m.contains("超时")
|
||||||
|
|| m.contains("too many") || m.contains("rate limit") || m.contains("timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sleepBeforeShipRetry(int attempt) {
|
||||||
|
long base = Math.max(200L, goofishProperties.getShipRetryDelayMs());
|
||||||
|
long delay = base * Math.max(1, attempt);
|
||||||
|
log.info("闲管家发货重试等待 {} ms", delay);
|
||||||
|
return sleepQuiet(delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void armShipCooldown(Long orderId) {
|
||||||
|
if (orderId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.opsForValue().set(REDIS_SHIP_COOLDOWN_PREFIX + orderId, "1",
|
||||||
|
SHIP_FAIL_COOLDOWN_SECONDS, TimeUnit.SECONDS);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("写入闲管家发货冷却失败 id={} {}", orderId, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean inShipCooldown(Long orderId) {
|
||||||
|
if (orderId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Boolean.TRUE.equals(stringRedisTemplate.hasKey(REDIS_SHIP_COOLDOWN_PREFIX + orderId));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean sleepQuiet(long millis) {
|
||||||
|
if (millis <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(millis);
|
||||||
|
return true;
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void patchShipError(ErpGoofishOrder row, String err) {
|
private void patchShipError(ErpGoofishOrder row, String err) {
|
||||||
ErpGoofishOrder p = new ErpGoofishOrder();
|
ErpGoofishOrder p = new ErpGoofishOrder();
|
||||||
p.setId(row.getId());
|
p.setId(row.getId());
|
||||||
@@ -1197,30 +1394,43 @@ public class GoofishOrderPipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int syncWaybillAndTryShipBatch(int limit) {
|
public int syncWaybillAndTryShipBatch(int limit) {
|
||||||
List<Integer> statuses = resolveAutoShipOrderStatuses();
|
Boolean got = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_SHIP_BATCH_LOCK, "1", 10, TimeUnit.MINUTES);
|
||||||
List<ErpGoofishOrder> rows = erpGoofishOrderMapper.selectPendingShip(statuses, limit);
|
if (!Boolean.TRUE.equals(got)) {
|
||||||
if (rows == null) {
|
log.info("闲管家发货扫描已在进行,跳过本轮");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
int n = 0;
|
try {
|
||||||
for (ErpGoofishOrder row : rows) {
|
List<Integer> statuses = resolveAutoShipOrderStatuses();
|
||||||
ErpGoofishOrder full = erpGoofishOrderMapper.selectById(row.getId());
|
List<ErpGoofishOrder> rows = erpGoofishOrderMapper.selectPendingShip(statuses, limit);
|
||||||
if (full == null) {
|
if (rows == null) {
|
||||||
continue;
|
return 0;
|
||||||
}
|
}
|
||||||
tryLinkJdOrder(full);
|
int n = 0;
|
||||||
syncWaybillFromRedis(full);
|
for (ErpGoofishOrder row : rows) {
|
||||||
if ((full.getRefundStatus() == null || full.getRefundStatus() == 0)
|
ErpGoofishOrder full = erpGoofishOrderMapper.selectById(row.getId());
|
||||||
&& statuses.contains(full.getOrderStatus())
|
if (full == null) {
|
||||||
&& StringUtils.isEmpty(full.getLocalWaybillNo())
|
continue;
|
||||||
&& StringUtils.isEmpty(full.getDetailWaybillNo())) {
|
}
|
||||||
refreshDetail(full);
|
tryLinkJdOrder(full);
|
||||||
syncWaybillFromRedis(full);
|
syncWaybillFromRedis(full);
|
||||||
|
if ((full.getRefundStatus() == null || full.getRefundStatus() == 0)
|
||||||
|
&& statuses.contains(full.getOrderStatus())
|
||||||
|
&& StringUtils.isEmpty(full.getLocalWaybillNo())
|
||||||
|
&& StringUtils.isEmpty(full.getDetailWaybillNo())) {
|
||||||
|
refreshDetail(full);
|
||||||
|
syncWaybillFromRedis(full);
|
||||||
|
}
|
||||||
|
tryAutoShip(full);
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
stringRedisTemplate.delete(REDIS_SHIP_BATCH_LOCK);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("释放闲管家发货扫描锁失败 {}", e.getMessage());
|
||||||
}
|
}
|
||||||
tryAutoShip(full);
|
|
||||||
n++;
|
|
||||||
}
|
}
|
||||||
return n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-11
@@ -60,6 +60,8 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
@Resource
|
@Resource
|
||||||
private IWeComShareLinkLogisticsJobService weComShareLinkLogisticsJobService;
|
private IWeComShareLinkLogisticsJobService weComShareLinkLogisticsJobService;
|
||||||
@Resource
|
@Resource
|
||||||
|
private com.ruoyi.jarvis.service.ILogisticsService logisticsService;
|
||||||
|
@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;
|
||||||
@@ -76,7 +78,7 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
private static final int EXTERNAL_LOGISTICS_LIST_LIMIT = 35;
|
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" + "{单的备注}\n" + "—————————\n" + "京粉实际价格:不用填";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> execute(String command) {
|
public List<String> execute(String command) {
|
||||||
@@ -96,6 +98,7 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
@Override
|
@Override
|
||||||
public List<String> execute(String command, boolean forceGenerate, boolean isFromConsole, String wecomUserId) {
|
public List<String> execute(String command, boolean forceGenerate, boolean isFromConsole, String wecomUserId) {
|
||||||
// 存储接收的消息到Redis队列
|
// 存储接收的消息到Redis队列
|
||||||
|
|
||||||
storeMessageToRedis("instruction:request", command);
|
storeMessageToRedis("instruction:request", command);
|
||||||
|
|
||||||
// 检测价保/赔付消息,自动标记后返到账
|
// 检测价保/赔付消息,自动标记后返到账
|
||||||
@@ -1504,6 +1507,9 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
if (logistics == null && order.getLogisticsLink() != null) {
|
if (logistics == null && order.getLogisticsLink() != null) {
|
||||||
logistics = order.getLogisticsLink();
|
logistics = order.getLogisticsLink();
|
||||||
}
|
}
|
||||||
|
if (isEmpty(logistics) && !isEmpty(order.getWaybillNo())) {
|
||||||
|
logistics = order.getWaybillNo();
|
||||||
|
}
|
||||||
if (isEmpty(model) && isEmpty(address) && isEmpty(logistics)) {
|
if (isEmpty(model) && isEmpty(address) && isEmpty(logistics)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -1568,7 +1574,7 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
if (isEmpty(order.getBuyer())) missing.append("下单人\n");
|
if (isEmpty(order.getBuyer())) missing.append("下单人\n");
|
||||||
if (order.getPaymentAmount() == null || order.getPaymentAmount() == 0) missing.append("下单价格\n");
|
if (order.getPaymentAmount() == null || order.getPaymentAmount() == 0) missing.append("下单价格\n");
|
||||||
if (order.getRebateAmount() == null) missing.append("后返金额\n");
|
if (order.getRebateAmount() == null) missing.append("后返金额\n");
|
||||||
if (isEmpty(order.getLogisticsLink())) missing.append("物流链接\n");
|
if (isEmpty(order.getLogisticsLink()) && isEmpty(order.getWaybillNo())) missing.append("物流链接或物流单号\n");
|
||||||
if (isEmpty(order.getAddress())) missing.append("收货地址\n");
|
if (isEmpty(order.getAddress())) missing.append("收货地址\n");
|
||||||
if (isEmpty(order.getModelNumber())) missing.append("型号\n");
|
if (isEmpty(order.getModelNumber())) missing.append("型号\n");
|
||||||
if (missing.length() > 0) {
|
if (missing.length() > 0) {
|
||||||
@@ -1576,8 +1582,10 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
return warn;
|
return warn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订单号重复校验(控制台入口跳过此校验,允许管理员更新单号)
|
boolean placeholderJdOrderId = isPlaceholderJdOrderId(order);
|
||||||
if (!isFromConsole && !isEmpty(order.getOrderId())) {
|
|
||||||
|
// 订单号重复校验(控制台入口跳过;填物流单号时京东单号「1」允许重复)
|
||||||
|
if (!isFromConsole && !isEmpty(order.getOrderId()) && !placeholderJdOrderId) {
|
||||||
JDOrder existingByOrderId = jdOrderService.selectJDOrderByOrderId(order.getOrderId());
|
JDOrder existingByOrderId = jdOrderService.selectJDOrderByOrderId(order.getOrderId());
|
||||||
if (existingByOrderId != null) {
|
if (existingByOrderId != null) {
|
||||||
// 如果是更新同一条记录(remark相同),则不提示重复
|
// 如果是更新同一条记录(remark相同),则不提示重复
|
||||||
@@ -1606,6 +1614,18 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 物流单号重复校验(控制台入口跳过)
|
||||||
|
if (!isFromConsole && !isEmpty(order.getWaybillNo())) {
|
||||||
|
JDOrder existingByWaybill = jdOrderService.selectJDOrderByWaybillNo(order.getWaybillNo());
|
||||||
|
if (existingByWaybill != null && !order.getRemark().equals(existingByWaybill.getRemark())) {
|
||||||
|
String warn = "[炸弹] [炸弹] [炸弹] 录单警告!!! \n物流单号重复!\n" +
|
||||||
|
"物流单号:" + order.getWaybillNo() + "\n" +
|
||||||
|
"已存在的订单:" + existingByWaybill.getRemark() + "\n" +
|
||||||
|
"下单时间:" + new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(existingByWaybill.getOrderTime());
|
||||||
|
return warn;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 地址重复提示(不阻断写入,与 JDUtil 提示一致)
|
// 地址重复提示(不阻断写入,与 JDUtil 提示一致)
|
||||||
List<JDOrder> byAddress = jdOrderService.selectJDOrderListByAddress(order.getAddress());
|
List<JDOrder> byAddress = jdOrderService.selectJDOrderListByAddress(order.getAddress());
|
||||||
if (byAddress != null && !byAddress.isEmpty()) {
|
if (byAddress != null && !byAddress.isEmpty()) {
|
||||||
@@ -1627,8 +1647,8 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证1:如果查询不到对应订单,提示用户(控制台入口跳过此校验)
|
// 验证1:如果查询不到对应订单,提示用户(控制台入口跳过;占位京东单号「1」跳过)
|
||||||
if (!isFromConsole && (isEmpty(order.getOrderId()) || orderRow == null)) {
|
if (!isFromConsole && !placeholderJdOrderId && (isEmpty(order.getOrderId()) || orderRow == null)) {
|
||||||
String warn = "[炸弹] [炸弹] [炸弹] 录单警告!!! \n查询不到对应订单(使用红包 或者 转链),请五分钟后再试,或重新下单";
|
String warn = "[炸弹] [炸弹] [炸弹] 录单警告!!! \n查询不到对应订单(使用红包 或者 转链),请五分钟后再试,或重新下单";
|
||||||
return warn;
|
return warn;
|
||||||
}
|
}
|
||||||
@@ -1650,7 +1670,8 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
JDOrder exists = null;
|
JDOrder exists = null;
|
||||||
|
|
||||||
// 如果订单号不为空,优先根据订单号查找已存在的订单(用于刷新数据)
|
// 如果订单号不为空,优先根据订单号查找已存在的订单(用于刷新数据)
|
||||||
if (!isEmpty(order.getOrderId())) {
|
// 占位「1」会有多条,不能按订单号更新,否则会覆盖第一单
|
||||||
|
if (!isEmpty(order.getOrderId()) && !placeholderJdOrderId) {
|
||||||
exists = jdOrderService.selectJDOrderByOrderId(order.getOrderId());
|
exists = jdOrderService.selectJDOrderByOrderId(order.getOrderId());
|
||||||
if (exists != null) {
|
if (exists != null) {
|
||||||
// 根据订单号找到已存在的订单,更新该订单(刷新数据)
|
// 根据订单号找到已存在的订单,更新该订单(刷新数据)
|
||||||
@@ -1670,6 +1691,8 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
persistManualWaybillIfPresent(order);
|
||||||
|
|
||||||
// H-TF订单触发延迟推送机制
|
// H-TF订单触发延迟推送机制
|
||||||
// 录单后,重置10分钟倒计时,10分钟内无新录单则自动推送
|
// 录单后,重置10分钟倒计时,10分钟内无新录单则自动推送
|
||||||
if ("H-TF".equals(order.getDistributionMark())) {
|
if ("H-TF".equals(order.getDistributionMark())) {
|
||||||
@@ -1712,6 +1735,9 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
if (logistics == null && order.getLogisticsLink() != null) {
|
if (logistics == null && order.getLogisticsLink() != null) {
|
||||||
logistics = order.getLogisticsLink();
|
logistics = order.getLogisticsLink();
|
||||||
}
|
}
|
||||||
|
if (isEmpty(logistics) && !isEmpty(order.getWaybillNo())) {
|
||||||
|
logistics = order.getWaybillNo();
|
||||||
|
}
|
||||||
if (!isEmpty(logistics)) {
|
if (!isEmpty(logistics)) {
|
||||||
sb.append("物流:").append(shortenLogisticsLineForReply(logistics)).append("\n");
|
sb.append("物流:").append(shortenLogisticsLineForReply(logistics)).append("\n");
|
||||||
}
|
}
|
||||||
@@ -1747,6 +1773,19 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
return s == null || s.isEmpty();
|
return s == null || s.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无物流链接、只填物流单号时,京东单号「1」为占位:允许重复,且不按该单号更新已有订单。
|
||||||
|
*/
|
||||||
|
private boolean isPlaceholderJdOrderId(JDOrder order) {
|
||||||
|
if (order == null || isEmpty(order.getOrderId()) || isEmpty(order.getWaybillNo())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!isEmpty(order.getLogisticsLink())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return "1".equals(order.getOrderId().trim());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化订单为完整表单格式
|
* 格式化订单为完整表单格式
|
||||||
* @param order 订单对象
|
* @param order 订单对象
|
||||||
@@ -1794,6 +1833,8 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
sb.append("物流链接(需填):\n");
|
sb.append("物流链接(需填):\n");
|
||||||
// 使用原始输入的完整物流链接,如果没有则使用数据库中的值
|
// 使用原始输入的完整物流链接,如果没有则使用数据库中的值
|
||||||
sb.append(originalLogisticsLink != null ? originalLogisticsLink : (order.getLogisticsLink() != null ? order.getLogisticsLink() : "")).append("\n");
|
sb.append(originalLogisticsLink != null ? originalLogisticsLink : (order.getLogisticsLink() != null ? order.getLogisticsLink() : "")).append("\n");
|
||||||
|
sb.append("物流单号(无链接时填):\n");
|
||||||
|
sb.append(order.getWaybillNo() != null ? order.getWaybillNo() : "").append("\n");
|
||||||
sb.append("备注(下单号码有变动/没法带分机号的写这里):\n");
|
sb.append("备注(下单号码有变动/没法带分机号的写这里):\n");
|
||||||
sb.append(order.getStatus() != null ? order.getStatus() : "").append("\n");
|
sb.append(order.getStatus() != null ? order.getStatus() : "").append("\n");
|
||||||
sb.append("—————————\n");
|
sb.append("—————————\n");
|
||||||
@@ -1852,8 +1893,8 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
if (input == null || input.isEmpty()) {
|
if (input == null || input.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// 提取"物流链接(需填):"到"备注(下单号码有变动/没法带分机号的写这里):"之间的完整内容
|
// 提取"物流链接(需填):"到「物流单号」或「备注」之间的完整内容
|
||||||
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("物流链接(需填):\\s*(.*?)\\s*(?=备注(下单号码有变动/没法带分机号的写这里):|\\Z)", java.util.regex.Pattern.DOTALL);
|
java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("物流链接(需填):\\s*(.*?)\\s*(?=物流单号(无链接时填):|物流单号:|备注(下单号码有变动/没法带分机号的写这里):|\\Z)", java.util.regex.Pattern.DOTALL);
|
||||||
java.util.regex.Matcher matcher = pattern.matcher(input);
|
java.util.regex.Matcher matcher = pattern.matcher(input);
|
||||||
if (matcher.find()) {
|
if (matcher.find()) {
|
||||||
String logisticsLink = matcher.group(1).trim();
|
String logisticsLink = matcher.group(1).trim();
|
||||||
@@ -2006,13 +2047,28 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
order.setAddress(address);
|
order.setAddress(address);
|
||||||
System.out.println("地址: " + (address != null && address.length() > 50 ? address.substring(0, 50) + "..." : address));
|
System.out.println("地址: " + (address != null && address.length() > 50 ? address.substring(0, 50) + "..." : address));
|
||||||
|
|
||||||
// 物流链接(新格式)或物流链接(旧格式)
|
// 物流链接(新格式)或物流链接(旧格式);无 URL 时把内容当作运单号
|
||||||
String logisticsLink = fields.getOrDefault("物流链接(需填)", null);
|
String logisticsLink = fields.getOrDefault("物流链接(需填)", null);
|
||||||
if (logisticsLink == null) {
|
if (logisticsLink == null) {
|
||||||
logisticsLink = fields.getOrDefault("物流链接", "");
|
logisticsLink = fields.getOrDefault("物流链接", "");
|
||||||
}
|
}
|
||||||
order.setLogisticsLink(extractFirstUrl(logisticsLink));
|
order.setLogisticsLink(extractFirstUrl(logisticsLink));
|
||||||
System.out.println("物流链接: " + order.getLogisticsLink());
|
System.out.println("物流链接: " + order.getLogisticsLink());
|
||||||
|
|
||||||
|
String waybillRaw = fields.getOrDefault("物流单号(无链接时填)", null);
|
||||||
|
if (waybillRaw == null) {
|
||||||
|
waybillRaw = fields.getOrDefault("物流单号", null);
|
||||||
|
}
|
||||||
|
String waybillUrl = extractFirstUrl(waybillRaw);
|
||||||
|
if (waybillUrl != null && isEmpty(order.getLogisticsLink())) {
|
||||||
|
order.setLogisticsLink(waybillUrl);
|
||||||
|
}
|
||||||
|
String waybillNo = normalizeTrackingNo(waybillRaw);
|
||||||
|
if (isEmpty(waybillNo) && isEmpty(order.getLogisticsLink())) {
|
||||||
|
waybillNo = normalizeTrackingNo(logisticsLink);
|
||||||
|
}
|
||||||
|
order.setWaybillNo(waybillNo);
|
||||||
|
System.out.println("物流单号: " + order.getWaybillNo());
|
||||||
|
|
||||||
// 订单号(新格式)或订单号(旧格式)
|
// 订单号(新格式)或订单号(旧格式)
|
||||||
String orderId = fields.getOrDefault("订单号(需填)", null);
|
String orderId = fields.getOrDefault("订单号(需填)", null);
|
||||||
@@ -2065,7 +2121,13 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
extractFieldWithSeparator(input, fields, "下单付款(注意核对):", "后返金额(注意核对):");
|
extractFieldWithSeparator(input, fields, "下单付款(注意核对):", "后返金额(注意核对):");
|
||||||
extractFieldWithSeparator(input, fields, "后返金额(注意核对):", "订单号(需填):");
|
extractFieldWithSeparator(input, fields, "后返金额(注意核对):", "订单号(需填):");
|
||||||
extractFieldWithSeparator(input, fields, "订单号(需填):", "物流链接(需填):");
|
extractFieldWithSeparator(input, fields, "订单号(需填):", "物流链接(需填):");
|
||||||
extractFieldWithSeparator(input, fields, "物流链接(需填):", "备注(下单号码有变动/没法带分机号的写这里):");
|
if (input.contains("物流单号(无链接时填):") || input.contains("物流单号:")) {
|
||||||
|
String waybillLabel = input.contains("物流单号(无链接时填):") ? "物流单号(无链接时填):" : "物流单号:";
|
||||||
|
extractFieldWithSeparator(input, fields, "物流链接(需填):", waybillLabel);
|
||||||
|
extractFieldWithSeparator(input, fields, waybillLabel, "备注(下单号码有变动/没法带分机号的写这里):");
|
||||||
|
} else {
|
||||||
|
extractFieldWithSeparator(input, fields, "物流链接(需填):", "备注(下单号码有变动/没法带分机号的写这里):");
|
||||||
|
}
|
||||||
|
|
||||||
// 手动提取"备注"字段(最后一个字段,可能后面有分隔线或"京粉实际价格")
|
// 手动提取"备注"字段(最后一个字段,可能后面有分隔线或"京粉实际价格")
|
||||||
java.util.regex.Pattern remarkPattern = java.util.regex.Pattern.compile("备注(下单号码有变动/没法带分机号的写这里):[\\s\\n]*([\\s\\S]*?)[\\s\\n]*(?=—————————|京粉实际价格|\\Z)", java.util.regex.Pattern.DOTALL);
|
java.util.regex.Pattern remarkPattern = java.util.regex.Pattern.compile("备注(下单号码有变动/没法带分机号的写这里):[\\s\\n]*([\\s\\S]*?)[\\s\\n]*(?=—————————|京粉实际价格|\\Z)", java.util.regex.Pattern.DOTALL);
|
||||||
@@ -2199,6 +2261,33 @@ public class InstructionServiceImpl implements IInstructionService {
|
|||||||
return matcher.find() ? matcher.group(0) : null;
|
return matcher.find() ? matcher.group(0) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 非 URL 的物流单号:去空白、去掉中文标签,长度 6–64 */
|
||||||
|
private String normalizeTrackingNo(String text) {
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (extractFirstUrl(text) != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String s = text.trim().replaceAll("\\s+", "");
|
||||||
|
s = s.replaceFirst("^(物流单号|运单号|快递单号)[::]?", "");
|
||||||
|
if (s.isEmpty() || s.length() < 6 || s.length() > 64) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistManualWaybillIfPresent(JDOrder order) {
|
||||||
|
if (order == null || order.getId() == null || isEmpty(order.getWaybillNo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
logisticsService.persistManualWaybill(order);
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("手工运单号写入 Redis 失败 - 订单ID: " + order.getId() + ", 错误: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String handleTransferLike(String input) {
|
private String handleTransferLike(String input) {
|
||||||
// 前端仍复用已有 /jarvis/jdorder/generatePromotionContent 接口,这里仅提供一个回显占位
|
// 前端仍复用已有 /jarvis/jdorder/generatePromotionContent 接口,这里仅提供一个回显占位
|
||||||
return "指令入口:请在\"一键转链\"页面使用(输入文案/链接后点生成)";
|
return "指令入口:请在\"一键转链\"页面使用(输入文案/链接后点生成)";
|
||||||
|
|||||||
@@ -78,6 +78,11 @@ public class JDOrderServiceImpl implements IJDOrderService {
|
|||||||
public JDOrder selectJDOrderByLogisticsLink(String logisticsLink) {
|
public JDOrder selectJDOrderByLogisticsLink(String logisticsLink) {
|
||||||
return jdOrderMapper.selectJDOrderByLogisticsLink(logisticsLink);
|
return jdOrderMapper.selectJDOrderByLogisticsLink(logisticsLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JDOrder selectJDOrderByWaybillNo(String waybillNo) {
|
||||||
|
return jdOrderMapper.selectJDOrderByWaybillNo(waybillNo);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public JDOrder selectJDOrderByThirdPartyOrderNo(String thirdPartyOrderNo) {
|
public JDOrder selectJDOrderByThirdPartyOrderNo(String thirdPartyOrderNo) {
|
||||||
|
|||||||
@@ -324,6 +324,41 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
return m.startsWith("F") || m.contains("\u95f2\u9c7c");
|
return m.startsWith("F") || m.contains("\u95f2\u9c7c");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void persistManualWaybill(JDOrder order) {
|
||||||
|
if (order == null || order.getId() == null || !StringUtils.hasText(order.getWaybillNo())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Long orderId = order.getId();
|
||||||
|
String waybillNo = order.getWaybillNo().trim();
|
||||||
|
String redisKey = REDIS_WAYBILL_KEY_PREFIX + orderId;
|
||||||
|
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
|
||||||
|
persistWaybillNoToOrder(orderId, waybillNo);
|
||||||
|
safeNotifyGoofishShip(orderId, waybillNo, "录单手工填写运单号;已写 Redis;随后触发闲鱼同步");
|
||||||
|
logger.info("手工运单号已固化 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void persistWaybillNoToOrder(Long orderId, String waybillNo) {
|
||||||
|
if (orderId == null || !StringUtils.hasText(waybillNo)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
JDOrder updateOrder = jdOrderService.selectJDOrderById(orderId);
|
||||||
|
if (updateOrder == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String current = updateOrder.getWaybillNo();
|
||||||
|
if (waybillNo.trim().equals(current == null ? "" : current.trim())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateOrder.setWaybillNo(waybillNo.trim());
|
||||||
|
jdOrderService.updateJDOrder(updateOrder);
|
||||||
|
logger.info("订单运单号已回写数据库 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("回写订单运单号失败 - 订单ID: {}, waybill_no: {}, err={}", orderId, waybillNo, e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean fetchLogisticsAndPush(JDOrder order) {
|
public boolean fetchLogisticsAndPush(JDOrder order) {
|
||||||
if (order == null || order.getId() == null) {
|
if (order == null || order.getId() == null) {
|
||||||
@@ -357,7 +392,11 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
|
|
||||||
// 检查物流链接
|
// 检查物流链接
|
||||||
String logisticsLink = order.getLogisticsLink();
|
String logisticsLink = order.getLogisticsLink();
|
||||||
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
|
if (!JDOrder.isHttpUrl(logisticsLink)) {
|
||||||
|
if (StringUtils.hasText(order.getWaybillNo())) {
|
||||||
|
persistManualWaybill(order);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
logger.info("订单暂无物流链接,跳过处理 - 订单ID: {}", orderId);
|
logger.info("订单暂无物流链接,跳过处理 - 订单ID: {}", orderId);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -411,6 +450,7 @@ public class LogisticsServiceImpl implements ILogisticsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.info("检测到waybill_no: {} - 订单ID: {}", waybillNo, orderId);
|
logger.info("检测到waybill_no: {} - 订单ID: {}", waybillNo, orderId);
|
||||||
|
persistWaybillNoToOrder(orderId, waybillNo);
|
||||||
|
|
||||||
// 标记物流链接是否更新
|
// 标记物流链接是否更新
|
||||||
boolean logisticsLinkUpdated = false;
|
boolean logisticsLinkUpdated = false;
|
||||||
|
|||||||
+409
-43
@@ -2,7 +2,6 @@ package com.ruoyi.jarvis.service.impl;
|
|||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.alibaba.fastjson2.JSONWriter;
|
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
import com.ruoyi.jarvis.domain.JDOrder;
|
import com.ruoyi.jarvis.domain.JDOrder;
|
||||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||||
@@ -28,6 +27,7 @@ import java.time.ZoneId;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.temporal.TemporalAdjusters;
|
import java.time.temporal.TemporalAdjusters;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -63,8 +63,16 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
|
|
||||||
public static final String DEFAULT_PROMPT =
|
public static final String DEFAULT_PROMPT =
|
||||||
"你是电商分销订单与利润分析助手。下面 JSON 已按慢单 jd_order 汇总,不要编造其中没有的数字。\n\n"
|
"你是电商分销订单与利润分析助手。下面 JSON 已按慢单 jd_order 汇总,不要编造其中没有的数字。\n\n"
|
||||||
|
+ "# 分销口径(必须先读,决定利润怎么算)\n"
|
||||||
|
+ "- F 开头 = 发单。F,以及带连字符的 F-xxx,除去 F-W、F-H = 完全自有单。利润来自售价减去成本和额外成本。\n"
|
||||||
|
+ "- 仅 F-W、F-H,或以 F-W-、F-H- 再带后缀 = 代下单。利润来源只有代下费用。F-Hello 这类不是 F-H。\n"
|
||||||
|
+ "- 非 F 开头(含 H-TF、TF、PDD、H- 等所有其余前缀)= 代下单单。利润只来自代下佣金,单均利润天然低,属正常。\n"
|
||||||
|
+ "- 未标记不要并进上面三类。\n"
|
||||||
|
+ "- 自有单、代下单、代下单单必须分开写:各几单、各多少利润、各多少利润率,不允许合并。\n"
|
||||||
|
+ "- 这三类的数字直接读 kind 和 kd,不要自己把 byMark 加总。\n"
|
||||||
|
+ "- JSON 里的利润是订单上已经算好的 profit,不要按售价现场重算。\n\n"
|
||||||
+ "# 统计口径(必须遵守)\n"
|
+ "# 统计口径(必须遵守)\n"
|
||||||
+ "- 只含 is_count_enabled=1 的单;已退款单计入 orderCount/refundCount,但不计入付款、后返、成本、售价、利润。\n"
|
+ "- 只含 is_count_enabled=1 的单;已退款单计入总单量和退款单量,但不计入付款、后返、成本、售价、利润。\n"
|
||||||
+ "- 金额单位均为元,保留两位小数。\n"
|
+ "- 金额单位均为元,保留两位小数。\n"
|
||||||
+ "- 日报对比「昨天」、周报对比「上一完整周」、月报对比「上一自然月」。\n\n"
|
+ "- 日报对比「昨天」、周报对比「上一完整周」、月报对比「上一自然月」。\n\n"
|
||||||
+ "# 资金与后返(必须遵守,禁止说反)\n"
|
+ "# 资金与后返(必须遵守,禁止说反)\n"
|
||||||
@@ -73,37 +81,29 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
+ "- 后返越少越好:同样利润下,后返少 = 占用资金少、垫付少。0 后返且利润为正,是资金效率更好,不是缺陷。\n"
|
+ "- 后返越少越好:同样利润下,后返少 = 占用资金少、垫付少。0 后返且利润为正,是资金效率更好,不是缺陷。\n"
|
||||||
+ "- 禁止把「后返低 / 无后返 / 后返环比下降」写成风险、拖累或建议去「补后返」。\n"
|
+ "- 禁止把「后返低 / 无后返 / 后返环比下降」写成风险、拖累或建议去「补后返」。\n"
|
||||||
+ "- 评价通道/型号只看利润、利润率、单均利润和垫资金额(付款);后返高反而说明垫资更多。\n\n"
|
+ "- 评价通道/型号只看利润、利润率、单均利润和垫资金额(付款);后返高反而说明垫资更多。\n\n"
|
||||||
+ "# JSON 字段说明\n"
|
+ "# JSON 结构(数组列,时区固定 Asia/Shanghai)\n"
|
||||||
+ "periodType:day=日报 / week=周报 / month=月报。\n"
|
+ "不要把订单逐条展开。列名在 JSON 顶部,不要把 _n/_amt/_row/_kind/_d/_kd 当成统计数字。\n"
|
||||||
+ "periodLabel:给人看的周期标题。\n"
|
+ "period:day=日报 / week=周报 / month=月报。label:周期标题。\n"
|
||||||
+ "timezone:统计时区。\n"
|
+ "cur / prev:本期、上期,结构相同。range:[开始日期, 结束日期](含当天)。\n"
|
||||||
+ "current / previous:本期、上期汇总,结构相同。\n"
|
+ "n 按 _n = [总单量(含退款), 退款单量, 有效单量, 缺利润单量]。有效 = 总单 - 退款。缺利润 = 有效单里 profit 为空。\n"
|
||||||
+ "current.beginDate / endDate:本期起止日期(含当天)。\n"
|
+ "amt 按 _amt = [付款, 后返, 成本, 售价, 额外成本, 利润, 单均利润, 利润率%]。\n"
|
||||||
+ "current.label:本期短标题。\n"
|
+ "付款是垫给京东的钱。后返是垫资里稍后收回的部分,越少越好,不是业绩。成本 = 付款 - 后返。\n"
|
||||||
+ "current.orderCount:参与统计的总单量(含退款)。\n"
|
+ "售价是对客价(未扣闲鱼手续费)。额外成本含运费补贴、补差,已从利润里扣。\n"
|
||||||
+ "current.refundCount:其中已退款单量。\n"
|
+ "利润是有效单利润合计。单均利润 = 利润 / 有效单量(分母含缺利润单)。利润率% = 利润 / 付款 × 100。\n"
|
||||||
+ "current.validOrderCount:有效单量 = orderCount - refundCount。\n"
|
+ "kind:分类型汇总,每行按 _kind。类型为 自有单、代下单、代下单单;有未标记时多一行。\n"
|
||||||
+ "current.missingProfitCount:有效单里 profit 为空的数量(还没定价/没算利润)。\n"
|
+ "byMark / byModel / byBuyer:每行按 _row = [名称, 有效单量, 付款, 后返, 额外成本, 利润, 类型],已按利润降序。\n"
|
||||||
+ "current.totalPayment:有效单下单付款合计(垫付给京东的钱,占用资金)。\n"
|
+ "byMark 为全部分销标记。byModel 为型号(型号本体+店铺前缀),byBuyer 为下单人前缀(第一个「-」前)。二者只留利润前 15,同一名称按类型拆成多行。\n"
|
||||||
+ "current.totalRebate:有效单后返合计(垫资里稍后收回的部分;越少越好,不是业绩)。\n"
|
+ "d:整体对比。键为 总单/退款/付款/后返/利润/单均/利润率,值按 _d = [增减, 环比%]。\n"
|
||||||
+ "current.totalCost:真实进货成本 = totalPayment - totalRebate。\n"
|
+ "kd:三类分开对比,每行按 _kd。利润率百分点 = 本期利润率减上期利润率,不是环比百分比。\n"
|
||||||
+ "current.totalSellingPrice:对客售价合计(直款或闲鱼成交价,未扣闲鱼手续费)。\n"
|
+ "增减 = 本期减上期;正数=增加,负数=减少。环比% 在上期为 0 且本期>0 时记为 100。\n\n"
|
||||||
+ "current.totalExtraCost:额外成本合计(运费补贴、补差等,已从利润里扣)。\n"
|
|
||||||
+ "current.totalProfit:有效单利润合计(空利润的单不计入,不是按售价现场重算)。\n"
|
|
||||||
+ "current.avgProfit:单均利润 = totalProfit / validOrderCount(分母含缺利润单,会拉低均值)。\n"
|
|
||||||
+ "current.profitRatePct:利润率% = totalProfit / totalPayment × 100。\n"
|
|
||||||
+ "current.byMark:按分销标记分组,按利润降序。F / F-xxx=发单,PDD=拼多多,H-TF=固定利润约 15。\n"
|
|
||||||
+ "current.byModel:按型号(型号本体+店铺前缀)分组,只保留利润前 15。\n"
|
|
||||||
+ "current.byBuyer:按下单人前缀分组(第一个「-」前视为同一人),只保留利润前 15。\n"
|
|
||||||
+ "分组对象字段:name=分组名,orderCount=有效单量,payment=付款,rebate=后返,extraCost=额外成本,profit=利润。\n"
|
|
||||||
+ "compare:本期相对上期。每个指标含 current/previous/delta/changePct。\n"
|
|
||||||
+ "compare.*.delta:本期减上期;正数=增加,负数=减少。\n"
|
|
||||||
+ "compare.*.changePct:环比幅度%,上期为 0 且本期>0 时记为 100。\n\n"
|
|
||||||
+ "# 写作要求\n"
|
+ "# 写作要求\n"
|
||||||
+ "1. 周期结论(1-2 句)\n"
|
+ "1. 周期结论(1-2句):用 kind 分别点出自有单、代下单、代下单单的有效单量、利润、利润率\n"
|
||||||
+ "2. 订单与利润对比:用 compare 写明增减和幅度,点明利润率、单均利润\n"
|
+ "2. 订单与利润对比:整体用 d;三类用 kd 分开写增减和幅度,点明利润率、单均利润\n"
|
||||||
+ "3. 结构拆解:分销标记 / 型号 / 下单人前缀的亮点与异常\n"
|
+ "3. 结构拆解:\n"
|
||||||
+ "4. 风险:退款、利润缺失、额外成本偏高、利润率偏低;不要把后返低写成风险\n"
|
+ " - 分销标记:以 kind 为准写自有单、代下单、代下单单,不要合并\n"
|
||||||
|
+ " - 型号:看 byModel 前 15 里各类型占多少\n"
|
||||||
|
+ " - 下单人前缀:看 byBuyer 的类型列,分清自有单前缀和代下单单前缀,代下单单重点看佣金(利润)\n"
|
||||||
|
+ "4. 风险:退款、利润缺失、额外成本偏高、利润率偏低;不要把后返低写成风险。代下单单利润率低属正常,不要单独当成异常\n"
|
||||||
+ "5. 建议:1-3 条可执行动作(不要建议「提高后返」)\n"
|
+ "5. 建议:1-3 条可执行动作(不要建议「提高后返」)\n"
|
||||||
+ "输出纯文本,适合企业微信,控制在 800 字以内,可用简单条目,不要用 Markdown 代码块。\n\n"
|
+ "输出纯文本,适合企业微信,控制在 800 字以内,可用简单条目,不要用 Markdown 代码块。\n\n"
|
||||||
+ "统计数据:\n{{stats_json}}";
|
+ "统计数据:\n{{stats_json}}";
|
||||||
@@ -136,7 +136,9 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
@Override
|
@Override
|
||||||
public Map<String, Object> buildStats(String periodType, String date) {
|
public Map<String, Object> buildStats(String periodType, String date) {
|
||||||
PeriodRange current = resolvePeriod(periodType, date, false);
|
PeriodRange current = resolvePeriod(periodType, date, false);
|
||||||
return assemblePayload(periodType, current, false);
|
Map<String, Object> stats = assemblePayload(periodType, current, false);
|
||||||
|
attachLlmJson(stats);
|
||||||
|
return stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -202,8 +204,10 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
private Map<String, Object> doAnalyze(String periodType, PeriodRange current, boolean push, String profileId,
|
private Map<String, Object> doAnalyze(String periodType, PeriodRange current, boolean push, String profileId,
|
||||||
String trigger) {
|
String trigger) {
|
||||||
Map<String, Object> stats = assemblePayload(periodType, current, true);
|
Map<String, Object> stats = assemblePayload(periodType, current, true);
|
||||||
String statsJson = JSON.toJSONString(stats, JSONWriter.Feature.PrettyFormat);
|
String statsJson = toLlmJson(stats);
|
||||||
String prompt = loadPrompt().replace("{{stats_json}}", statsJson);
|
String prompt = renderPrompt(statsJson);
|
||||||
|
log.info("慢单利润分析提交 period={} llmJsonChars={} promptChars={}",
|
||||||
|
current.label, statsJson.length(), prompt.length());
|
||||||
|
|
||||||
String report;
|
String report;
|
||||||
String aiError = null;
|
String aiError = null;
|
||||||
@@ -248,6 +252,8 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
result.put("generatedAt", java.time.LocalDateTime.now(ZONE).format(TS));
|
result.put("generatedAt", java.time.LocalDateTime.now(ZONE).format(TS));
|
||||||
result.put("trigger", normalizeTrigger(trigger));
|
result.put("trigger", normalizeTrigger(trigger));
|
||||||
result.put("stats", stats);
|
result.put("stats", stats);
|
||||||
|
result.put("llmJson", statsJson);
|
||||||
|
result.put("llmJsonChars", statsJson.length());
|
||||||
result.put("report", report);
|
result.put("report", report);
|
||||||
result.put("aiError", aiError);
|
result.put("aiError", aiError);
|
||||||
result.put("pushed", pushed);
|
result.put("pushed", pushed);
|
||||||
@@ -273,12 +279,157 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
payload.put("current", cur);
|
payload.put("current", cur);
|
||||||
payload.put("previous", prev);
|
payload.put("previous", prev);
|
||||||
payload.put("compare", compare(cur, prev));
|
payload.put("compare", compare(cur, prev));
|
||||||
|
payload.put("kindCompare", compareKinds(cur, prev));
|
||||||
if (!forLlm) {
|
if (!forLlm) {
|
||||||
payload.put("generatedAt", java.time.LocalDateTime.now(ZONE).format(TS));
|
payload.put("generatedAt", java.time.LocalDateTime.now(ZONE).format(TS));
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void attachLlmJson(Map<String, Object> stats) {
|
||||||
|
String llmJson = toLlmJson(stats);
|
||||||
|
stats.put("llmJson", llmJson);
|
||||||
|
stats.put("llmJsonChars", llmJson.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 交给模型的紧凑统计。页面卡片仍用对象;这个字符串才是实际提交,分组和指标都是列数组。
|
||||||
|
*/
|
||||||
|
static String toLlmJson(Map<String, Object> stats) {
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
out.put("_n", Arrays.asList("总单", "退款", "有效", "缺利润"));
|
||||||
|
out.put("_amt", Arrays.asList("付款", "后返", "成本", "售价", "额外成本", "利润", "单均", "利润率%"));
|
||||||
|
out.put("_kind", Arrays.asList("类型", "总单", "退款", "有效", "缺利润", "付款", "后返", "成本", "售价", "额外成本", "利润", "单均", "利润率%"));
|
||||||
|
out.put("_row", Arrays.asList("名称", "单量", "付款", "后返", "额外成本", "利润", "类型"));
|
||||||
|
out.put("_d", Arrays.asList("增减", "环比%"));
|
||||||
|
out.put("_kd", Arrays.asList("类型", "有效增减", "有效环比%", "利润增减", "利润环比%", "利润率百分点"));
|
||||||
|
out.put("period", stats.get("periodType"));
|
||||||
|
out.put("label", stats.get("periodLabel"));
|
||||||
|
out.put("cur", compactPeriod(castMap(stats.get("current"))));
|
||||||
|
out.put("prev", compactPeriod(castMap(stats.get("previous"))));
|
||||||
|
out.put("d", compactDelta(castMap(stats.get("compare"))));
|
||||||
|
out.put("kd", compactKindDelta(stats.get("kindCompare")));
|
||||||
|
return JSON.toJSONString(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> compactPeriod(Map<String, Object> period) {
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
if (period == null) {
|
||||||
|
out.put("range", Arrays.asList("", ""));
|
||||||
|
out.put("n", Arrays.asList(0, 0, 0, 0));
|
||||||
|
out.put("amt", Arrays.asList(0, 0, 0, 0, 0, 0, 0, 0));
|
||||||
|
out.put("kind", Collections.emptyList());
|
||||||
|
out.put("byMark", Collections.emptyList());
|
||||||
|
out.put("byModel", Collections.emptyList());
|
||||||
|
out.put("byBuyer", Collections.emptyList());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
out.put("range", Arrays.asList(period.get("beginDate"), period.get("endDate")));
|
||||||
|
out.put("n", Arrays.asList(
|
||||||
|
period.get("orderCount"), period.get("refundCount"),
|
||||||
|
period.get("validOrderCount"), period.get("missingProfitCount")));
|
||||||
|
out.put("amt", Arrays.asList(
|
||||||
|
period.get("totalPayment"), period.get("totalRebate"), period.get("totalCost"),
|
||||||
|
period.get("totalSellingPrice"), period.get("totalExtraCost"), period.get("totalProfit"),
|
||||||
|
period.get("avgProfit"), period.get("profitRatePct")));
|
||||||
|
out.put("kind", compactKindRows(period.get("byKind")));
|
||||||
|
out.put("byMark", compactRows(period.get("byMark")));
|
||||||
|
out.put("byModel", compactRows(firstList(period.get("byModelSplit"), period.get("byModel"))));
|
||||||
|
out.put("byBuyer", compactRows(firstList(period.get("byBuyerSplit"), period.get("byBuyer"))));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<Object>> compactRows(Object groups) {
|
||||||
|
List<List<Object>> rows = new ArrayList<>();
|
||||||
|
if (!(groups instanceof List)) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
for (Object item : (List<?>) groups) {
|
||||||
|
Map<String, Object> row = castMap(item);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rows.add(Arrays.asList(
|
||||||
|
row.get("name"), row.get("orderCount"), row.get("payment"),
|
||||||
|
row.get("rebate"), row.get("extraCost"), row.get("profit"),
|
||||||
|
row.get("kind") == null ? "" : row.get("kind")));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> compactDelta(Map<String, Object> compare) {
|
||||||
|
Map<String, Object> out = new LinkedHashMap<>();
|
||||||
|
String[][] keys = {
|
||||||
|
{"orderCount", "总单"},
|
||||||
|
{"refundCount", "退款"},
|
||||||
|
{"totalPayment", "付款"},
|
||||||
|
{"totalRebate", "后返"},
|
||||||
|
{"totalProfit", "利润"},
|
||||||
|
{"avgProfit", "单均"},
|
||||||
|
{"profitRatePct", "利润率"}
|
||||||
|
};
|
||||||
|
for (String[] key : keys) {
|
||||||
|
Map<String, Object> item = compare == null ? null : castMap(compare.get(key[0]));
|
||||||
|
if (item == null) {
|
||||||
|
out.put(key[1], Arrays.asList(0, 0));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.put(key[1], Arrays.asList(item.get("delta"), item.get("changePct")));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<Object>> compactKindRows(Object groups) {
|
||||||
|
List<List<Object>> rows = new ArrayList<>();
|
||||||
|
if (!(groups instanceof List)) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
for (Object item : (List<?>) groups) {
|
||||||
|
Map<String, Object> row = castMap(item);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rows.add(Arrays.asList(
|
||||||
|
row.get("name"), row.get("orderCount"), row.get("refundCount"), row.get("validOrderCount"),
|
||||||
|
row.get("missingProfitCount"), row.get("totalPayment"), row.get("totalRebate"), row.get("totalCost"),
|
||||||
|
row.get("totalSellingPrice"), row.get("totalExtraCost"), row.get("totalProfit"),
|
||||||
|
row.get("avgProfit"), row.get("profitRatePct")));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<List<Object>> compactKindDelta(Object groups) {
|
||||||
|
List<List<Object>> rows = new ArrayList<>();
|
||||||
|
if (!(groups instanceof List)) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
for (Object item : (List<?>) groups) {
|
||||||
|
Map<String, Object> row = castMap(item);
|
||||||
|
if (row == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
rows.add(Arrays.asList(
|
||||||
|
row.get("name"), row.get("validDelta"), row.get("validChangePct"),
|
||||||
|
row.get("profitDelta"), row.get("profitChangePct"), row.get("rateDelta")));
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Object firstList(Object preferred, Object fallback) {
|
||||||
|
if (preferred instanceof List && !((List<?>) preferred).isEmpty()) {
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<String, Object> castMap(Object value) {
|
||||||
|
if (value instanceof Map) {
|
||||||
|
return (Map<String, Object>) value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, Object> summarize(PeriodRange range) {
|
private Map<String, Object> summarize(PeriodRange range) {
|
||||||
JDOrder query = new JDOrder();
|
JDOrder query = new JDOrder();
|
||||||
query.getParams().put("beginTime", range.begin.format(DAY));
|
query.getParams().put("beginTime", range.begin.format(DAY));
|
||||||
@@ -299,15 +450,23 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
Map<String, GroupAcc> byMark = new LinkedHashMap<>();
|
Map<String, GroupAcc> byMark = new LinkedHashMap<>();
|
||||||
Map<String, GroupAcc> byModel = new LinkedHashMap<>();
|
Map<String, GroupAcc> byModel = new LinkedHashMap<>();
|
||||||
Map<String, GroupAcc> byBuyer = new LinkedHashMap<>();
|
Map<String, GroupAcc> byBuyer = new LinkedHashMap<>();
|
||||||
|
Map<String, GroupAcc> byModelSplit = new LinkedHashMap<>();
|
||||||
|
Map<String, GroupAcc> byBuyerSplit = new LinkedHashMap<>();
|
||||||
|
Map<String, KindAcc> byKind = new LinkedHashMap<>();
|
||||||
|
|
||||||
for (JDOrder o : rows) {
|
for (JDOrder o : rows) {
|
||||||
if (!isCountEnabled(o)) {
|
if (!isCountEnabled(o)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
orderCount++;
|
orderCount++;
|
||||||
|
String markName = blankTo(o.getDistributionMark(), "未标记");
|
||||||
|
String kind = markKind(markName);
|
||||||
|
KindAcc ka = byKind.computeIfAbsent(kind, k -> new KindAcc());
|
||||||
|
ka.orderCount++;
|
||||||
boolean refunded = o.getIsRefunded() != null && o.getIsRefunded() == 1;
|
boolean refunded = o.getIsRefunded() != null && o.getIsRefunded() == 1;
|
||||||
if (refunded) {
|
if (refunded) {
|
||||||
refundCount++;
|
refundCount++;
|
||||||
|
ka.refundCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
BigDecimal pay = money(o.getPaymentAmount());
|
BigDecimal pay = money(o.getPaymentAmount());
|
||||||
@@ -318,14 +477,23 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
rebate = rebate.add(reb);
|
rebate = rebate.add(reb);
|
||||||
extraCost = extraCost.add(extra);
|
extraCost = extraCost.add(extra);
|
||||||
selling = selling.add(sell);
|
selling = selling.add(sell);
|
||||||
|
ka.payment = ka.payment.add(pay);
|
||||||
|
ka.rebate = ka.rebate.add(reb);
|
||||||
|
ka.extraCost = ka.extraCost.add(extra);
|
||||||
|
ka.selling = ka.selling.add(sell);
|
||||||
if (o.getProfit() == null) {
|
if (o.getProfit() == null) {
|
||||||
missingProfitCount++;
|
missingProfitCount++;
|
||||||
|
ka.missingProfit++;
|
||||||
} else {
|
} else {
|
||||||
profit = profit.add(money(o.getProfit()));
|
BigDecimal pft = money(o.getProfit());
|
||||||
|
profit = profit.add(pft);
|
||||||
|
ka.profit = ka.profit.add(pft);
|
||||||
}
|
}
|
||||||
addGroup(byMark, blankTo(o.getDistributionMark(), "未标记"), pay, reb, extra, o.getProfit());
|
addGroup(byMark, markName, pay, reb, extra, o.getProfit(), kind);
|
||||||
addGroup(byModel, modelKey(o), pay, reb, extra, o.getProfit());
|
addGroup(byModel, modelKey(o), pay, reb, extra, o.getProfit(), null);
|
||||||
addGroup(byBuyer, buyerGroupKey(o.getBuyer()), pay, reb, extra, o.getProfit());
|
addGroup(byBuyer, buyerGroupKey(o.getBuyer()), pay, reb, extra, o.getProfit(), null);
|
||||||
|
addSplit(byModelSplit, modelKey(o), kind, pay, reb, extra, o.getProfit());
|
||||||
|
addSplit(byBuyerSplit, buyerGroupKey(o.getBuyer()), kind, pay, reb, extra, o.getProfit());
|
||||||
}
|
}
|
||||||
|
|
||||||
int valid = orderCount - refundCount;
|
int valid = orderCount - refundCount;
|
||||||
@@ -351,9 +519,12 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
? profit.multiply(BigDecimal.valueOf(100)).divide(payment, 2, RoundingMode.HALF_UP)
|
? profit.multiply(BigDecimal.valueOf(100)).divide(payment, 2, RoundingMode.HALF_UP)
|
||||||
: BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
: BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
out.put("profitRatePct", num(rate));
|
out.put("profitRatePct", num(rate));
|
||||||
|
out.put("byKind", toKindList(byKind));
|
||||||
out.put("byMark", toGroupList(byMark, Integer.MAX_VALUE));
|
out.put("byMark", toGroupList(byMark, Integer.MAX_VALUE));
|
||||||
out.put("byModel", toGroupList(byModel, TOP_N));
|
out.put("byModel", toGroupList(byModel, TOP_N));
|
||||||
out.put("byBuyer", toGroupList(byBuyer, TOP_N));
|
out.put("byBuyer", toGroupList(byBuyer, TOP_N));
|
||||||
|
out.put("byModelSplit", toGroupList(byModelSplit, TOP_N));
|
||||||
|
out.put("byBuyerSplit", toGroupList(byBuyerSplit, TOP_N));
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,6 +560,83 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
out.put(key, item);
|
out.put(key, item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> compareKinds(Map<String, Object> cur, Map<String, Object> prev) {
|
||||||
|
Map<String, Map<String, Object>> a = indexKind(cur);
|
||||||
|
Map<String, Map<String, Object>> b = indexKind(prev);
|
||||||
|
List<String> names = new ArrayList<>(Arrays.asList("自有单", "代下单", "代下单单"));
|
||||||
|
if (kindHasOrders(a.get("未标记")) || kindHasOrders(b.get("未标记"))) {
|
||||||
|
names.add("未标记");
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (String name : names) {
|
||||||
|
Map<String, Object> ca = a.get(name);
|
||||||
|
Map<String, Object> cb = b.get(name);
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("name", name);
|
||||||
|
fillKindDelta(row, "validOrderCount", "validDelta", "validChangePct", ca, cb);
|
||||||
|
fillKindDelta(row, "totalProfit", "profitDelta", "profitChangePct", ca, cb);
|
||||||
|
BigDecimal rateA = toDecimal(ca == null ? null : ca.get("profitRatePct"));
|
||||||
|
BigDecimal rateB = toDecimal(cb == null ? null : cb.get("profitRatePct"));
|
||||||
|
row.put("rateDelta", num(rateA.subtract(rateB).setScale(2, RoundingMode.HALF_UP)));
|
||||||
|
out.add(row);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fillKindDelta(Map<String, Object> row, String src, String deltaKey, String pctKey,
|
||||||
|
Map<String, Object> cur, Map<String, Object> prev) {
|
||||||
|
BigDecimal a = toDecimal(cur == null ? null : cur.get(src));
|
||||||
|
BigDecimal b = toDecimal(prev == null ? null : prev.get(src));
|
||||||
|
BigDecimal delta = a.subtract(b).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
row.put(deltaKey, num(delta));
|
||||||
|
row.put(pctKey, num(changePct(a, b, delta)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static BigDecimal changePct(BigDecimal current, BigDecimal previous, BigDecimal delta) {
|
||||||
|
if (previous.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
if (current.compareTo(BigDecimal.ZERO) == 0) {
|
||||||
|
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
return BigDecimal.valueOf(100).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
return delta.multiply(BigDecimal.valueOf(100)).divide(previous.abs(), 2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean kindHasOrders(Map<String, Object> row) {
|
||||||
|
return row != null && toDecimal(row.get("orderCount")).compareTo(BigDecimal.ZERO) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static Map<String, Map<String, Object>> indexKind(Map<String, Object> period) {
|
||||||
|
Map<String, Map<String, Object>> out = new LinkedHashMap<>();
|
||||||
|
if (period == null || !(period.get("byKind") instanceof List)) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
for (Object item : (List<?>) period.get("byKind")) {
|
||||||
|
Map<String, Object> row = castMap(item);
|
||||||
|
if (row != null && row.get("name") != null) {
|
||||||
|
out.put(String.valueOf(row.get("name")), row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String renderPrompt(String statsJson) {
|
||||||
|
String prompt = loadPrompt();
|
||||||
|
String body = statsJson;
|
||||||
|
if (prompt.contains("current.totalProfit") || prompt.contains("compare.*")) {
|
||||||
|
body = "下面 JSON 已改为列数组。忽略上文 current/previous/compare 的对象字段说明。\n"
|
||||||
|
+ "自有单、代下单(仅 F-W / F-H)、代下单单的单量、利润、利润率直接读 kind 和 kd,不要自己把 byMark 加总,也不要合并。\n"
|
||||||
|
+ "列名以 _n、_amt、_row、_kind、_d、_kd 为准。\n"
|
||||||
|
+ statsJson;
|
||||||
|
log.warn("慢单利润分析提示词仍是旧字段说明,已在数据前附加列数组说明。请用新模板覆盖 Redis 里的 order:profit_analysis。");
|
||||||
|
}
|
||||||
|
if (prompt.contains("{{stats_json}}")) {
|
||||||
|
return prompt.replace("{{stats_json}}", body);
|
||||||
|
}
|
||||||
|
return prompt + "\n\n统计数据:\n" + body;
|
||||||
|
}
|
||||||
|
|
||||||
private String loadPrompt() {
|
private String loadPrompt() {
|
||||||
if (redisTemplate != null) {
|
if (redisTemplate != null) {
|
||||||
try {
|
try {
|
||||||
@@ -571,6 +819,20 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
sb.append("付款 ").append(cur.get("totalPayment"))
|
sb.append("付款 ").append(cur.get("totalPayment"))
|
||||||
.append(formatCmp(cmp, "totalPayment"))
|
.append(formatCmp(cmp, "totalPayment"))
|
||||||
.append(" 后返 ").append(cur.get("totalRebate")).append('\n');
|
.append(" 后返 ").append(cur.get("totalRebate")).append('\n');
|
||||||
|
List<Map<String, Object>> kinds = (List<Map<String, Object>>) cur.get("byKind");
|
||||||
|
if (kinds != null && !kinds.isEmpty()) {
|
||||||
|
sb.append("分类型:");
|
||||||
|
int n = 0;
|
||||||
|
for (Map<String, Object> k : kinds) {
|
||||||
|
if (n > 0) {
|
||||||
|
sb.append(";");
|
||||||
|
}
|
||||||
|
sb.append(k.get("name")).append(' ').append(k.get("validOrderCount")).append("单/")
|
||||||
|
.append(k.get("totalProfit")).append("(").append(k.get("profitRatePct")).append("%)");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
List<Map<String, Object>> marks = (List<Map<String, Object>>) cur.get("byMark");
|
List<Map<String, Object>> marks = (List<Map<String, Object>>) cur.get("byMark");
|
||||||
if (marks != null && !marks.isEmpty()) {
|
if (marks != null && !marks.isEmpty()) {
|
||||||
sb.append("分销:");
|
sb.append("分销:");
|
||||||
@@ -623,7 +885,8 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
if ("week".equals(type)) {
|
if ("week".equals(type)) {
|
||||||
anchor = today.minusWeeks(1).with(DayOfWeek.MONDAY);
|
anchor = today.minusWeeks(1).with(DayOfWeek.MONDAY);
|
||||||
} else if ("month".equals(type)) {
|
} else if ("month".equals(type)) {
|
||||||
anchor = today.minusMonths(1).withDayOfMonth(1);
|
// 定时任务推上一完整月;页面不选月份时看本月,和订单列表里正在看的月对齐
|
||||||
|
anchor = scheduled ? today.minusMonths(1).withDayOfMonth(1) : today;
|
||||||
} else {
|
} else {
|
||||||
// 日报:当天晚上推当天,页面预览默认也是今天
|
// 日报:当天晚上推当天,页面预览默认也是今天
|
||||||
anchor = today;
|
anchor = today;
|
||||||
@@ -669,8 +932,12 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
if (StringUtils.isEmpty(date)) {
|
if (StringUtils.isEmpty(date)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
String raw = date.trim();
|
||||||
try {
|
try {
|
||||||
return LocalDate.parse(date.trim(), DAY);
|
if (raw.length() == 7) {
|
||||||
|
return LocalDate.parse(raw + "-01", DAY);
|
||||||
|
}
|
||||||
|
return LocalDate.parse(raw, DAY);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -703,9 +970,53 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String markKind(String mark) {
|
||||||
|
if (mark == null) {
|
||||||
|
return "未标记";
|
||||||
|
}
|
||||||
|
String m = mark.trim();
|
||||||
|
if (m.isEmpty() || "未标记".equals(m)) {
|
||||||
|
return "未标记";
|
||||||
|
}
|
||||||
|
if (markToken(m, "F-W") || markToken(m, "F-H")) {
|
||||||
|
return "代下单";
|
||||||
|
}
|
||||||
|
if (m.regionMatches(true, 0, "F", 0, 1)) {
|
||||||
|
return "自有单";
|
||||||
|
}
|
||||||
|
return "代下单单";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** F-W / F-H 本身,或以 F-W-、F-H- 再带后缀。F-Hello 不算 F-H。 */
|
||||||
|
private static boolean markToken(String mark, String token) {
|
||||||
|
if (mark.length() < token.length() || !mark.regionMatches(true, 0, token, 0, token.length())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mark.length() == token.length()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
char next = mark.charAt(token.length());
|
||||||
|
return next == '-' || next == '_';
|
||||||
|
}
|
||||||
|
|
||||||
private static void addGroup(Map<String, GroupAcc> map, String name, BigDecimal pay, BigDecimal reb,
|
private static void addGroup(Map<String, GroupAcc> map, String name, BigDecimal pay, BigDecimal reb,
|
||||||
BigDecimal extra, Double profit) {
|
BigDecimal extra, Double profit, String kind) {
|
||||||
GroupAcc acc = map.computeIfAbsent(name, k -> new GroupAcc());
|
GroupAcc acc = map.computeIfAbsent(name, k -> new GroupAcc());
|
||||||
|
if (kind != null) {
|
||||||
|
acc.kind = kind;
|
||||||
|
}
|
||||||
|
bump(acc, pay, reb, extra, profit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addSplit(Map<String, GroupAcc> map, String name, String kind, BigDecimal pay, BigDecimal reb,
|
||||||
|
BigDecimal extra, Double profit) {
|
||||||
|
GroupAcc acc = map.computeIfAbsent(name + "\u0001" + kind, k -> new GroupAcc());
|
||||||
|
acc.displayName = name;
|
||||||
|
acc.kind = kind;
|
||||||
|
bump(acc, pay, reb, extra, profit);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bump(GroupAcc acc, BigDecimal pay, BigDecimal reb, BigDecimal extra, Double profit) {
|
||||||
acc.orderCount++;
|
acc.orderCount++;
|
||||||
acc.payment = acc.payment.add(pay);
|
acc.payment = acc.payment.add(pay);
|
||||||
acc.rebate = acc.rebate.add(reb);
|
acc.rebate = acc.rebate.add(reb);
|
||||||
@@ -715,6 +1026,45 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<Map<String, Object>> toKindList(Map<String, KindAcc> map) {
|
||||||
|
List<String> names = new ArrayList<>(Arrays.asList("自有单", "代下单", "代下单单"));
|
||||||
|
KindAcc unmarked = map.get("未标记");
|
||||||
|
if (unmarked != null && unmarked.orderCount > 0) {
|
||||||
|
names.add("未标记");
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> out = new ArrayList<>();
|
||||||
|
for (String name : names) {
|
||||||
|
out.add(kindRow(name, map.get(name)));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> kindRow(String name, KindAcc src) {
|
||||||
|
KindAcc a = src == null ? new KindAcc() : src;
|
||||||
|
int valid = a.orderCount - a.refundCount;
|
||||||
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
|
row.put("name", name);
|
||||||
|
row.put("orderCount", a.orderCount);
|
||||||
|
row.put("refundCount", a.refundCount);
|
||||||
|
row.put("validOrderCount", valid);
|
||||||
|
row.put("missingProfitCount", a.missingProfit);
|
||||||
|
row.put("totalPayment", num(a.payment));
|
||||||
|
row.put("totalRebate", num(a.rebate));
|
||||||
|
row.put("totalCost", num(a.payment.subtract(a.rebate)));
|
||||||
|
row.put("totalSellingPrice", num(a.selling));
|
||||||
|
row.put("totalExtraCost", num(a.extraCost));
|
||||||
|
row.put("totalProfit", num(a.profit));
|
||||||
|
BigDecimal avg = valid > 0
|
||||||
|
? a.profit.divide(BigDecimal.valueOf(valid), 2, RoundingMode.HALF_UP)
|
||||||
|
: BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
row.put("avgProfit", num(avg));
|
||||||
|
BigDecimal rate = a.payment.compareTo(BigDecimal.ZERO) > 0
|
||||||
|
? a.profit.multiply(BigDecimal.valueOf(100)).divide(a.payment, 2, RoundingMode.HALF_UP)
|
||||||
|
: BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
row.put("profitRatePct", num(rate));
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Map<String, Object>> toGroupList(Map<String, GroupAcc> map, int limit) {
|
private static List<Map<String, Object>> toGroupList(Map<String, GroupAcc> map, int limit) {
|
||||||
List<Map.Entry<String, GroupAcc>> list = new ArrayList<>(map.entrySet());
|
List<Map.Entry<String, GroupAcc>> list = new ArrayList<>(map.entrySet());
|
||||||
list.sort(Comparator
|
list.sort(Comparator
|
||||||
@@ -728,7 +1078,10 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
}
|
}
|
||||||
GroupAcc a = e.getValue();
|
GroupAcc a = e.getValue();
|
||||||
Map<String, Object> row = new LinkedHashMap<>();
|
Map<String, Object> row = new LinkedHashMap<>();
|
||||||
row.put("name", e.getKey());
|
row.put("name", a.displayName != null ? a.displayName : e.getKey());
|
||||||
|
if (a.kind != null) {
|
||||||
|
row.put("kind", a.kind);
|
||||||
|
}
|
||||||
row.put("orderCount", a.orderCount);
|
row.put("orderCount", a.orderCount);
|
||||||
row.put("payment", num(a.payment));
|
row.put("payment", num(a.payment));
|
||||||
row.put("rebate", num(a.rebate));
|
row.put("rebate", num(a.rebate));
|
||||||
@@ -906,10 +1259,23 @@ public class OrderProfitAnalysisServiceImpl implements IOrderProfitAnalysisServi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class GroupAcc {
|
private static final class GroupAcc {
|
||||||
|
String displayName;
|
||||||
|
String kind;
|
||||||
int orderCount;
|
int orderCount;
|
||||||
BigDecimal payment = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
BigDecimal payment = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
BigDecimal rebate = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
BigDecimal rebate = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
BigDecimal extraCost = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
BigDecimal extraCost = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
BigDecimal profit = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
BigDecimal profit = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class KindAcc {
|
||||||
|
int orderCount;
|
||||||
|
int refundCount;
|
||||||
|
int missingProfit;
|
||||||
|
BigDecimal payment = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal rebate = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal extraCost = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal selling = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
BigDecimal profit = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public class TencentDocServiceImpl implements ITencentDocService {
|
|||||||
row.add(order.getOrderTime() != null ? sdf.format(order.getOrderTime()) : "");
|
row.add(order.getOrderTime() != null ? sdf.format(order.getOrderTime()) : "");
|
||||||
row.add(order.getModelNumber() != null ? order.getModelNumber() : "");
|
row.add(order.getModelNumber() != null ? order.getModelNumber() : "");
|
||||||
row.add(order.getAddress() != null ? order.getAddress() : "");
|
row.add(order.getAddress() != null ? order.getAddress() : "");
|
||||||
row.add(order.getLogisticsLink() != null ? order.getLogisticsLink() : "");
|
row.add(order.resolveLogisticsDocValue() != null ? order.resolveLogisticsDocValue() : "");
|
||||||
row.add(order.getBuyer() != null ? order.getBuyer() : "");
|
row.add(order.getBuyer() != null ? order.getBuyer() : "");
|
||||||
row.add(order.getPaymentAmount() != null ? order.getPaymentAmount().toString() : "");
|
row.add(order.getPaymentAmount() != null ? order.getPaymentAmount().toString() : "");
|
||||||
row.add(order.getRebateAmount() != null ? order.getRebateAmount().toString() : "");
|
row.add(order.getRebateAmount() != null ? order.getRebateAmount().toString() : "");
|
||||||
@@ -345,8 +345,9 @@ public class TencentDocServiceImpl implements ITencentDocService {
|
|||||||
if (arrangedColumn != null) {
|
if (arrangedColumn != null) {
|
||||||
requests.add(buildUpdateRequest(sheetId, rowIndex, arrangedColumn, "2", false));
|
requests.add(buildUpdateRequest(sheetId, rowIndex, arrangedColumn, "2", false));
|
||||||
}
|
}
|
||||||
if (logisticsColumn != null && order.getLogisticsLink() != null && !order.getLogisticsLink().isEmpty()) {
|
String logisticsDocValue = order.resolveLogisticsDocValue();
|
||||||
requests.add(buildUpdateRequest(sheetId, rowIndex, logisticsColumn, order.getLogisticsLink(), true)); // 超链接
|
if (logisticsColumn != null && logisticsDocValue != null) {
|
||||||
|
requests.add(buildUpdateRequest(sheetId, rowIndex, logisticsColumn, logisticsDocValue, JDOrder.isHttpUrl(logisticsDocValue)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 使用 batchUpdate 写入
|
// 5. 使用 batchUpdate 写入
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
<result property="rebateAmount" column="rebate_amount"/>
|
<result property="rebateAmount" column="rebate_amount"/>
|
||||||
<result property="address" column="address"/>
|
<result property="address" column="address"/>
|
||||||
<result property="logisticsLink" column="logistics_link"/>
|
<result property="logisticsLink" column="logistics_link"/>
|
||||||
|
<result property="waybillNo" column="waybill_no"/>
|
||||||
<result property="tencentDocPushed" column="tencent_doc_pushed"/>
|
<result property="tencentDocPushed" column="tencent_doc_pushed"/>
|
||||||
<result property="tencentDocPushTime" column="tencent_doc_push_time"/>
|
<result property="tencentDocPushTime" column="tencent_doc_push_time"/>
|
||||||
<result property="orderId" column="order_id"/>
|
<result property="orderId" column="order_id"/>
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
|
|
||||||
<sql id="selectJDOrderBase">
|
<sql id="selectJDOrderBase">
|
||||||
select id, remark, distribution_mark, model_number, model_shop, link, payment_amount, rebate_amount,
|
select id, remark, distribution_mark, model_number, model_shop, link, payment_amount, rebate_amount,
|
||||||
address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
|
address, logistics_link, waybill_no, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
|
||||||
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
|
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
|
||||||
is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date,
|
is_price_protected, price_protected_date, is_invoice_opened, invoice_opened_date, is_review_posted, review_posted_date,
|
||||||
rebate_remark_json, rebate_remark_has_abnormal,
|
rebate_remark_json, rebate_remark_has_abnormal,
|
||||||
@@ -65,6 +66,18 @@
|
|||||||
order_id like concat('%', #{params.orderSearch}, '%')
|
order_id like concat('%', #{params.orderSearch}, '%')
|
||||||
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
|
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
|
||||||
or distribution_mark like concat('%', #{params.orderSearch}, '%')
|
or distribution_mark like concat('%', #{params.orderSearch}, '%')
|
||||||
|
or waybill_no like concat('%', #{params.orderSearch}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="params.mobileQuickSearch != null and params.mobileQuickSearch != ''">
|
||||||
|
and (
|
||||||
|
remark like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or order_id like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or third_party_order_no like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or distribution_mark like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or waybill_no like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or buyer like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or address like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<if test="params.quickRecordSearch != null and params.quickRecordSearch != ''">
|
<if test="params.quickRecordSearch != null and params.quickRecordSearch != ''">
|
||||||
@@ -76,6 +89,8 @@
|
|||||||
or order_id like concat('%', #{params.quickRecordSearch}, '%')
|
or order_id like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
or third_party_order_no like concat('%', #{params.quickRecordSearch}, '%')
|
or third_party_order_no like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
or distribution_mark like concat('%', #{params.quickRecordSearch}, '%')
|
or distribution_mark like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
|
or waybill_no like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
|
or logistics_link like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
||||||
@@ -96,6 +111,7 @@
|
|||||||
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
||||||
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
|
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
|
||||||
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
|
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
|
||||||
|
<if test="waybillNo != null and waybillNo != ''"> and waybill_no like concat('%', #{waybillNo}, '%')</if>
|
||||||
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
|
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
|
||||||
<if test="orderTime != null"> and order_time = #{orderTime}</if>
|
<if test="orderTime != null"> and order_time = #{orderTime}</if>
|
||||||
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
|
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
|
||||||
@@ -135,6 +151,18 @@
|
|||||||
order_id like concat('%', #{params.orderSearch}, '%')
|
order_id like concat('%', #{params.orderSearch}, '%')
|
||||||
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
|
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
|
||||||
or distribution_mark like concat('%', #{params.orderSearch}, '%')
|
or distribution_mark like concat('%', #{params.orderSearch}, '%')
|
||||||
|
or waybill_no like concat('%', #{params.orderSearch}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
<if test="params.mobileQuickSearch != null and params.mobileQuickSearch != ''">
|
||||||
|
and (
|
||||||
|
remark like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or order_id like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or third_party_order_no like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or distribution_mark like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or waybill_no like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or buyer like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
|
or address like concat('%', #{params.mobileQuickSearch}, '%')
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<if test="params.quickRecordSearch != null and params.quickRecordSearch != ''">
|
<if test="params.quickRecordSearch != null and params.quickRecordSearch != ''">
|
||||||
@@ -146,6 +174,8 @@
|
|||||||
or order_id like concat('%', #{params.quickRecordSearch}, '%')
|
or order_id like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
or third_party_order_no like concat('%', #{params.quickRecordSearch}, '%')
|
or third_party_order_no like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
or distribution_mark like concat('%', #{params.quickRecordSearch}, '%')
|
or distribution_mark like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
|
or waybill_no like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
|
or logistics_link like concat('%', #{params.quickRecordSearch}, '%')
|
||||||
)
|
)
|
||||||
</if>
|
</if>
|
||||||
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
|
||||||
@@ -166,6 +196,7 @@
|
|||||||
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
|
||||||
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
|
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
|
||||||
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
|
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
|
||||||
|
<if test="waybillNo != null and waybillNo != ''"> and waybill_no like concat('%', #{waybillNo}, '%')</if>
|
||||||
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
|
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
|
||||||
<if test="orderTime != null"> and order_time = #{orderTime}</if>
|
<if test="orderTime != null"> and order_time = #{orderTime}</if>
|
||||||
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
|
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
|
||||||
@@ -213,7 +244,7 @@
|
|||||||
<insert id="insertJDOrder" parameterType="JDOrder" useGeneratedKeys="true" keyProperty="id">
|
<insert id="insertJDOrder" parameterType="JDOrder" useGeneratedKeys="true" keyProperty="id">
|
||||||
insert into jd_order (
|
insert into jd_order (
|
||||||
remark, distribution_mark, model_number, model_shop, link,
|
remark, distribution_mark, model_number, model_shop, link,
|
||||||
payment_amount, rebate_amount, address, logistics_link,
|
payment_amount, rebate_amount, address, logistics_link, waybill_no,
|
||||||
tencent_doc_pushed, tencent_doc_push_time,
|
tencent_doc_pushed, tencent_doc_push_time,
|
||||||
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
|
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
|
||||||
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
|
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date,
|
||||||
@@ -221,7 +252,7 @@
|
|||||||
selling_price_type, selling_price, profit, selling_price_manual, profit_manual, extra_cost
|
selling_price_type, selling_price, profit, selling_price_manual, profit_manual, extra_cost
|
||||||
) values (
|
) values (
|
||||||
#{remark}, #{distributionMark}, #{modelNumber}, #{modelShop}, #{link},
|
#{remark}, #{distributionMark}, #{modelNumber}, #{modelShop}, #{link},
|
||||||
#{paymentAmount}, #{rebateAmount}, #{address}, #{logisticsLink},
|
#{paymentAmount}, #{rebateAmount}, #{address}, #{logisticsLink}, #{waybillNo},
|
||||||
0, null,
|
0, null,
|
||||||
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice},
|
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice},
|
||||||
#{isRefunded}, #{refundDate}, #{isRefundReceived}, #{refundReceivedDate}, #{isRebateReceived}, #{rebateReceivedDate},
|
#{isRefunded}, #{refundDate}, #{isRefundReceived}, #{refundReceivedDate}, #{isRebateReceived}, #{rebateReceivedDate},
|
||||||
@@ -242,6 +273,7 @@
|
|||||||
<if test="rebateAmount != null"> rebate_amount = #{rebateAmount},</if>
|
<if test="rebateAmount != null"> rebate_amount = #{rebateAmount},</if>
|
||||||
<if test="address != null"> address = #{address},</if>
|
<if test="address != null"> address = #{address},</if>
|
||||||
<if test="logisticsLink != null"> logistics_link = #{logisticsLink},</if>
|
<if test="logisticsLink != null"> logistics_link = #{logisticsLink},</if>
|
||||||
|
<if test="waybillNo != null"> waybill_no = #{waybillNo},</if>
|
||||||
<if test="tencentDocPushed != null"> tencent_doc_pushed = #{tencentDocPushed},</if>
|
<if test="tencentDocPushed != null"> tencent_doc_pushed = #{tencentDocPushed},</if>
|
||||||
<if test="tencentDocPushTime != null"> tencent_doc_push_time = #{tencentDocPushTime},</if>
|
<if test="tencentDocPushTime != null"> tencent_doc_push_time = #{tencentDocPushTime},</if>
|
||||||
<if test="orderId != null"> order_id = #{orderId},</if>
|
<if test="orderId != null"> order_id = #{orderId},</if>
|
||||||
@@ -317,6 +349,13 @@
|
|||||||
limit 1
|
limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectJDOrderByWaybillNo" parameterType="string" resultMap="JDOrderResult">
|
||||||
|
<include refid="selectJDOrderBase"/>
|
||||||
|
where waybill_no = #{waybillNo}
|
||||||
|
order by order_time desc
|
||||||
|
limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="selectJDOrderByThirdPartyOrderNo" parameterType="string" resultMap="JDOrderResult">
|
<select id="selectJDOrderByThirdPartyOrderNo" parameterType="string" resultMap="JDOrderResult">
|
||||||
<include refid="selectJDOrderBase"/>
|
<include refid="selectJDOrderBase"/>
|
||||||
where third_party_order_no = #{thirdPartyOrderNo}
|
where third_party_order_no = #{thirdPartyOrderNo}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- 京东订单:物流运单号(手工填写或扫描回写;与 logistics_link 独立,无链接时也可录单)
|
||||||
|
ALTER TABLE jd_order ADD COLUMN waybill_no VARCHAR(128) DEFAULT NULL COMMENT '物流运单号(手工填写或扫描回写)' AFTER logistics_link;
|
||||||
Reference in New Issue
Block a user