Compare commits
92 Commits
ef286d3bd2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e78cc67476 | ||
|
|
6fb46cc203 | ||
|
|
7582868b2c | ||
|
|
9d03cca517 | ||
|
|
8b5abb44ee | ||
|
|
e75f71d37b | ||
|
|
5da74a155c | ||
|
|
a88600788a | ||
|
|
cf8008bdc1 | ||
|
|
d97a977a0e | ||
|
|
1f25cc5d15 | ||
|
|
66aa339906 | ||
|
|
acd693f122 | ||
|
|
751844493b | ||
|
|
256b54ffab | ||
|
|
0ff357148b | ||
|
|
7cd7440f1f | ||
|
|
f5f14c730f | ||
|
|
a7068053e1 | ||
|
|
babe687679 | ||
|
|
de335831d4 | ||
|
|
a10d561fcb | ||
|
|
656f3d28a9 | ||
|
|
e420aaeb9e | ||
|
|
01bea5005e | ||
|
|
c825c6b81a | ||
|
|
1446ea2432 | ||
|
|
52b8f13b2d | ||
|
|
94f319514e | ||
|
|
5205d8c155 | ||
|
|
fed0158444 | ||
|
|
24cf538475 | ||
|
|
c50975bce5 | ||
|
|
042068ccf1 | ||
|
|
52d0adfc85 | ||
|
|
ede30b5f36 | ||
|
|
ce3af838bd | ||
|
|
0205fe2c09 | ||
|
|
6f482256c5 | ||
|
|
31e7e6853b | ||
|
|
a2c4589046 | ||
|
|
16bcd45c63 | ||
|
|
e94f17973c | ||
|
|
c9876df3de | ||
|
|
2d4f933791 | ||
|
|
a22d17de73 | ||
|
|
9af1a369f7 | ||
|
|
49c855ff78 | ||
|
|
0838d16652 | ||
|
|
6a43af0a34 | ||
|
|
1b4a73cd25 | ||
|
|
f02de5950e | ||
|
|
2b9cd6dd2e | ||
|
|
d0d51df465 | ||
|
|
9e0c6d88b1 | ||
|
|
8a77598c88 | ||
|
|
43e44c8f7f | ||
|
|
c3cafdcbe0 | ||
|
|
6fbfecf690 | ||
|
|
bce83f680c | ||
|
|
74e7990947 | ||
|
|
c31a34f519 | ||
|
|
ca452882b4 | ||
|
|
07c9aac9e6 | ||
|
|
3e37587074 | ||
|
|
fa7e26cf6e | ||
|
|
d4fdf076e9 | ||
|
|
9c503464c1 | ||
|
|
6b88e5376e | ||
|
|
2e5540904f | ||
|
|
c841990b49 | ||
|
|
72d5856838 | ||
|
|
d361e93895 | ||
|
|
9b45142cca | ||
|
|
72008d7de1 | ||
|
|
94b65fb760 | ||
|
|
2fb9777342 | ||
|
|
75d7c8e6de | ||
|
|
9ae74c999e | ||
|
|
921c8a2374 | ||
|
|
6a88a68320 | ||
|
|
a515ec33fb | ||
|
|
f2f6d02b2f | ||
|
|
9f3fb23a91 | ||
|
|
312a068bd3 | ||
|
|
3d5ee6e624 | ||
|
|
066ab35a17 | ||
|
|
318cef274e | ||
|
|
175cd3ba01 | ||
|
|
6ecedf91b3 | ||
|
|
b37865a676 | ||
|
|
918f737c94 |
7
pom.xml
7
pom.xml
@@ -35,6 +35,7 @@
|
||||
<logback.version>1.2.13</logback.version>
|
||||
<spring-security.version>5.7.12</spring-security.version>
|
||||
<spring-framework.version>5.3.39</spring-framework.version>
|
||||
<rocketmq-spring.version>2.2.3</rocketmq-spring.version>
|
||||
</properties>
|
||||
|
||||
<!-- 依赖声明 -->
|
||||
@@ -218,6 +219,12 @@
|
||||
<version>${ruoyi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<version>${rocketmq-spring.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/**
|
||||
@@ -14,6 +15,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
*/
|
||||
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
|
||||
@EnableScheduling
|
||||
@EnableAsync
|
||||
public class RuoYiApplication
|
||||
{
|
||||
public static void main(String[] args)
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.ruoyi.jarvis.wecom;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 调用 wxSend 的企微应用文本主动推送(POST /wecom/active-push)。
|
||||
*/
|
||||
@Component
|
||||
public class WxSendWeComPushClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WxSendWeComPushClient.class);
|
||||
|
||||
public static final String HEADER_PUSH_SECRET = "X-WxSend-WeCom-Push-Secret";
|
||||
|
||||
@Value("${jarvis.wecom.wxsend-base-url:}")
|
||||
private String wxsendBaseUrl;
|
||||
|
||||
@Value("${jarvis.wecom.push-secret:}")
|
||||
private String pushSecret;
|
||||
|
||||
/**
|
||||
* 在被动回复返回后延迟再发,保证企微侧先出现首条被动消息。
|
||||
*/
|
||||
public void scheduleActivePushes(String toUser, List<String> contents) {
|
||||
if (!StringUtils.hasText(wxsendBaseUrl) || !StringUtils.hasText(pushSecret)
|
||||
|| !StringUtils.hasText(toUser) || contents == null || contents.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final String userId = toUser.trim();
|
||||
final List<String> list = new ArrayList<>(contents);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
Thread.sleep(450);
|
||||
String base = normalizeBase(wxsendBaseUrl);
|
||||
String url = base + "/wecom/active-push";
|
||||
for (String c : list) {
|
||||
if (!StringUtils.hasText(c)) {
|
||||
continue;
|
||||
}
|
||||
postJson(url, userId, c.trim());
|
||||
Thread.sleep(120);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("企微主动推送任务异常 userId={} msg={}", userId, e.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static String normalizeBase(String base) {
|
||||
String b = base.trim();
|
||||
if (b.endsWith("/")) {
|
||||
return b.substring(0, b.length() - 1);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private void postJson(String url, String toUser, String content) {
|
||||
JSONObject body = new JSONObject();
|
||||
body.put("toUser", toUser);
|
||||
body.put("content", content);
|
||||
byte[] bytes = body.toJSONString().getBytes(StandardCharsets.UTF_8);
|
||||
HttpURLConnection conn = null;
|
||||
try {
|
||||
conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setConnectTimeout(15000);
|
||||
conn.setReadTimeout(60000);
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
|
||||
conn.setRequestProperty(HEADER_PUSH_SECRET, pushSecret);
|
||||
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);
|
||||
if (code < 200 || code >= 300) {
|
||||
log.warn("wxSend active-push HTTP {} body={}", code, resp);
|
||||
} else {
|
||||
log.debug("wxSend active-push OK http={} resp={}", code, resp);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("wxSend active-push 请求失败 url={} err={}", url, e.toString());
|
||||
} finally {
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,129 @@
|
||||
package com.ruoyi.web.controller.common;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.R;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.erp.request.IERPAccount;
|
||||
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
|
||||
import com.ruoyi.jarvis.service.erp.ErpAccountResolver;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* 开放平台回调接收端
|
||||
* 注意:/product/receive 与 /order/receive 为示例路径,请在开放平台配置时使用你自己的正式回调地址
|
||||
* 闲管家开放平台推送回调(请在开放平台填写真实 URL)
|
||||
* 订单:POST .../open/callback/order/receive?appid=×tamp=&sign=
|
||||
* <p>
|
||||
* 成功/失败体须与《订单推送通知》OpenAPI 一致:{@code result=success|fail} + {@code msg};
|
||||
* 仅当 {@code result} 为 success 时平台认为接收成功(失败最多重试 3 次;建议业务异步处理、快速返回)。
|
||||
*/
|
||||
@Anonymous
|
||||
@RestController
|
||||
@RequestMapping("/open/callback")
|
||||
public class OpenCallbackController extends BaseController {
|
||||
public class OpenCallbackController {
|
||||
|
||||
@Resource
|
||||
private ErpAccountResolver erpAccountResolver;
|
||||
|
||||
@Resource
|
||||
private IErpGoofishOrderService erpGoofishOrderService;
|
||||
|
||||
@PostMapping("/product/receive")
|
||||
public JSONObject receiveProductCallback(
|
||||
@RequestParam("appid") String appid,
|
||||
@RequestParam(value = "timestamp", required = false) Long timestamp,
|
||||
@RequestParam(value = "seller_id", required = false) Long sellerId,
|
||||
@RequestParam("sign") String sign,
|
||||
@RequestBody JSONObject body
|
||||
@RequestBody(required = false) String rawBody
|
||||
) {
|
||||
if (!verifySign(appid, timestamp, sign, body)) {
|
||||
JSONObject fail = new JSONObject();
|
||||
fail.put("result", "fail");
|
||||
fail.put("msg", "签名失败");
|
||||
return fail;
|
||||
String normalizedBody = normalizeJsonBody(rawBody);
|
||||
IERPAccount account = erpAccountResolver.resolveStrict(appid);
|
||||
if (!verifyGoofishSign(account, timestamp, sellerId, sign, normalizedBody)) {
|
||||
return failCallback("签名失败");
|
||||
}
|
||||
JSONObject ok = new JSONObject();
|
||||
ok.put("result", "success");
|
||||
ok.put("msg", "接收成功");
|
||||
return ok;
|
||||
return successCallback();
|
||||
}
|
||||
|
||||
@PostMapping("/order/receive")
|
||||
public JSONObject receiveOrderCallback(
|
||||
@RequestParam("appid") String appid,
|
||||
@RequestParam(value = "timestamp", required = false) Long timestamp,
|
||||
@RequestParam(value = "seller_id", required = false) Long sellerId,
|
||||
@RequestParam("sign") String sign,
|
||||
@RequestBody JSONObject body
|
||||
@RequestBody(required = false) String rawBody
|
||||
) {
|
||||
if (!verifySign(appid, timestamp, sign, body)) {
|
||||
JSONObject fail = new JSONObject();
|
||||
fail.put("result", "fail");
|
||||
fail.put("msg", "签名失败");
|
||||
return fail;
|
||||
String normalizedBody = normalizeJsonBody(rawBody);
|
||||
IERPAccount account = erpAccountResolver.resolveStrict(appid);
|
||||
if (account == null) {
|
||||
return failCallback("未找到启用的 AppKey 配置");
|
||||
}
|
||||
if (!verifyGoofishSign(account, timestamp, sellerId, sign, normalizedBody)) {
|
||||
return failCallback("签名失败");
|
||||
}
|
||||
JSONObject body;
|
||||
try {
|
||||
body = "{}".equals(normalizedBody) ? new JSONObject() : JSON.parseObject(normalizedBody);
|
||||
} catch (Exception e) {
|
||||
return failCallback("请求体不是合法JSON");
|
||||
}
|
||||
try {
|
||||
erpGoofishOrderService.publishOrProcessNotify(appid, timestamp, body);
|
||||
} catch (Exception e) {
|
||||
return failCallback("入队异常");
|
||||
}
|
||||
return successCallback();
|
||||
}
|
||||
|
||||
/**
|
||||
* 与开放平台示例一致:签名字符串 = md5(appKey + "," + md5(body原文) + "," + timestamp + [,sellerId] + "," + appSecret)
|
||||
* body 须与推送原文完全一致后做 MD5,不能用解析后再 toJSONString(字段顺序变化会导致验签失败)。
|
||||
*/
|
||||
private boolean verifyGoofishSign(IERPAccount account, Long timestamp, Long sellerId, String sign, String bodyExactForMd5) {
|
||||
if (account == null || StringUtils.isEmpty(sign)) {
|
||||
return false;
|
||||
}
|
||||
String jsonForMd5 = bodyExactForMd5 == null || bodyExactForMd5.isEmpty() ? "{}" : bodyExactForMd5;
|
||||
String bodyMd5 = md5Hex(jsonForMd5);
|
||||
long ts = timestamp == null ? 0L : timestamp;
|
||||
String data;
|
||||
if (sellerId != null) {
|
||||
data = account.getApiKey() + "," + bodyMd5 + "," + ts + "," + sellerId + "," + account.getApiKeySecret();
|
||||
} else {
|
||||
data = account.getApiKey() + "," + bodyMd5 + "," + ts + "," + account.getApiKeySecret();
|
||||
}
|
||||
String local = md5Hex(data);
|
||||
return StringUtils.equalsIgnoreCase(local, sign.trim());
|
||||
}
|
||||
|
||||
private static String normalizeJsonBody(String rawBody) {
|
||||
if (rawBody == null) {
|
||||
return "{}";
|
||||
}
|
||||
String t = rawBody.trim();
|
||||
return t.isEmpty() ? "{}" : t;
|
||||
}
|
||||
|
||||
/** 与《订单推送通知》notify_resp_ok 一致:result=success 时平台才停止重试 */
|
||||
private static JSONObject successCallback() {
|
||||
JSONObject ok = new JSONObject();
|
||||
ok.put("result", "success");
|
||||
ok.put("msg", "接收成功");
|
||||
return ok;
|
||||
}
|
||||
|
||||
private boolean verifySign(String appid, Long timestamp, String sign, JSONObject body) {
|
||||
// TODO: 这里需要根据appid查出对应的 appKey/appSecret
|
||||
// 为了示例,直接使用 ERPAccount.ACCOUNT_HUGE 的常量。生产请替换为从数据库/配置读取
|
||||
String appKey = "1016208368633221";
|
||||
String appSecret = "waLiRMgFcixLbcLjUSSwo370Hp1nBcBu";
|
||||
|
||||
String json = body == null ? "{}" : body.toJSONString();
|
||||
String data = appKey + "," + md5(json) + "," + (timestamp == null ? 0 : timestamp) + "," + appSecret;
|
||||
String local = md5(data);
|
||||
return StringUtils.equalsIgnoreCase(local, sign);
|
||||
/** 与 notify_resp_fail 一致:result=fail */
|
||||
private static JSONObject failCallback(String msg) {
|
||||
JSONObject j = new JSONObject();
|
||||
j.put("result", "fail");
|
||||
j.put("msg", msg == null || msg.isEmpty() ? "处理失败" : msg);
|
||||
return j;
|
||||
}
|
||||
|
||||
private String md5(String str) {
|
||||
private String md5Hex(String str) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] digest = md.digest(str.getBytes(StandardCharsets.UTF_8));
|
||||
@@ -82,5 +137,3 @@ public class OpenCallbackController extends BaseController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@ import com.ruoyi.common.utils.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import com.ruoyi.erp.request.ERPAccount;
|
||||
import com.ruoyi.erp.request.IERPAccount;
|
||||
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
||||
import com.ruoyi.jarvis.service.IErpOpenConfigService;
|
||||
import com.ruoyi.jarvis.service.erp.ErpAccountResolver;
|
||||
import com.ruoyi.erp.request.ProductCreateRequest;
|
||||
import com.ruoyi.erp.request.ProductCategoryListQueryRequest;
|
||||
import com.ruoyi.erp.request.ProductPropertyListQueryRequest;
|
||||
@@ -20,6 +24,7 @@ import com.ruoyi.erp.request.ProductDownShelfRequest;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.validation.constraints.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
@@ -36,13 +41,19 @@ public class ProductController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IOuterIdGeneratorService outerIdGeneratorService;
|
||||
|
||||
@Resource
|
||||
private ErpAccountResolver erpAccountResolver;
|
||||
|
||||
@Resource
|
||||
private IErpOpenConfigService erpOpenConfigService;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ProductController.class);
|
||||
|
||||
@PostMapping("/createByPromotion")
|
||||
public R<?> createByPromotion(@RequestBody @Validated CreateProductFromPromotionRequest req) {
|
||||
try {
|
||||
ERPAccount account = resolveAccount(req.getAppid());
|
||||
IERPAccount account = resolveAccount(req.getAppid());
|
||||
// 1) 组装 ERPShop
|
||||
ERPShop erpShop = new ERPShop();
|
||||
erpShop.setChannelCatid(req.getChannelCatId());
|
||||
@@ -143,7 +154,7 @@ public class ProductController extends BaseController {
|
||||
@PostMapping("/publish")
|
||||
public R<?> publish(@RequestBody @Validated PublishRequest req) {
|
||||
try {
|
||||
ERPAccount account = resolveAccount(req.getAppid());
|
||||
IERPAccount account = resolveAccount(req.getAppid());
|
||||
ProductPublishRequest publishRequest = new ProductPublishRequest(account);
|
||||
publishRequest.setProductId(req.getProductId());
|
||||
publishRequest.setUserName(req.getUserName());
|
||||
@@ -344,10 +355,22 @@ public class ProductController extends BaseController {
|
||||
String name = firstNonBlank(row.getString("user_name"), row.getString("xy_name"), row.getString("username"), row.getString("nick"));
|
||||
if (name != null) {
|
||||
String label = name;
|
||||
for (ERPAccount a : ERPAccount.values()) {
|
||||
if (name.equals(a.getXyName())) {
|
||||
label = name + "(" + a.getRemark() + ")";
|
||||
break;
|
||||
List<ErpOpenConfig> cfgs = erpOpenConfigService.selectEnabledOrderBySort();
|
||||
if (cfgs != null) {
|
||||
for (ErpOpenConfig c : cfgs) {
|
||||
if (name.equals(c.getXyUserName())) {
|
||||
String r = c.getRemark() != null ? c.getRemark() : c.getAppKey();
|
||||
label = name + "(" + r + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (label.equals(name)) {
|
||||
for (ERPAccount a : ERPAccount.values()) {
|
||||
if (name.equals(a.getXyName())) {
|
||||
label = name + "(" + a.getRemark() + ")";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
options.add(new Option(name, label));
|
||||
@@ -376,20 +399,24 @@ public class ProductController extends BaseController {
|
||||
@GetMapping("/ERPAccount")
|
||||
public R<?> erpAccounts() {
|
||||
java.util.List<Option> list = new java.util.ArrayList<>();
|
||||
List<ErpOpenConfig> cfgs = erpOpenConfigService.selectEnabledOrderBySort();
|
||||
if (cfgs != null) {
|
||||
for (ErpOpenConfig c : cfgs) {
|
||||
String label = StringUtils.isNotEmpty(c.getRemark()) ? c.getRemark() : c.getXyUserName();
|
||||
if (StringUtils.isEmpty(label)) {
|
||||
label = c.getAppKey();
|
||||
}
|
||||
list.add(new Option(c.getAppKey(), "【配置】" + label));
|
||||
}
|
||||
}
|
||||
for (ERPAccount a : ERPAccount.values()) {
|
||||
// 仅显示备注作为 label,value 仍为 appid
|
||||
list.add(new Option(a.getApiKey(), a.getRemark()));
|
||||
list.add(new Option(a.getApiKey(), "【内置】" + a.getRemark()));
|
||||
}
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
private ERPAccount resolveAccount(String appid) {
|
||||
if (appid != null && !appid.isEmpty()) {
|
||||
for (ERPAccount a : ERPAccount.values()) {
|
||||
if (a.getApiKey().equals(appid)) return a;
|
||||
}
|
||||
}
|
||||
return ERPAccount.ACCOUNT_HUGE;
|
||||
private IERPAccount resolveAccount(String appid) {
|
||||
return erpAccountResolver.resolve(appid);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -558,7 +585,7 @@ public class ProductController extends BaseController {
|
||||
@PostMapping("/downShelf")
|
||||
public R<?> downShelf(@RequestBody @Validated DownShelfRequest req) {
|
||||
try {
|
||||
ERPAccount account = resolveAccount(req.getAppid());
|
||||
IERPAccount account = resolveAccount(req.getAppid());
|
||||
ProductDownShelfRequest downShelfRequest = new ProductDownShelfRequest(account);
|
||||
downShelfRequest.setProductId(req.getProductId());
|
||||
String resp = downShelfRequest.getResponseBody();
|
||||
@@ -576,7 +603,7 @@ public class ProductController extends BaseController {
|
||||
@PostMapping("/batchPublish")
|
||||
public R<?> batchPublish(@RequestBody @Validated BatchPublishRequest req) {
|
||||
try {
|
||||
ERPAccount account = resolveAccount(req.getAppid());
|
||||
IERPAccount account = resolveAccount(req.getAppid());
|
||||
List<Long> productIds = req.getProductIds();
|
||||
|
||||
if (productIds == null || productIds.isEmpty()) {
|
||||
@@ -651,7 +678,7 @@ public class ProductController extends BaseController {
|
||||
@PostMapping("/batchDownShelf")
|
||||
public R<?> batchDownShelf(@RequestBody @Validated BatchDownShelfRequest req) {
|
||||
try {
|
||||
ERPAccount account = resolveAccount(req.getAppid());
|
||||
IERPAccount account = resolveAccount(req.getAppid());
|
||||
List<Long> productIds = req.getProductIds();
|
||||
|
||||
if (productIds == null || productIds.isEmpty()) {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/erpGoofishOrder")
|
||||
public class ErpGoofishOrderController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private IErpGoofishOrderService erpGoofishOrderService;
|
||||
|
||||
@Resource
|
||||
private JarvisGoofishProperties goofishProperties;
|
||||
|
||||
/**
|
||||
* 订单变更日志全表检索(跨订单排查),须配置在 /{id} 之前避免歧义;对应日志标记 [goofish-order-event]
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:list')")
|
||||
@GetMapping("/eventLog/list")
|
||||
public TableDataInfo eventLogList(ErpGoofishOrderEventLogQuery query) {
|
||||
startPage();
|
||||
List<ErpGoofishOrderEventLog> list = erpGoofishOrderService.selectEventLogList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ErpGoofishOrder query) {
|
||||
startPage();
|
||||
List<ErpGoofishOrder> list = erpGoofishOrderService.selectList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:query')")
|
||||
@GetMapping("/{id}")
|
||||
public AjaxResult getInfo(@PathVariable Long id) {
|
||||
return AjaxResult.success(erpGoofishOrderService.selectById(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:query')")
|
||||
@GetMapping("/{id}/eventLogs")
|
||||
public AjaxResult eventLogs(@PathVariable Long id) {
|
||||
List<ErpGoofishOrderEventLog> list = erpGoofishOrderService.listEventLogsByOrderId(id);
|
||||
return AjaxResult.success(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:edit')")
|
||||
@Log(title = "闲管家拉单", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/pull/{appKey}")
|
||||
public AjaxResult pullOne(@PathVariable String appKey, @RequestParam(value = "hours", required = false) Integer hours) {
|
||||
int h = hours == null ? goofishProperties.getPullLookbackHours() : hours;
|
||||
int n = erpGoofishOrderService.pullOrdersForAppKey(appKey, h);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("processedItems", n);
|
||||
data.put("lookbackHours", h);
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:edit')")
|
||||
@Log(title = "闲管家全量拉单", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/pullAll")
|
||||
public AjaxResult pullAll(@RequestParam(value = "hours", required = false) Integer hours) {
|
||||
int h = hours == null ? goofishProperties.getPullLookbackHours() : hours;
|
||||
int n = erpGoofishOrderService.pullAllEnabled(h);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("processedItems", n);
|
||||
data.put("lookbackHours", h);
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:edit')")
|
||||
@Log(title = "闲管家历史全量拉单", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/pull/{appKey}/full")
|
||||
public AjaxResult pullOneFull(@PathVariable String appKey) {
|
||||
int n = erpGoofishOrderService.pullOrdersForAppKeyFullHistory(appKey);
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("processedItems", n);
|
||||
data.put("pullFullHistoryDays", goofishProperties.getPullFullHistoryDays());
|
||||
data.put("pullTimeChunkSeconds", goofishProperties.getPullTimeChunkSeconds());
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:edit')")
|
||||
@Log(title = "闲管家全账号历史全量拉单", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/pullAll/full")
|
||||
public AjaxResult pullAllFull() {
|
||||
int n = erpGoofishOrderService.pullAllEnabledFullHistory();
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("processedItems", n);
|
||||
data.put("pullFullHistoryDays", goofishProperties.getPullFullHistoryDays());
|
||||
data.put("pullTimeChunkSeconds", goofishProperties.getPullTimeChunkSeconds());
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:edit')")
|
||||
@Log(title = "闲管家订单详情刷新", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/refreshDetail/{id}")
|
||||
public AjaxResult refreshDetail(@PathVariable Long id) {
|
||||
erpGoofishOrderService.refreshDetail(id);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpGoofishOrder:edit')")
|
||||
@Log(title = "闲管家重试发货", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/retryShip/{id}")
|
||||
public AjaxResult retryShip(@PathVariable Long id) {
|
||||
erpGoofishOrderService.retryShip(id);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.erp.request.ExpressCompaniesQueryRequest;
|
||||
import com.ruoyi.erp.request.IERPAccount;
|
||||
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
||||
import com.ruoyi.jarvis.service.IErpOpenConfigService;
|
||||
import com.ruoyi.jarvis.service.erp.ErpAccountResolver;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/erpOpenConfig")
|
||||
public class ErpOpenConfigController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private IErpOpenConfigService erpOpenConfigService;
|
||||
|
||||
@Resource
|
||||
private ErpAccountResolver erpAccountResolver;
|
||||
|
||||
/**
|
||||
* 查询闲管家快递公司列表(POST body 固定为 {},与签名规则一致)
|
||||
*
|
||||
* @param appKey 与配置中心一致;不传则按 {@link ErpAccountResolver#resolve(String)} 默认账号
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpOpenConfig:list')")
|
||||
@GetMapping("/expressCompanies")
|
||||
public AjaxResult expressCompanies(@RequestParam(required = false) String appKey) {
|
||||
try {
|
||||
IERPAccount cred = erpAccountResolver.resolve(appKey);
|
||||
ExpressCompaniesQueryRequest req = new ExpressCompaniesQueryRequest(cred);
|
||||
String resp = req.getResponseBody();
|
||||
JSONObject o = JSONObject.parseObject(resp);
|
||||
if (o == null) {
|
||||
return AjaxResult.error("开放平台返回空");
|
||||
}
|
||||
if (o.getIntValue("code") != 0) {
|
||||
return AjaxResult.error(o.getString("msg") != null ? o.getString("msg") : resp);
|
||||
}
|
||||
return AjaxResult.success(o.get("data"));
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error("调用快递公司接口失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpOpenConfig:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(ErpOpenConfig query) {
|
||||
startPage();
|
||||
List<ErpOpenConfig> list = erpOpenConfigService.selectList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpOpenConfig:query')")
|
||||
@GetMapping("/{id}")
|
||||
public AjaxResult getInfo(@PathVariable Long id) {
|
||||
return AjaxResult.success(erpOpenConfigService.selectById(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpOpenConfig:add')")
|
||||
@Log(title = "闲管家应用配置", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public AjaxResult add(@RequestBody ErpOpenConfig row) {
|
||||
return toAjax(erpOpenConfigService.insert(row));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpOpenConfig:edit')")
|
||||
@Log(title = "闲管家应用配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody ErpOpenConfig row) {
|
||||
return toAjax(erpOpenConfigService.update(row));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpOpenConfig:remove')")
|
||||
@Log(title = "闲管家应用配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Long[] ids) {
|
||||
int n = 0;
|
||||
if (ids != null) {
|
||||
for (Long id : ids) {
|
||||
n += erpOpenConfigService.deleteById(id);
|
||||
}
|
||||
}
|
||||
return toAjax(n);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import java.util.List;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -17,7 +18,9 @@ import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.jarvis.domain.ErpProduct;
|
||||
import com.ruoyi.jarvis.domain.ErpProductExportRow;
|
||||
import com.ruoyi.jarvis.service.IErpProductService;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
|
||||
@@ -51,12 +54,14 @@ public class ErpProductController extends BaseController
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:export')")
|
||||
@Log(title = "闲鱼商品", businessType = BusinessType.EXPORT)
|
||||
@GetMapping("/export")
|
||||
public AjaxResult export(ErpProduct erpProduct)
|
||||
@PostMapping("/export")
|
||||
public void export(HttpServletResponse response, ErpProduct erpProduct)
|
||||
{
|
||||
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
|
||||
ExcelUtil<ErpProduct> util = new ExcelUtil<ErpProduct>(ErpProduct.class);
|
||||
return util.exportExcel(list, "闲鱼商品数据");
|
||||
String batchAt = DateUtils.getTime();
|
||||
List<ErpProductExportRow> rows = ErpProductExportRow.fromList(list, batchAt);
|
||||
ExcelUtil<ErpProductExportRow> util = new ExcelUtil<ErpProductExportRow>(ErpProductExportRow.class);
|
||||
util.exportExcel(response, rows, "闲鱼商品_AI明细");
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,8 +28,8 @@ public class InstructionController extends BaseController {
|
||||
public AjaxResult execute(@RequestBody Map<String, Object> body) {
|
||||
String cmd = body != null ? (body.get("command") != null ? String.valueOf(body.get("command")) : null) : null;
|
||||
boolean forceGenerate = body != null && body.get("forceGenerate") != null && Boolean.parseBoolean(String.valueOf(body.get("forceGenerate")));
|
||||
// 控制台入口,传递 isFromConsole=true,跳过订单查询校验
|
||||
java.util.List<String> result = instructionService.execute(cmd, forceGenerate, true);
|
||||
// 控制台入口:全量统计视角(排除后台标记不参与统计的联盟),非单个企微成员
|
||||
java.util.List<String> result = instructionService.execute(cmd, forceGenerate, true, null);
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.ruoyi.common.utils.http.HttpUtils;
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.domain.OrderRows;
|
||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||
import com.ruoyi.jarvis.service.IOrderRowsService;
|
||||
import com.ruoyi.jarvis.service.IGiftCouponService;
|
||||
import com.ruoyi.jarvis.domain.GiftCoupon;
|
||||
@@ -41,6 +42,7 @@ public class JDOrderController extends BaseController {
|
||||
private final IOrderRowsService orderRowsService;
|
||||
private final IGiftCouponService giftCouponService;
|
||||
private final ISysConfigService sysConfigService;
|
||||
private final ILogisticsService logisticsService;
|
||||
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
|
||||
private static final java.util.regex.Pattern URL_DETECT_PATTERN = java.util.regex.Pattern.compile(
|
||||
"(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)",
|
||||
@@ -53,11 +55,13 @@ public class JDOrderController extends BaseController {
|
||||
java.util.regex.Pattern.CASE_INSENSITIVE);
|
||||
|
||||
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService,
|
||||
IGiftCouponService giftCouponService, ISysConfigService sysConfigService) {
|
||||
IGiftCouponService giftCouponService, ISysConfigService sysConfigService,
|
||||
ILogisticsService logisticsService) {
|
||||
this.jdOrderService = jdOrderService;
|
||||
this.orderRowsService = orderRowsService;
|
||||
this.giftCouponService = giftCouponService;
|
||||
this.sysConfigService = sysConfigService;
|
||||
this.logisticsService = logisticsService;
|
||||
}
|
||||
|
||||
private final static String skey = "2192057370ef8140c201079969c956a3";
|
||||
@@ -68,12 +72,6 @@ public class JDOrderController extends BaseController {
|
||||
@Value("${jarvis.server.jarvis-java.jd-api-path:/jd}")
|
||||
private String jdApiPath;
|
||||
|
||||
@Value("${jarvis.server.logistics.base-url:http://127.0.0.1:5001}")
|
||||
private String logisticsBaseUrl;
|
||||
|
||||
@Value("${jarvis.server.logistics.fetch-path:/fetch_logistics}")
|
||||
private String logisticsFetchPath;
|
||||
|
||||
/**
|
||||
* 获取JD接口请求URL
|
||||
*/
|
||||
@@ -947,9 +945,7 @@ public class JDOrderController extends BaseController {
|
||||
logger.info("手动获取物流信息 - 订单ID: {}, 订单号: {}, 分销标识: {}, 物流链接: {}",
|
||||
orderId, order.getOrderId(), distributionMark, logisticsLink);
|
||||
|
||||
// 构建外部接口URL
|
||||
String externalUrl = logisticsBaseUrl + logisticsFetchPath + "?tracking_url=" +
|
||||
java.net.URLEncoder.encode(logisticsLink, "UTF-8");
|
||||
String externalUrl = logisticsService.buildFetchLogisticsRequestUrl(logisticsLink);
|
||||
|
||||
logger.info("准备调用外部接口 - URL: {}", externalUrl);
|
||||
|
||||
@@ -1057,22 +1053,30 @@ public class JDOrderController extends BaseController {
|
||||
}
|
||||
|
||||
try {
|
||||
// 构建配置键名
|
||||
String configKey = CONFIG_KEY_PREFIX + distributionMark.trim();
|
||||
|
||||
// 从系统配置中获取接收人列表
|
||||
String trimmed = distributionMark.trim();
|
||||
String configKey = CONFIG_KEY_PREFIX + trimmed;
|
||||
String configValue = sysConfigService.selectConfigByKey(configKey);
|
||||
|
||||
if (StringUtils.hasText(configValue)) {
|
||||
// 清理配置值(去除空格)
|
||||
String touser = configValue.trim().replaceAll(",\\s+", ",");
|
||||
logger.info("从配置获取接收人列表 - 分销标识: {}, 配置键: {}, 接收人: {}",
|
||||
distributionMark, configKey, touser);
|
||||
return touser;
|
||||
} else {
|
||||
logger.debug("未找到接收人配置 - 分销标识: {}, 配置键: {}", distributionMark, configKey);
|
||||
return null;
|
||||
}
|
||||
if (trimmed.startsWith("F-") || "F".equals(trimmed)) {
|
||||
if (!trimmed.equals("F")) {
|
||||
String fallbackKey = CONFIG_KEY_PREFIX + "F";
|
||||
String fallbackVal = sysConfigService.selectConfigByKey(fallbackKey);
|
||||
if (StringUtils.hasText(fallbackVal)) {
|
||||
String touser = fallbackVal.trim().replaceAll(",\\s+", ",");
|
||||
logger.info("从配置获取接收人列表(F 系回退) - 分销标识: {}, 配置键: {}, 接收人: {}",
|
||||
distributionMark, fallbackKey, touser);
|
||||
return touser;
|
||||
}
|
||||
}
|
||||
}
|
||||
logger.debug("未找到接收人配置 - 分销标识: {}, 配置键: {}", distributionMark, configKey);
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
logger.error("获取接收人配置失败 - 分销标识: {}, 错误: {}", distributionMark, e.getMessage(), e);
|
||||
return null;
|
||||
|
||||
@@ -14,6 +14,13 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
/**
|
||||
* 金山文档 OAuth 回调(独立路径,避免前端路由拦截)
|
||||
* 回调地址示例:https://your-domain/kdocs-callback
|
||||
@@ -30,26 +37,57 @@ public class KdocsCallbackController extends BaseController {
|
||||
|
||||
@Anonymous
|
||||
@GetMapping
|
||||
public ResponseEntity<?> oauthCallbackGet(@RequestParam(value = "code", required = false) String code,
|
||||
public ResponseEntity<?> oauthCallbackGet(HttpServletRequest request,
|
||||
@RequestParam(value = "code", required = false) String code,
|
||||
@RequestParam(value = "state", required = false) String state,
|
||||
@RequestParam(value = "error", required = false) String error,
|
||||
@RequestParam(value = "error_description", required = false) String errorDescription) {
|
||||
return handleOAuthCallback(code, state, error, errorDescription);
|
||||
return handleOAuthCallback(request, code, state, error, errorDescription);
|
||||
}
|
||||
|
||||
/**
|
||||
* 部分开放平台校验可能使用 POST。
|
||||
* 部分开放平台校验可能使用 POST;JSON body 时需回显 challenge 等字段。
|
||||
*/
|
||||
@Anonymous
|
||||
@PostMapping
|
||||
public ResponseEntity<?> oauthCallbackPost(@RequestParam(value = "code", required = false) String code,
|
||||
@RequestParam(value = "state", required = false) String state,
|
||||
@RequestParam(value = "error", required = false) String error,
|
||||
@RequestParam(value = "error_description", required = false) String errorDescription) {
|
||||
return handleOAuthCallback(code, state, error, errorDescription);
|
||||
public ResponseEntity<?> oauthCallbackPost(HttpServletRequest request) throws IOException {
|
||||
String ct = StringUtils.defaultString(request.getContentType()).toLowerCase();
|
||||
if (ct.contains("application/json")) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader r = request.getReader()) {
|
||||
char[] buf = new char[4096];
|
||||
int n;
|
||||
while ((n = r.read(buf)) != -1) {
|
||||
sb.append(buf, 0, n);
|
||||
}
|
||||
}
|
||||
String raw = sb.toString();
|
||||
if (StringUtils.isNotBlank(raw)) {
|
||||
try {
|
||||
JSONObject o = JSON.parseObject(raw);
|
||||
if (o != null && o.containsKey("code")) {
|
||||
Object cv = o.get("code");
|
||||
if (cv != null) {
|
||||
String c = String.valueOf(cv);
|
||||
if (StringUtils.isNotBlank(c) && !"null".equals(c)) {
|
||||
return handleOAuthCallback(request, c, o.getString("state"), o.getString("error"), o.getString("error_description"));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("解析 OAuth POST JSON: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return KdocsCallbackProbeResponses.callbackReadyJson(request, raw);
|
||||
}
|
||||
String code = request.getParameter("code");
|
||||
String state = request.getParameter("state");
|
||||
String error = request.getParameter("error");
|
||||
String errorDescription = request.getParameter("error_description");
|
||||
return handleOAuthCallback(request, code, state, error, errorDescription);
|
||||
}
|
||||
|
||||
private ResponseEntity<?> handleOAuthCallback(String code, String state, String error, String errorDescription) {
|
||||
private ResponseEntity<?> handleOAuthCallback(HttpServletRequest request, String code, String state, String error, String errorDescription) {
|
||||
try {
|
||||
if (error != null) {
|
||||
String msg = errorDescription != null ? errorDescription : error;
|
||||
@@ -58,7 +96,7 @@ public class KdocsCallbackController extends BaseController {
|
||||
}
|
||||
// 无 code:多为平台校验回调可达性,或用户直接打开本地址(非授权失败)
|
||||
if (StringUtils.isBlank(code)) {
|
||||
return callbackEndpointInfoPage();
|
||||
return callbackEndpointInfoPage(request);
|
||||
}
|
||||
log.info("金山文档授权回调 code 已收到 state={}", state);
|
||||
KdocsTokenInfo tokenInfo = kdocsOAuthService.getAccessTokenByCode(code);
|
||||
@@ -100,7 +138,7 @@ public class KdocsCallbackController extends BaseController {
|
||||
/**
|
||||
* 无授权参数时的占位页:HTTP 200,避免被误判为「回调不可用」,也不向 opener 误发失败消息。
|
||||
*/
|
||||
private ResponseEntity<String> callbackEndpointInfoPage() {
|
||||
return KdocsCallbackProbeResponses.callbackReadyPage();
|
||||
private ResponseEntity<String> callbackEndpointInfoPage(HttpServletRequest request) {
|
||||
return KdocsCallbackProbeResponses.callbackReadyJson(request, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.Enumeration;
|
||||
|
||||
/**
|
||||
* 开放平台校验回调 URL 时多为 GET、无 code,需直接 200;勿对校验请求返回 302。
|
||||
* 金山文档 ChallengeURLValidator 等校验会解析响应为 JSON;返回 HTML 会报 unmarshal challenge response json invalid。
|
||||
* 无 OAuth code 时返回与开放平台风格接近的 JSON,并回显 query / JSON body 中的字段(如 challenge)。
|
||||
*/
|
||||
public final class KdocsCallbackProbeResponses {
|
||||
|
||||
private KdocsCallbackProbeResponses() {
|
||||
}
|
||||
|
||||
private static final MediaType JSON_UTF8 = MediaType.parseMediaType("application/json;charset=UTF-8");
|
||||
|
||||
private static final MediaType HTML_UTF8 = MediaType.parseMediaType("text/html;charset=UTF-8");
|
||||
|
||||
private static final String BODY = "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='robots' content='noindex'>"
|
||||
private static final String HTML_BODY = "<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='robots' content='noindex'>"
|
||||
+ "<title>金山文档授权回调</title></head>"
|
||||
+ "<body style='font-family:sans-serif;text-align:center;padding:40px;color:#333'>"
|
||||
+ "<h2>金山文档授权回调</h2>"
|
||||
@@ -23,9 +33,70 @@ public final class KdocsCallbackProbeResponses {
|
||||
+ "<p>请在系统中点击「连接金山文档」或「授权」后,由金山文档页面自动跳转到此处。</p>"
|
||||
+ "</body></html>";
|
||||
|
||||
public static ResponseEntity<String> callbackReadyPage() {
|
||||
/**
|
||||
* 浏览器直接打开回调页时使用(Accept 偏 HTML)。
|
||||
*/
|
||||
public static ResponseEntity<String> callbackReadyHtmlPage() {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(HTML_UTF8);
|
||||
return new ResponseEntity<>(BODY, headers, HttpStatus.OK);
|
||||
return new ResponseEntity<>(HTML_BODY, headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* 平台 URL 校验:合法 JSON,兼容 WebOffice/开放平台常见 envelope,并回显校验参数。
|
||||
*
|
||||
* @param jsonBody POST application/json 时的原始 body,可为 null
|
||||
*/
|
||||
public static ResponseEntity<String> callbackReadyJson(HttpServletRequest request, String jsonBody) {
|
||||
JSONObject data = new JSONObject();
|
||||
|
||||
Enumeration<String> names = request.getParameterNames();
|
||||
while (names.hasMoreElements()) {
|
||||
String n = names.nextElement();
|
||||
data.put(n, request.getParameter(n));
|
||||
}
|
||||
|
||||
mergeJsonPrimitivesIntoData(data, jsonBody);
|
||||
|
||||
JSONObject root = new JSONObject();
|
||||
root.put("code", 0);
|
||||
root.put("message", "");
|
||||
root.put("result", "ok");
|
||||
root.put("data", data);
|
||||
|
||||
if (data.containsKey("challenge")) {
|
||||
root.put("challenge", data.get("challenge"));
|
||||
}
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(JSON_UTF8);
|
||||
return new ResponseEntity<>(root.toJSONString(), headers, HttpStatus.OK);
|
||||
}
|
||||
|
||||
private static void mergeJsonPrimitivesIntoData(JSONObject data, String jsonBody) {
|
||||
if (StringUtils.isBlank(jsonBody)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
JSONObject in = JSON.parseObject(jsonBody);
|
||||
if (in == null) {
|
||||
return;
|
||||
}
|
||||
for (String k : in.keySet()) {
|
||||
Object v = in.get(k);
|
||||
if (v == null) {
|
||||
continue;
|
||||
}
|
||||
if (v instanceof JSONObject || v instanceof JSONArray) {
|
||||
continue;
|
||||
}
|
||||
if (!data.containsKey(k)) {
|
||||
data.put(k, v);
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 非 JSON 则忽略
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -178,6 +178,8 @@ public class OrderRowsController extends BaseController
|
||||
|
||||
/**
|
||||
* 构建统计数据。
|
||||
* <p>汇总口径:{@code totalCommission} 为预估佣金合计,排除取消单(validCode=3);{@code totalActualFee} 为实际佣金,仅统计已完成(validCode=17);
|
||||
* {@code estimatePaidPending} 为已付款待收货(validCode=16)的预估佣金,与分组 {@code paid} 一致。</p>
|
||||
* @param forList true=与列表同数据源(不排除 isCount=0),保证总订单数与分页一致;false=独立统计(排除 isCount=0)
|
||||
*/
|
||||
private Map<String, Object> buildStatistics(OrderRows orderRows, Date beginTime, Date endTime, boolean forList) {
|
||||
@@ -209,7 +211,7 @@ public class OrderRowsController extends BaseController
|
||||
groupStats.put("cancel", createGroupStat("取消", "cancel"));
|
||||
groupStats.put("invalid", createGroupStat("无效", "invalid"));
|
||||
groupStats.put("pending", createGroupStat("待付款", "pending"));
|
||||
groupStats.put("paid", createGroupStat("已付款", "paid"));
|
||||
groupStats.put("paid", createGroupStat("已付款(待结算)", "paid"));
|
||||
groupStats.put("finished", createGroupStat("已完成", "finished"));
|
||||
groupStats.put("deposit", createGroupStat("已付定金", "deposit"));
|
||||
groupStats.put("illegal", createGroupStat("违规", "illegal"));
|
||||
@@ -263,8 +265,14 @@ public class OrderRowsController extends BaseController
|
||||
actualFeeAmount = row.getActualFee() != null ? row.getActualFee() : 0;
|
||||
}
|
||||
|
||||
totalCommission += commissionAmount;
|
||||
totalActualFee += actualFeeAmount;
|
||||
// 顶部「预估佣金」汇总:排除取消单(validCode=3),其余状态累加单条 commissionAmount
|
||||
if (!"3".equals(validCode)) {
|
||||
totalCommission += commissionAmount;
|
||||
}
|
||||
// 顶部「实际佣金」汇总:仅已完成(validCode=17),与联盟「已结算」口径一致
|
||||
if ("17".equals(validCode)) {
|
||||
totalActualFee += actualFeeAmount;
|
||||
}
|
||||
|
||||
if (validCode != null) {
|
||||
for (Map.Entry<String, List<String>> group : groups.entrySet()) {
|
||||
@@ -290,6 +298,8 @@ public class OrderRowsController extends BaseController
|
||||
result.put("totalCosPrice", totalCosPrice);
|
||||
result.put("totalCommission", totalCommission);
|
||||
result.put("totalActualFee", totalActualFee);
|
||||
// 已付款待结算:与分组 paid 的预估佣金口径一致,便于独立展示卡片
|
||||
result.put("estimatePaidPending", (Double) groupStats.get("paid").get("commission"));
|
||||
result.put("totalSkuNum", totalSkuNum);
|
||||
result.put("violationOrders", violationOrders);
|
||||
result.put("violationCommission", violationCommission);
|
||||
|
||||
@@ -156,6 +156,139 @@ public class SocialMediaController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出多套大模型接入配置及当前激活 id
|
||||
*/
|
||||
@GetMapping("/llm-config")
|
||||
public AjaxResult listLlmProfiles()
|
||||
{
|
||||
try {
|
||||
return socialMediaService.listLlmProfiles();
|
||||
} catch (Exception e) {
|
||||
logger.error("列出大模型接入配置失败", e);
|
||||
return AjaxResult.error("获取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单套配置(编辑)
|
||||
*/
|
||||
@GetMapping("/llm-config/profiles/{id}")
|
||||
public AjaxResult getLlmProfile(@PathVariable("id") String id)
|
||||
{
|
||||
try {
|
||||
return socialMediaService.getLlmProfile(id);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取大模型接入配置失败", e);
|
||||
return AjaxResult.error("获取失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增一套配置
|
||||
*/
|
||||
@Log(title = "新增大模型接入配置", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/llm-config/profiles")
|
||||
public AjaxResult createLlmProfile(@RequestBody Map<String, Object> request)
|
||||
{
|
||||
try {
|
||||
return socialMediaService.createLlmProfile(request);
|
||||
} catch (Exception e) {
|
||||
logger.error("新增大模型接入配置失败", e);
|
||||
return AjaxResult.error("保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新一套配置
|
||||
*/
|
||||
@Log(title = "更新大模型接入配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/llm-config/profiles/{id}")
|
||||
public AjaxResult updateLlmProfile(@PathVariable("id") String id, @RequestBody Map<String, Object> request)
|
||||
{
|
||||
try {
|
||||
return socialMediaService.updateLlmProfile(id, request);
|
||||
} catch (Exception e) {
|
||||
logger.error("更新大模型接入配置失败", e);
|
||||
return AjaxResult.error("保存失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一套配置
|
||||
*/
|
||||
@Log(title = "删除大模型接入配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/llm-config/profiles/{id}")
|
||||
public AjaxResult deleteLlmProfile(@PathVariable("id") String id)
|
||||
{
|
||||
try {
|
||||
return socialMediaService.deleteLlmProfile(id);
|
||||
} catch (Exception e) {
|
||||
logger.error("删除大模型接入配置失败", e);
|
||||
return AjaxResult.error("删除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活指定配置为当前使用
|
||||
*/
|
||||
@Log(title = "激活大模型接入配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/llm-config/active/{id}")
|
||||
public AjaxResult setActiveLlmProfile(@PathVariable("id") String id)
|
||||
{
|
||||
try {
|
||||
return socialMediaService.setActiveLlmProfile(id);
|
||||
} catch (Exception e) {
|
||||
logger.error("激活大模型接入配置失败", e);
|
||||
return AjaxResult.error("操作失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消激活(Jarvis 使用 yml 默认 Ollama)
|
||||
*/
|
||||
@Log(title = "取消激活大模型接入配置", businessType = BusinessType.UPDATE)
|
||||
@DeleteMapping("/llm-config/active")
|
||||
public AjaxResult clearActiveLlmProfile()
|
||||
{
|
||||
try {
|
||||
return socialMediaService.clearActiveLlmProfile();
|
||||
} catch (Exception e) {
|
||||
logger.error("取消激活大模型接入配置失败", e);
|
||||
return AjaxResult.error("操作失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有套及旧版单键
|
||||
*/
|
||||
@Log(title = "清空大模型接入配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/llm-config")
|
||||
public AjaxResult resetAllLlmConfig()
|
||||
{
|
||||
try {
|
||||
return socialMediaService.resetAllLlmConfig();
|
||||
} catch (Exception e) {
|
||||
logger.error("清空大模型接入配置失败", e);
|
||||
return AjaxResult.error("清除失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 大模型连通测试(转发 Jarvis,默认问题 1+1)
|
||||
*/
|
||||
@Log(title = "大模型连通测试", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/llm-config/test")
|
||||
public AjaxResult testLlmProfile(@RequestBody(required = false) Map<String, Object> request)
|
||||
{
|
||||
try {
|
||||
return socialMediaService.testLlmProfile(request);
|
||||
} catch (Exception e) {
|
||||
logger.error("大模型连通测试失败", e);
|
||||
return AjaxResult.error("测试失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 闲鱼文案(手动):根据标题+可选型号生成代下单、教你下单文案,不依赖JD接口
|
||||
*/
|
||||
@@ -164,9 +297,7 @@ public class SocialMediaController extends BaseController
|
||||
public AjaxResult generateXianyuWenan(@RequestBody Map<String, Object> request)
|
||||
{
|
||||
try {
|
||||
String title = (String) request.get("title");
|
||||
String remark = (String) request.get("remark");
|
||||
Map<String, Object> result = socialMediaService.generateXianyuWenan(title, remark);
|
||||
Map<String, Object> result = socialMediaService.generateXianyuWenan(request);
|
||||
if (Boolean.TRUE.equals(result.get("success"))) {
|
||||
return AjaxResult.success(result);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.ruoyi.common.core.redis.RedisCache;
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.service.ITencentDocService;
|
||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||
import com.ruoyi.jarvis.util.TencentDocDataParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -54,6 +55,9 @@ public class TencentDocController extends BaseController {
|
||||
@Autowired
|
||||
private com.ruoyi.jarvis.service.ITencentDocDelayedPushService delayedPushService;
|
||||
|
||||
@Autowired
|
||||
private com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||
|
||||
/** 单次请求最大行数(腾讯文档 API:行数≤1000) */
|
||||
private static final int API_MAX_ROWS_PER_REQUEST = 200;
|
||||
/** 用 rowTotal 时接口实际单次只能读 200 行 */
|
||||
@@ -595,7 +599,8 @@ public class TencentDocController extends BaseController {
|
||||
if (cell.containsKey("cellValue")) {
|
||||
String cellText = cell.getJSONObject("cellValue").getString("text");
|
||||
if (cellText != null) {
|
||||
if (cellText.contains("单号")) {
|
||||
// 「物流单号」也含「单号」,须排除,否则会误把物流列当成单号列
|
||||
if (cellText.contains("单号") && !cellText.contains("物流")) {
|
||||
orderNoColumn = i;
|
||||
} else if (cellText.contains("物流")) {
|
||||
logisticsColumn = i;
|
||||
@@ -607,7 +612,7 @@ public class TencentDocController extends BaseController {
|
||||
if (orderNoColumn == -1 || logisticsColumn == -1) {
|
||||
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
|
||||
"FAILED", "未找到'单号'或'物流'列");
|
||||
return AjaxResult.error("未找到'单号'或'物流'列,请检查表头配置");
|
||||
return AjaxResult.error("未找到「单号/客户单号/第三方单号」或「物流」列,请检查表头配置");
|
||||
}
|
||||
|
||||
log.info("表头解析完成 - 单号列: {}, 物流列: {}", orderNoColumn, logisticsColumn);
|
||||
@@ -895,6 +900,90 @@ public class TencentDocController extends BaseController {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步中出现报错时,将摘要推送到企业微信(wxSend 闲鱼应用通道)
|
||||
*/
|
||||
private void pushTencentDocRowErrorsToWeCom(String batchId, String fileId, String sheetId,
|
||||
int filledCount, int skippedCount, int errorCount,
|
||||
List<Map<String, Object>> errorLogs) {
|
||||
if (errorCount <= 0 && (errorLogs == null || errorLogs.isEmpty())) {
|
||||
return;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("【腾讯文档推送】同步存在报错\n");
|
||||
if (batchId != null && !batchId.isEmpty()) {
|
||||
sb.append("批次: ").append(batchId).append("\n");
|
||||
}
|
||||
if (fileId != null && !fileId.isEmpty()) {
|
||||
sb.append("fileId: ").append(fileId).append("\n");
|
||||
}
|
||||
if (sheetId != null && !sheetId.isEmpty()) {
|
||||
sb.append("sheetId: ").append(sheetId).append("\n");
|
||||
}
|
||||
sb.append(String.format("成功队列: %d, 跳过: %d, 错误: %d\n", filledCount, skippedCount, errorCount));
|
||||
if (errorLogs != null && !errorLogs.isEmpty()) {
|
||||
int max = Math.min(15, errorLogs.size());
|
||||
for (int i = 0; i < max; i++) {
|
||||
Map<String, Object> el = errorLogs.get(i);
|
||||
Object on = el != null ? el.get("orderNo") : null;
|
||||
Object row = el != null ? el.get("row") : null;
|
||||
Object em = el != null ? el.get("errorMessage") : null;
|
||||
Object et = el != null ? el.get("errorType") : null;
|
||||
sb.append(String.format("%d. 单号:%s 行:%s", i + 1,
|
||||
on != null ? on : "-", row != null ? row : "-"));
|
||||
if (et != null && String.valueOf(et).length() > 0) {
|
||||
sb.append(" ").append(et);
|
||||
}
|
||||
sb.append("\n");
|
||||
if (em != null) {
|
||||
String msg = String.valueOf(em);
|
||||
if (msg.length() > 120) {
|
||||
msg = msg.substring(0, 119) + "…";
|
||||
}
|
||||
sb.append(" ").append(msg).append("\n");
|
||||
}
|
||||
}
|
||||
if (errorLogs.size() > max) {
|
||||
sb.append("… 共 ").append(errorLogs.size()).append(" 条错误\n");
|
||||
}
|
||||
}
|
||||
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "", sb.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并同一行的腾讯文档填充任务(物流更新与仅补京东单号合并为一次 batchUpdate)
|
||||
*/
|
||||
private void mergeTencentDocRowFillUpdate(JSONObject target, JSONObject extra) {
|
||||
if (extra.containsKey("logisticsLink")) {
|
||||
target.put("logisticsLink", extra.getString("logisticsLink"));
|
||||
target.remove("isJdOrderIdOnlyUpdate");
|
||||
}
|
||||
if (Boolean.TRUE.equals(extra.getBoolean("isLinkUpdated"))) {
|
||||
target.put("isLinkUpdated", true);
|
||||
if (extra.containsKey("oldLogisticsLink")) {
|
||||
target.put("oldLogisticsLink", extra.getString("oldLogisticsLink"));
|
||||
}
|
||||
if (extra.containsKey("newLogisticsLink")) {
|
||||
target.put("newLogisticsLink", extra.getString("newLogisticsLink"));
|
||||
}
|
||||
}
|
||||
if (extra.containsKey("phone")) {
|
||||
target.put("phone", extra.getString("phone"));
|
||||
}
|
||||
if (extra.containsKey("phoneColumn")) {
|
||||
target.put("phoneColumn", extra.getInteger("phoneColumn"));
|
||||
}
|
||||
if (extra.containsKey("jdOrderId")) {
|
||||
target.put("jdOrderId", extra.getString("jdOrderId"));
|
||||
}
|
||||
if (extra.containsKey("jdOrderIdColumn")) {
|
||||
target.put("jdOrderIdColumn", extra.getInteger("jdOrderIdColumn"));
|
||||
}
|
||||
if (Boolean.TRUE.equals(extra.getBoolean("isJdOrderIdOnlyUpdate")) && !target.containsKey("logisticsLink")) {
|
||||
target.put("isJdOrderIdOnlyUpdate", true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量同步物流链接 - 读取表格数据,根据单号查询订单系统中的物流链接,并填充到表格
|
||||
* 优化:记录上次处理的最大行数,每次从最大行数-100开始读取,避免重复处理历史数据
|
||||
@@ -903,6 +992,7 @@ public class TencentDocController extends BaseController {
|
||||
@Anonymous
|
||||
@PostMapping("/fillLogisticsByOrderNo")
|
||||
public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
|
||||
String batchId = null;
|
||||
try {
|
||||
// 直接尝试刷新token(如果失败,说明需要首次授权)
|
||||
String accessToken;
|
||||
@@ -920,7 +1010,7 @@ public class TencentDocController extends BaseController {
|
||||
}
|
||||
|
||||
// 从参数获取批次ID(如果是批量调用会传入)
|
||||
String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
|
||||
batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
|
||||
|
||||
// 从参数或配置中获取文档信息
|
||||
String fileId = (String) params.get("fileId");
|
||||
@@ -983,7 +1073,8 @@ public class TencentDocController extends BaseController {
|
||||
// 读取表格数据(先读取表头行用于识别列位置)
|
||||
// 根据官方文档,使用 A1 表示法(Excel格式)
|
||||
// 参考:https://docs.qq.com/open/document/app/openapi/v3/sheet/get/get_range.html
|
||||
String headerRange = String.format("A%d:Z%d", headerRow, headerRow); // 例如:A2:Z2
|
||||
// 与数据区列宽一致,避免「京东下单订单号」在 Z 列之后时无法识别表头
|
||||
String headerRange = String.format("A%d:%s%d", headerRow, DATA_RANGE_COL_END, headerRow);
|
||||
log.info("读取表头 - 行号: {}, range: {}", headerRow, headerRange);
|
||||
|
||||
JSONObject headerData = null;
|
||||
@@ -1007,7 +1098,8 @@ public class TencentDocController extends BaseController {
|
||||
}
|
||||
|
||||
// 自动识别列位置(从表头中识别)
|
||||
Integer orderNoColumn = null; // "单号"列
|
||||
Integer orderNoColumn = null; // 第三方「单号」列(排除京东下单订单号、物流单号)
|
||||
Integer jdPlaceOrderNoColumn = null; // 「京东下单订单号」列,对应本地 orderId
|
||||
Integer logisticsLinkColumn = null; // "物流单号"列
|
||||
Integer remarkColumn = null; // "备注"列
|
||||
Integer arrangedColumn = null; // "是否安排"列
|
||||
@@ -1019,59 +1111,65 @@ public class TencentDocController extends BaseController {
|
||||
return AjaxResult.error("无法识别表头,表头数据为空");
|
||||
}
|
||||
|
||||
// 查找所有相关列
|
||||
log.info("开始识别表头列,共 {} 列", headerRowData.size());
|
||||
// 列名须与表格完全一致(仅忽略首尾空白、不间断空格等):备注、是否安排、物流单号、下单电话、标记、京东下单订单号;另需「单号」「客户单号」或「第三方单号」之一
|
||||
log.info("开始识别表头列(完全匹配列名),共 {} 列", headerRowData.size());
|
||||
for (int i = 0; i < headerRowData.size(); i++) {
|
||||
String cellValue = headerRowData.getString(i);
|
||||
if (cellValue != null) {
|
||||
String cellValueTrim = cellValue.trim();
|
||||
log.debug("列 {} 内容: [{}]", i, cellValueTrim);
|
||||
if (cellValue == null) {
|
||||
continue;
|
||||
}
|
||||
String norm = TencentDocDataParser.normalizeTencentDocHeader(cellValue);
|
||||
log.debug("列 {} 原始: [{}] 规范化: [{}]", i, cellValue.trim(), norm);
|
||||
|
||||
// 识别"单号"列
|
||||
if (orderNoColumn == null && cellValueTrim.contains("单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
// 识别"物流单号"或"物流链接"列
|
||||
if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
// 识别"备注"列(可选)
|
||||
if (remarkColumn == null && cellValueTrim.contains("备注")) {
|
||||
remarkColumn = i;
|
||||
log.info("✓ 识别到 '备注' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
// 识别"是否安排"列(可选)
|
||||
if (arrangedColumn == null && (cellValueTrim.contains("是否安排") || cellValueTrim.contains("安排"))) {
|
||||
arrangedColumn = i;
|
||||
log.info("✓ 识别到 '是否安排' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
// 识别"标记"列(可选)
|
||||
if (markColumn == null && cellValueTrim.contains("标记")) {
|
||||
markColumn = i;
|
||||
log.info("✓ 识别到 '标记' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
// 识别"下单电话"列(可选)
|
||||
if (phoneColumn == null && (cellValueTrim.contains("下单电话"))) {
|
||||
phoneColumn = i;
|
||||
log.info("✓ 识别到 '下单电话' 列:第 {} 列(索引{}),列名: [{}]", i + 1, i, cellValueTrim);
|
||||
}
|
||||
if (jdPlaceOrderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "京东下单订单号")) {
|
||||
jdPlaceOrderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「京东下单订单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("\u2713 列名完全匹配「客户单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 列名完全匹配「物流单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) {
|
||||
logisticsLinkColumn = i;
|
||||
log.warn("✓ 列名匹配「物流链接」(建议改为「物流单号」):第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (remarkColumn == null && TencentDocDataParser.headerEquals(cellValue, "备注")) {
|
||||
remarkColumn = i;
|
||||
log.info("✓ 列名完全匹配「备注」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (arrangedColumn == null && TencentDocDataParser.headerEquals(cellValue, "是否安排")) {
|
||||
arrangedColumn = i;
|
||||
log.info("✓ 列名完全匹配「是否安排」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (markColumn == null && TencentDocDataParser.headerEquals(cellValue, "标记")) {
|
||||
markColumn = i;
|
||||
log.info("✓ 列名完全匹配「标记」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (phoneColumn == null && TencentDocDataParser.headerEquals(cellValue, "下单电话")) {
|
||||
phoneColumn = i;
|
||||
log.info("✓ 列名完全匹配「下单电话」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
}
|
||||
log.info("表头列识别完成");
|
||||
|
||||
// 检查必需的列是否都已识别
|
||||
if (orderNoColumn == null) {
|
||||
return AjaxResult.error("无法找到'单号'列,请检查表头是否包含'单号'字段");
|
||||
return AjaxResult.error("无法找到列名完全为「单号」「客户单号」或「第三方单号」的列(请与表格列名一致,勿加空格或后缀)");
|
||||
}
|
||||
if (logisticsLinkColumn == null) {
|
||||
return AjaxResult.error("无法找到'物流单号'或'物流链接'列,请检查表头");
|
||||
return AjaxResult.error("无法找到列名完全为「物流单号」的列(兼容「物流链接」);请与表格列名一致");
|
||||
}
|
||||
|
||||
// 提示可选列的识别情况
|
||||
@@ -1087,9 +1185,12 @@ public class TencentDocController extends BaseController {
|
||||
if (phoneColumn == null) {
|
||||
log.warn("未找到'下单电话'列,将跳过该字段的更新");
|
||||
}
|
||||
if (jdPlaceOrderNoColumn == null) {
|
||||
log.warn("未找到'京东下单订单号'列,将跳过该字段的同步(请在表头增加该列以写入京东 orderId)");
|
||||
}
|
||||
|
||||
log.info("列位置识别完成 - 单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}",
|
||||
orderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);
|
||||
log.info("列位置识别完成 - 单号: {}, 京东下单订单号: {}, 物流单号: {}, 备注: {}, 是否安排: {}, 标记: {}, 下单电话: {}",
|
||||
orderNoColumn, jdPlaceOrderNoColumn, logisticsLinkColumn, remarkColumn, arrangedColumn, markColumn, phoneColumn);
|
||||
|
||||
// 读取数据行:接口实际只能读 200 行,严格限制单次行数,失败时逐步缩小范围重试
|
||||
// 腾讯文档 get_range 的 range 为「结束行不包含」:要读到 endRow 含最后一行,须传 endRow+1
|
||||
@@ -1229,7 +1330,9 @@ public class TencentDocController extends BaseController {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
|
||||
// 仅要求能读到第三方单号、物流列;京东列常在更右侧且稀疏行可能未返回尾部空列,不能要求 row 长度覆盖京东列
|
||||
int minDataColIdx = Math.max(orderNoColumn, logisticsLinkColumn);
|
||||
if (row == null || row.size() <= minDataColIdx) {
|
||||
continue; // 跳过空行或列数不足的行
|
||||
}
|
||||
|
||||
@@ -1263,7 +1366,17 @@ public class TencentDocController extends BaseController {
|
||||
if (existingOrder != null) {
|
||||
String dbLogisticsLink = existingOrder.getLogisticsLink();
|
||||
String trimmedDbLink = (dbLogisticsLink != null) ? dbLogisticsLink.trim() : "";
|
||||
|
||||
String dbJdOrderId = existingOrder.getOrderId() != null ? existingOrder.getOrderId().trim() : "";
|
||||
String docJdOrderId = "";
|
||||
if (jdPlaceOrderNoColumn != null && row.size() > jdPlaceOrderNoColumn) {
|
||||
try {
|
||||
String cj = row.getString(jdPlaceOrderNoColumn);
|
||||
docJdOrderId = cj != null ? cj.trim() : "";
|
||||
} catch (Exception ex) {
|
||||
log.debug("读取京东下单订单号列异常 行 {}: {}", excelRow, ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 比对物流链接,如果不一致则需要更新
|
||||
if (!trimmedDbLink.isEmpty() && !trimmedExistingLink.equals(trimmedDbLink)) {
|
||||
// 物流链接不一致,需要更新文档中的链接
|
||||
@@ -1279,6 +1392,11 @@ public class TencentDocController extends BaseController {
|
||||
update.put("isLinkUpdated", true); // 标记为物流链接更新
|
||||
update.put("oldLogisticsLink", trimmedExistingLink); // 旧链接
|
||||
update.put("newLogisticsLink", trimmedDbLink); // 新链接
|
||||
// 物流变更:与京东单号一并同步(单号随物流变,覆盖文档原京东单号)
|
||||
if (jdPlaceOrderNoColumn != null && !dbJdOrderId.isEmpty()) {
|
||||
update.put("jdOrderId", dbJdOrderId);
|
||||
update.put("jdOrderIdColumn", jdPlaceOrderNoColumn);
|
||||
}
|
||||
updates.add(update);
|
||||
|
||||
filledCount++; // 统计为填充数量
|
||||
@@ -1293,6 +1411,17 @@ public class TencentDocController extends BaseController {
|
||||
|
||||
log.info("========== 物流链接不一致,已加入更新队列 - 单号: {}, 行号: {}, 旧链接: {}, 新链接: {} ==========",
|
||||
orderNo, excelRow, trimmedExistingLink, trimmedDbLink);
|
||||
} else if (docJdOrderId.isEmpty() && !dbJdOrderId.isEmpty() && jdPlaceOrderNoColumn != null) {
|
||||
// 文档已有物流、京东单号列为空:仅补写京东单号
|
||||
JSONObject jdUpdate = new JSONObject();
|
||||
jdUpdate.put("row", excelRow);
|
||||
jdUpdate.put("orderNo", orderNo);
|
||||
jdUpdate.put("isJdOrderIdOnlyUpdate", true);
|
||||
jdUpdate.put("jdOrderId", dbJdOrderId);
|
||||
jdUpdate.put("jdOrderIdColumn", jdPlaceOrderNoColumn);
|
||||
updates.add(jdUpdate);
|
||||
filledCount++;
|
||||
log.info("========== 文档有物流无京东单号,补写京东下单订单号 - 第三方单号: {}, 行: {} ==========", orderNo, excelRow);
|
||||
} else {
|
||||
// 物流链接一致或数据库中没有链接,只需同步订单状态
|
||||
if (existingOrder.getTencentDocPushed() == null || existingOrder.getTencentDocPushed() == 0) {
|
||||
@@ -1330,7 +1459,7 @@ public class TencentDocController extends BaseController {
|
||||
}
|
||||
|
||||
try {
|
||||
// 根据第三方单号查询订单
|
||||
// 根据第三方单号查询订单(与文档「单号/客户单号/第三方单号」列单元格一致)
|
||||
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);
|
||||
|
||||
if (order == null) {
|
||||
@@ -1382,6 +1511,12 @@ public class TencentDocController extends BaseController {
|
||||
update.put("phoneColumn", phoneColumn);
|
||||
}
|
||||
|
||||
// 文档无物流:与物流一并写入京东单号(无物流无单号 → 一起写入)
|
||||
if (jdPlaceOrderNoColumn != null && order.getOrderId() != null && !order.getOrderId().trim().isEmpty()) {
|
||||
update.put("jdOrderId", order.getOrderId().trim());
|
||||
update.put("jdOrderIdColumn", jdPlaceOrderNoColumn);
|
||||
}
|
||||
|
||||
// 注意:不再保存order对象,写入成功后会重新查询以确保数据最新
|
||||
updates.add(update);
|
||||
|
||||
@@ -1420,12 +1555,17 @@ public class TencentDocController extends BaseController {
|
||||
// 获取今天的日期,格式:yyMMdd(如:251105)
|
||||
String today = new java.text.SimpleDateFormat("yyMMdd").format(new java.util.Date());
|
||||
|
||||
// 将更新按行分组,批量写入
|
||||
Map<Integer, JSONObject> rowUpdates = new java.util.HashMap<>();
|
||||
// 将更新按行分组,批量写入(同一行可能同时有物流与京东单号任务,需合并)
|
||||
Map<Integer, JSONObject> rowUpdates = new java.util.LinkedHashMap<>();
|
||||
for (int i = 0; i < updates.size(); i++) {
|
||||
JSONObject update = updates.getJSONObject(i);
|
||||
int row = update.getIntValue("row");
|
||||
rowUpdates.put(row, update);
|
||||
JSONObject existing = rowUpdates.get(row);
|
||||
if (existing == null) {
|
||||
rowUpdates.put(row, update);
|
||||
} else {
|
||||
mergeTencentDocRowFillUpdate(existing, update);
|
||||
}
|
||||
}
|
||||
|
||||
// 批量写入(每行单独写入,同时更新多个字段)
|
||||
@@ -1437,11 +1577,94 @@ public class TencentDocController extends BaseController {
|
||||
try {
|
||||
int row = entry.getKey();
|
||||
JSONObject update = entry.getValue();
|
||||
String logisticsLink = update.getString("logisticsLink");
|
||||
String expectedOrderNo = update.getString("orderNo");
|
||||
|
||||
// 重新读取该行数据,验证单号和物流链接列
|
||||
String verifyRange = String.format("A%d:Z%d", row, row);
|
||||
// 文档已有物流、仅补写京东下单订单号(有物流无京东单号)
|
||||
if (Boolean.TRUE.equals(update.getBoolean("isJdOrderIdOnlyUpdate"))) {
|
||||
Integer jdCol = update.getInteger("jdOrderIdColumn");
|
||||
String jdId = update.getString("jdOrderId");
|
||||
if (jdCol == null || jdId == null || jdId.trim().isEmpty()) {
|
||||
log.warn("仅补京东单号任务参数不全 - 行 {}", row);
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
jdId = jdId.trim();
|
||||
String jdVerifyRange = String.format("A%d:%s%d", row, DATA_RANGE_COL_END, row);
|
||||
JSONObject jdVerifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, jdVerifyRange);
|
||||
if (jdVerifyData == null || !jdVerifyData.containsKey("values")) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
JSONArray jdVerifyRows = jdVerifyData.getJSONArray("values");
|
||||
if (jdVerifyRows == null || jdVerifyRows.isEmpty()) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
JSONArray jdVerifyRow = jdVerifyRows.getJSONArray(0);
|
||||
int jdBaseNeedIdx = Math.max(orderNoColumn, logisticsLinkColumn);
|
||||
if (jdVerifyRow == null || jdVerifyRow.size() <= jdBaseNeedIdx) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
String jdCurOrderNo = jdVerifyRow.getString(orderNoColumn);
|
||||
if (jdCurOrderNo == null || !jdCurOrderNo.trim().equals(expectedOrderNo)) {
|
||||
errorCount++;
|
||||
continue;
|
||||
}
|
||||
String jdDocLogistics = jdVerifyRow.getString(logisticsLinkColumn);
|
||||
if (jdDocLogistics == null || jdDocLogistics.trim().isEmpty()) {
|
||||
log.info("跳过仅补京东单号 - 行 {} 物流列为空", row);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
String docJd = "";
|
||||
if (jdVerifyRow.size() > jdCol) {
|
||||
try {
|
||||
String sj = jdVerifyRow.getString(jdCol);
|
||||
docJd = sj != null ? sj.trim() : "";
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (!docJd.isEmpty()) {
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
JSONArray jdRequests = new JSONArray();
|
||||
jdRequests.add(buildUpdateCellRequest(sheetId, row - 1, jdCol, jdId, false));
|
||||
JSONObject jdBatch = new JSONObject();
|
||||
jdBatch.put("requests", jdRequests);
|
||||
tencentDocService.batchUpdate(accessToken, fileId, jdBatch);
|
||||
currentBatchSuccessUpdates++;
|
||||
if (row > currentBatchMaxSuccessRow) {
|
||||
currentBatchMaxSuccessRow = row;
|
||||
}
|
||||
log.info("✓ 已补写京东下单订单号 - 第三方单号: {}, 行: {}, 值: {}", expectedOrderNo, row, jdId);
|
||||
logOperation(batchId, fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, jdId,
|
||||
"SUCCESS", "补写京东下单订单号");
|
||||
Map<String, Object> jdSuccessLog = new java.util.HashMap<>();
|
||||
jdSuccessLog.put("orderNo", expectedOrderNo);
|
||||
jdSuccessLog.put("row", row);
|
||||
jdSuccessLog.put("jdOrderId", jdId);
|
||||
jdSuccessLog.put("updateType", "JD_ORDER_ID");
|
||||
successLogs.add(jdSuccessLog);
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
String logisticsLink = update.getString("logisticsLink");
|
||||
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
|
||||
log.warn("跳过写入 - 行 {} 缺少物流链接", row);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 重新读取该行(列宽与批量读取一致,含 Z 列之后的京东单号列)
|
||||
String verifyRange = String.format("A%d:%s%d", row, DATA_RANGE_COL_END, row);
|
||||
JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange);
|
||||
|
||||
if (verifyData == null || !verifyData.containsKey("values")) {
|
||||
@@ -1476,8 +1699,9 @@ public class TencentDocController extends BaseController {
|
||||
}
|
||||
|
||||
JSONArray verifyRow = verifyRows.getJSONArray(0);
|
||||
if (verifyRow == null || verifyRow.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
|
||||
log.warn("验证失败 - 行 {} 列数不足", row);
|
||||
int verifyMinColIdx = Math.max(orderNoColumn, logisticsLinkColumn);
|
||||
if (verifyRow == null || verifyRow.size() <= verifyMinColIdx) {
|
||||
log.warn("验证失败 - 行 {} 列数不足(需覆盖单号列与物流列)", row);
|
||||
errorCount++;
|
||||
|
||||
// 记录错误详情
|
||||
@@ -1567,6 +1791,37 @@ public class TencentDocController extends BaseController {
|
||||
log.info("✓ 物流链接更新,保留原标记日期 - 单号: {}, 行: {}", expectedOrderNo, row);
|
||||
}
|
||||
|
||||
// 物流变更:与京东单号一并覆盖写入;首次填物流:仅文档京东列为空时写入
|
||||
boolean wroteJdOrderIdCell = false;
|
||||
String jdIdUpdForLog = null;
|
||||
Integer jdColUpd = update.getInteger("jdOrderIdColumn");
|
||||
String jdIdUpd = update.getString("jdOrderId");
|
||||
if (jdColUpd != null && jdIdUpd != null && !jdIdUpd.trim().isEmpty()) {
|
||||
jdIdUpd = jdIdUpd.trim();
|
||||
String docJdVal = "";
|
||||
if (verifyRow.size() > jdColUpd) {
|
||||
try {
|
||||
String sj = verifyRow.getString(jdColUpd);
|
||||
docJdVal = sj != null ? sj.trim() : "";
|
||||
} catch (Exception ignore) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
boolean shouldWriteJd = Boolean.TRUE.equals(isLinkUpdated) || docJdVal.isEmpty();
|
||||
if (shouldWriteJd) {
|
||||
requests.add(buildUpdateCellRequest(sheetId, row - 1, jdColUpd, jdIdUpd, false));
|
||||
wroteJdOrderIdCell = true;
|
||||
jdIdUpdForLog = jdIdUpd;
|
||||
if (Boolean.TRUE.equals(isLinkUpdated)) {
|
||||
log.info("✓ 同步京东下单订单号(随物流变更) - 第三方单号: {}, 行: {}, 值: {}", expectedOrderNo, row, jdIdUpd);
|
||||
} else {
|
||||
log.info("✓ 写入京东下单订单号(与物流一并) - 第三方单号: {}, 行: {}, 值: {}", expectedOrderNo, row, jdIdUpd);
|
||||
}
|
||||
} else {
|
||||
log.info("✓ 文档已有京东下单订单号,跳过写入 - 第三方单号: {}, 行: {}, 现有: [{}]", expectedOrderNo, row, docJdVal);
|
||||
}
|
||||
}
|
||||
|
||||
// 构建完整的 batchUpdate 请求体
|
||||
JSONObject batchUpdateBody = new JSONObject();
|
||||
batchUpdateBody.put("requests", requests);
|
||||
@@ -1622,6 +1877,9 @@ public class TencentDocController extends BaseController {
|
||||
if (orderToUpdate != null && orderToUpdate.getModelNumber() != null) {
|
||||
successLog.put("modelNumber", orderToUpdate.getModelNumber());
|
||||
}
|
||||
if (wroteJdOrderIdCell && jdIdUpdForLog != null) {
|
||||
successLog.put("jdOrderId", jdIdUpdForLog);
|
||||
}
|
||||
|
||||
// 检查是否为物流链接更新(复用之前的变量)
|
||||
if (Boolean.TRUE.equals(isLinkUpdated)) {
|
||||
@@ -1694,6 +1952,7 @@ public class TencentDocController extends BaseController {
|
||||
result.put("skippedCount", skippedCount);
|
||||
result.put("errorCount", errorCount);
|
||||
result.put("orderNoColumn", orderNoColumn);
|
||||
result.put("jdPlaceOrderNoColumn", jdPlaceOrderNoColumn);
|
||||
result.put("logisticsLinkColumn", logisticsLinkColumn);
|
||||
result.put("skipPushedOrders", skipPushedOrders); // 是否跳过已推送订单
|
||||
|
||||
@@ -1720,7 +1979,7 @@ public class TencentDocController extends BaseController {
|
||||
if (errorCount > 0 && successUpdates == 0) {
|
||||
status = "FAILED";
|
||||
} else if (errorCount > 0) {
|
||||
status = "PARTIAL_SUCCESS";
|
||||
status = "PARTIAL";
|
||||
}
|
||||
batchPushService.updateBatchPushRecord(batchId, status, successUpdates, skippedCount, errorCount,
|
||||
message, null);
|
||||
@@ -1748,7 +2007,21 @@ public class TencentDocController extends BaseController {
|
||||
return AjaxResult.success("填充物流链接完成", result);
|
||||
} catch (Exception e) {
|
||||
log.error("填充物流链接失败", e);
|
||||
return AjaxResult.error("填充物流链接失败: " + e.getMessage());
|
||||
String errMsg = e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName();
|
||||
if (batchId != null && !batchId.trim().isEmpty()) {
|
||||
try {
|
||||
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0, null, errMsg);
|
||||
} catch (Exception ex) {
|
||||
log.error("异常后更新批量推送记录失败 batchId={}", batchId, ex);
|
||||
}
|
||||
try {
|
||||
wxSendGoofishNotifyClient.pushGoofishAgentText(null, "",
|
||||
"【腾讯文档推送】批量同步异常\n批次: " + batchId + "\n" + errMsg);
|
||||
} catch (Exception ex) {
|
||||
log.warn("腾讯文档推送异常企微通知失败: {}", ex.toString());
|
||||
}
|
||||
}
|
||||
return AjaxResult.error("填充物流链接失败: " + errMsg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1893,6 +2166,10 @@ public class TencentDocController extends BaseController {
|
||||
String fileId = tencentDocConfig.getFileId();
|
||||
String sheetId = tencentDocConfig.getSheetId();
|
||||
|
||||
if (fileId != null && !fileId.trim().isEmpty()) {
|
||||
batchPushService.reconcileStaleRunningRecords(fileId);
|
||||
}
|
||||
|
||||
if (fileId != null && sheetId != null) {
|
||||
com.ruoyi.jarvis.domain.TencentDocBatchPushRecord lastSuccess =
|
||||
batchPushService.getLastSuccessRecord(fileId, sheetId);
|
||||
@@ -2032,23 +2309,33 @@ public class TencentDocController extends BaseController {
|
||||
|
||||
for (int i = 0; i < headerRowData.size(); i++) {
|
||||
String cellValue = headerRowData.getString(i);
|
||||
if (cellValue != null) {
|
||||
String cellValueTrim = cellValue.trim();
|
||||
|
||||
if (orderNoColumn == null && cellValueTrim.contains("单号") && !cellValueTrim.contains("物流")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (cellValue == null) {
|
||||
continue;
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("\u2713 列名完全匹配「客户单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 列名完全匹配「物流单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 列名匹配「物流链接」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (orderNoColumn == null || logisticsLinkColumn == null) {
|
||||
return AjaxResult.error("无法识别表头列,请确保表头包含'单号'和'物流单号'列");
|
||||
return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」「客户单号」或「第三方单号」,以及「物流单号」(或「物流链接」)");
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
@@ -2330,23 +2617,33 @@ public class TencentDocController extends BaseController {
|
||||
|
||||
for (int i = 0; i < headerRowData.size(); i++) {
|
||||
String cellValue = headerRowData.getString(i);
|
||||
if (cellValue != null) {
|
||||
String cellValueTrim = cellValue.trim();
|
||||
|
||||
if (orderNoColumn == null && cellValueTrim.contains("单号") && !cellValueTrim.contains("物流")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 识别到 '单号' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
|
||||
if (logisticsLinkColumn == null && (cellValueTrim.contains("物流单号") || cellValueTrim.contains("物流链接"))) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 识别到 '物流单号' 列:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (cellValue == null) {
|
||||
continue;
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "客户单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("\u2713 列名完全匹配「客户单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (orderNoColumn == null && TencentDocDataParser.headerEquals(cellValue, "第三方单号")) {
|
||||
orderNoColumn = i;
|
||||
log.info("✓ 列名完全匹配「第三方单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流单号")) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 列名完全匹配「物流单号」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
if (logisticsLinkColumn == null && TencentDocDataParser.headerEquals(cellValue, "物流链接")) {
|
||||
logisticsLinkColumn = i;
|
||||
log.info("✓ 列名匹配「物流链接」:第 {} 列(索引{})", i + 1, i);
|
||||
}
|
||||
}
|
||||
|
||||
if (orderNoColumn == null || logisticsLinkColumn == null) {
|
||||
return AjaxResult.error("无法识别表头列,请确保表头包含'单号'和'物流单号'列");
|
||||
return AjaxResult.error("无法识别表头列,请确保存在列名完全为「单号」「客户单号」或「第三方单号」,以及「物流单号」(或「物流链接」)");
|
||||
}
|
||||
|
||||
// 统计结果
|
||||
@@ -2438,11 +2735,10 @@ public class TencentDocController extends BaseController {
|
||||
String cleanedLogisticsLink = cleanLogisticsLink(logisticsLinkFromDoc);
|
||||
|
||||
try {
|
||||
// 通过第三方单号查找本地订单
|
||||
// 通过第三方单号查找本地订单;找不到再按内部单号(remark)
|
||||
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNoFromDoc.trim());
|
||||
|
||||
if (order == null) {
|
||||
// 如果通过第三方单号找不到,尝试通过内部单号(remark)查找
|
||||
order = jdOrderService.selectJDOrderByRemark(orderNoFromDoc.trim());
|
||||
}
|
||||
|
||||
@@ -2868,6 +3164,14 @@ public class TencentDocController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount > 0 || (errorLogs != null && !errorLogs.isEmpty())) {
|
||||
try {
|
||||
pushTencentDocRowErrorsToWeCom(batchId, fileId, sheetId, filledCount, skippedCount, errorCount, errorLogs);
|
||||
} catch (Exception ex) {
|
||||
log.warn("腾讯文档报错企微推送失败: {}", ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 构建请求体
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.put("title", "腾讯文档同步成功");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
|
||||
import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
|
||||
import com.ruoyi.jarvis.service.IWeComInboundService;
|
||||
import com.ruoyi.jarvis.service.IWeComInboundTraceService;
|
||||
import com.ruoyi.jarvis.wecom.WxSendWeComPushClient;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* wxSend 企微回调桥接:HTTPS + 共享密钥,无登录态
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/wecom")
|
||||
public class WeComInboundController {
|
||||
|
||||
public static final String HEADER_SECRET = "X-Jarvis-WeCom-Secret";
|
||||
|
||||
@Value("${jarvis.wecom.inbound-secret:}")
|
||||
private String inboundSecret;
|
||||
|
||||
@Resource
|
||||
private IWeComInboundService weComInboundService;
|
||||
@Resource
|
||||
private IWeComInboundTraceService weComInboundTraceService;
|
||||
@Resource
|
||||
private WxSendWeComPushClient wxSendWeComPushClient;
|
||||
|
||||
@PostMapping("/inbound")
|
||||
public AjaxResult inbound(
|
||||
@RequestHeader(value = HEADER_SECRET, required = false) String secret,
|
||||
@RequestBody WeComInboundRequest body) {
|
||||
if (!StringUtils.hasText(inboundSecret) || !inboundSecret.equals(secret)) {
|
||||
return AjaxResult.error("拒绝访问");
|
||||
}
|
||||
WeComInboundRequest req = body != null ? body : new WeComInboundRequest();
|
||||
WeComInboundResult result = weComInboundService.handleInbound(req);
|
||||
weComInboundTraceService.recordInbound(req, result.toTraceFullText());
|
||||
Map<String, Object> data = new HashMap<>(4);
|
||||
data.put("reply", result.getPassiveReply());
|
||||
data.put("activePushCount", result.getActivePushContents().size());
|
||||
wxSendWeComPushClient.scheduleActivePushes(req.getFromUserName(), result.getActivePushContents());
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.jarvis.domain.WeComInboundTrace;
|
||||
import com.ruoyi.jarvis.domain.dto.WeComTestDataCleanRequest;
|
||||
import com.ruoyi.jarvis.service.IWeComInboundTraceService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企微 inbound 消息追踪查询
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/wecom/inboundTrace")
|
||||
public class WeComInboundTraceController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private IWeComInboundTraceService weComInboundTraceService;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:inboundTrace:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(WeComInboundTrace query) {
|
||||
startPage();
|
||||
List<WeComInboundTrace> list = weComInboundTraceService.selectWeComInboundTraceList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:inboundTrace:list')")
|
||||
@GetMapping("/{id}")
|
||||
public AjaxResult getInfo(@PathVariable Long id) {
|
||||
return success(weComInboundTraceService.selectWeComInboundTraceById(id));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:inboundTrace:remove')")
|
||||
@Log(title = "企微消息跟踪", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public AjaxResult remove(@PathVariable Long[] ids) {
|
||||
return toAjax(weComInboundTraceService.deleteWeComInboundTraceByIds(ids));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理联调/测试数据:追踪表 + 可选 Redis 企微会话与 adhoc 队列
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:inboundTrace:remove')")
|
||||
@Log(title = "企微消息测试数据清理", businessType = BusinessType.CLEAN)
|
||||
@PostMapping("/cleanTestData")
|
||||
public AjaxResult cleanTestData(@RequestBody(required = false) WeComTestDataCleanRequest body) {
|
||||
return success(weComInboundTraceService.cleanTestData(body));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.ruoyi.web.controller.jarvis;
|
||||
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.core.page.TableDataInfo;
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
import com.ruoyi.jarvis.mapper.WeComShareLinkLogisticsJobMapper;
|
||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||
import com.ruoyi.jarvis.service.IWeComShareLinkLogisticsJobService;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/jarvis/wecom/shareLinkLogisticsJob")
|
||||
public class WeComShareLinkLogisticsJobController extends BaseController {
|
||||
|
||||
@Resource
|
||||
private IWeComShareLinkLogisticsJobService weComShareLinkLogisticsJobService;
|
||||
@Resource
|
||||
private ILogisticsService logisticsService;
|
||||
@Resource
|
||||
private WeComShareLinkLogisticsJobMapper weComShareLinkLogisticsJobMapper;
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo list(WeComShareLinkLogisticsJob query) {
|
||||
startPage();
|
||||
List<WeComShareLinkLogisticsJob> list = weComShareLinkLogisticsJobService.selectList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@GetMapping("/{jobKey}")
|
||||
public AjaxResult getInfo(@PathVariable("jobKey") String jobKey) {
|
||||
return success(weComShareLinkLogisticsJobService.selectByJobKey(jobKey));
|
||||
}
|
||||
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:import')")
|
||||
@PostMapping("/backfillFromInboundTrace")
|
||||
public AjaxResult backfillFromInboundTrace() {
|
||||
Map<String, Object> r = weComShareLinkLogisticsJobService.backfillImportedFromInboundTrace();
|
||||
return success(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* 与订单列表「获取物流」一致:立即请求物流接口,有运单则推送分享链模板,并回写任务行。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@PostMapping("/fetchShareLinkManually")
|
||||
public AjaxResult fetchShareLinkManually(@RequestBody Map<String, Object> body) {
|
||||
if (body == null || body.get("jobKey") == null) {
|
||||
return AjaxResult.error("jobKey 不能为空");
|
||||
}
|
||||
String jobKey = body.get("jobKey").toString().trim();
|
||||
if (!StringUtils.hasText(jobKey)) {
|
||||
return AjaxResult.error("jobKey 不能为空");
|
||||
}
|
||||
WeComShareLinkLogisticsJob job = weComShareLinkLogisticsJobService.selectByJobKey(jobKey);
|
||||
if (job == null) {
|
||||
return AjaxResult.error("任务不存在");
|
||||
}
|
||||
if ("CANCELLED".equalsIgnoreCase(job.getStatus())) {
|
||||
return AjaxResult.error("任务已取消扫描,请先恢复或新建任务");
|
||||
}
|
||||
if (!StringUtils.hasText(job.getTrackingUrl())) {
|
||||
return AjaxResult.error("该任务无物流短链");
|
||||
}
|
||||
String remark = job.getUserRemark() != null ? job.getUserRemark() : "";
|
||||
String touser = job.getTouserPush() != null ? job.getTouserPush() : "";
|
||||
Map<String, Object> data = logisticsService.adminFetchShareLinkLogisticsDebug(
|
||||
job.getTrackingUrl(), remark, touser);
|
||||
data.put("jobKey", jobKey);
|
||||
|
||||
int successAttempts = job.getScanAttempts() == null ? 1 : job.getScanAttempts() + 1;
|
||||
String adhocNote = data.get("adhocNote") != null ? data.get("adhocNote").toString() : "";
|
||||
String note = "manual:" + adhocNote;
|
||||
if (Boolean.TRUE.equals(data.get("terminalSuccess"))) {
|
||||
String wb = data.get("waybillNo") != null ? data.get("waybillNo").toString() : null;
|
||||
weComShareLinkLogisticsJobMapper.updateByJobKey(jobKey, "PUSHED", note, successAttempts,
|
||||
StringUtils.hasText(wb) ? wb : null);
|
||||
} else {
|
||||
/* 失败仍走自动队列时,不得垫高 scan_attempts,否则 Redis attempts 与定时 drain 上限错位,未超限也会被放弃 */
|
||||
weComShareLinkLogisticsJobMapper.updateByJobKey(jobKey, "WAITING", note, job.getScanAttempts(), null);
|
||||
WeComShareLinkLogisticsJob refreshed = weComShareLinkLogisticsJobService.selectByJobKey(jobKey);
|
||||
if (refreshed != null) {
|
||||
logisticsService.pushShareLinkJobToRedis(refreshed);
|
||||
}
|
||||
}
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动执行一轮与定时任务相同的 Redis 待队列弹出(条数上限同 adhoc-pending-batch-size)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@PostMapping("/drainPendingQueueOnce")
|
||||
public AjaxResult drainPendingQueueOnce() {
|
||||
int n = logisticsService.drainPendingShareLinkQueue();
|
||||
Map<String, Object> r = new LinkedHashMap<>();
|
||||
r.put("processedFromQueue", n);
|
||||
r.put("hint", "为单次弹栈处理条数;每项内部仍可能因未出单重新入队");
|
||||
return AjaxResult.success(r);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单取消等:标记为 CANCELLED,不再被定时对账入队;队列弹出时也会跳过物流请求与推送。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@PostMapping("/cancel")
|
||||
public AjaxResult cancel(@RequestBody Map<String, Object> body) {
|
||||
if (body == null || body.get("jobKey") == null) {
|
||||
return AjaxResult.error("jobKey 不能为空");
|
||||
}
|
||||
String jobKey = body.get("jobKey").toString().trim();
|
||||
if (!StringUtils.hasText(jobKey)) {
|
||||
return AjaxResult.error("jobKey 不能为空");
|
||||
}
|
||||
WeComShareLinkLogisticsJob job = weComShareLinkLogisticsJobService.selectByJobKey(jobKey);
|
||||
if (job == null) {
|
||||
return AjaxResult.error("任务不存在");
|
||||
}
|
||||
if ("CANCELLED".equalsIgnoreCase(job.getStatus())) {
|
||||
return AjaxResult.success("已是取消状态");
|
||||
}
|
||||
String extra = body.get("lastNote") != null ? body.get("lastNote").toString().trim() : "";
|
||||
String note = "manual_cancel";
|
||||
if (StringUtils.hasText(extra)) {
|
||||
note = note + "|" + extra;
|
||||
}
|
||||
if (note.length() > 500) {
|
||||
note = note.substring(0, 500) + "…";
|
||||
}
|
||||
weComShareLinkLogisticsJobMapper.updateByJobKey(jobKey, "CANCELLED", note, null, null);
|
||||
return AjaxResult.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 物理删除任务行(Redis 中已存在的同 jobKey 队列项仍可能被弹出,但会因库中无行而跳过扫描)。
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('jarvis:wecom:shareLinkLog:list')")
|
||||
@DeleteMapping("/{jobKey}")
|
||||
public AjaxResult remove(@PathVariable("jobKey") String jobKey) {
|
||||
if (!StringUtils.hasText(jobKey)) {
|
||||
return AjaxResult.error("jobKey 不能为空");
|
||||
}
|
||||
weComShareLinkLogisticsJobMapper.deleteByJobKey(jobKey.trim());
|
||||
return AjaxResult.success();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
@@ -41,11 +43,35 @@ public class Wps365ToKdocsCallbackRedirectController {
|
||||
|
||||
private ResponseEntity<?> handleWps365(HttpServletRequest request, String code, String error) {
|
||||
if (StringUtils.isBlank(code) && StringUtils.isBlank(error)) {
|
||||
return KdocsCallbackProbeResponses.callbackReadyPage();
|
||||
String jsonBody = readJsonBodyIfPost(request);
|
||||
return KdocsCallbackProbeResponses.callbackReadyJson(request, jsonBody);
|
||||
}
|
||||
String q = request.getQueryString();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setLocation(URI.create(KdocsCallbackUrlBuilder.absoluteKdocsCallback(request, q)));
|
||||
return new ResponseEntity<>(null, headers, HttpStatus.FOUND);
|
||||
}
|
||||
|
||||
private static String readJsonBodyIfPost(HttpServletRequest request) {
|
||||
if (!"POST".equalsIgnoreCase(request.getMethod())) {
|
||||
return null;
|
||||
}
|
||||
String ct = StringUtils.defaultString(request.getContentType()).toLowerCase();
|
||||
if (!ct.contains("application/json")) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
try (BufferedReader r = request.getReader()) {
|
||||
char[] buf = new char[4096];
|
||||
int n;
|
||||
while ((n = r.read(buf)) != -1) {
|
||||
sb.append(buf, 0, n);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import com.ruoyi.common.core.domain.AjaxResult;
|
||||
@@ -12,6 +13,7 @@ import com.ruoyi.common.utils.http.HttpUtils;
|
||||
import com.ruoyi.framework.web.domain.Server;
|
||||
import com.ruoyi.jarvis.service.ILogisticsService;
|
||||
import com.ruoyi.jarvis.service.IWxSendService;
|
||||
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.HashMap;
|
||||
@@ -32,6 +34,9 @@ public class ServerController
|
||||
@Resource
|
||||
private IWxSendService wxSendService;
|
||||
|
||||
@Resource
|
||||
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||
|
||||
/** Ollama 服务地址,用于健康检查 */
|
||||
@Value("${jarvis.ollama.base-url:http://192.168.8.34:11434}")
|
||||
private String ollamaBaseUrl;
|
||||
@@ -72,23 +77,23 @@ public class ServerController
|
||||
healthMap.put("logistics", logisticsMap);
|
||||
}
|
||||
|
||||
// 微信推送服务健康检测
|
||||
try {
|
||||
IWxSendService.HealthCheckResult wxSendHealth = wxSendService.checkHealth();
|
||||
Map<String, Object> wxSendMap = new HashMap<>();
|
||||
wxSendMap.put("healthy", wxSendHealth.isHealthy());
|
||||
wxSendMap.put("status", wxSendHealth.getStatus());
|
||||
wxSendMap.put("message", wxSendHealth.getMessage());
|
||||
wxSendMap.put("serviceUrl", wxSendHealth.getServiceUrl());
|
||||
healthMap.put("wxSend", wxSendMap);
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> wxSendMap = new HashMap<>();
|
||||
wxSendMap.put("healthy", false);
|
||||
wxSendMap.put("status", "异常");
|
||||
wxSendMap.put("message", "健康检测异常: " + e.getMessage());
|
||||
wxSendMap.put("serviceUrl", "");
|
||||
healthMap.put("wxSend", wxSendMap);
|
||||
}
|
||||
// 微信推送:不在此自动下发消息,仅展示配置地址;真实检测见 POST /monitor/server/health/wx-send-test
|
||||
Map<String, Object> wxSendMap = new HashMap<>();
|
||||
wxSendMap.put("manualOnly", true);
|
||||
wxSendMap.put("healthy", null);
|
||||
wxSendMap.put("status", "未检测");
|
||||
wxSendMap.put("message", "点击「测试」将发送一条健康检查消息(会真实推送到微信)");
|
||||
wxSendMap.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
|
||||
healthMap.put("wxSend", wxSendMap);
|
||||
|
||||
// 企微闲鱼通知:仅展示接口地址;真实检测见 POST /monitor/server/health/goofish-notify-test
|
||||
Map<String, Object> goofishMap = new HashMap<>();
|
||||
goofishMap.put("manualOnly", true);
|
||||
goofishMap.put("healthy", null);
|
||||
goofishMap.put("status", "未检测");
|
||||
goofishMap.put("message", "点击「测试」将经 wxSend 向企微闲鱼应用发送一条测试文本");
|
||||
goofishMap.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
|
||||
healthMap.put("goofishNotify", goofishMap);
|
||||
|
||||
// Ollama 服务健康检测(调试用)
|
||||
try {
|
||||
@@ -115,6 +120,54 @@ public class ServerController
|
||||
|
||||
return AjaxResult.success(healthMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动测试微信推送(会真实下发一条消息)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||
@PostMapping("/health/wx-send-test")
|
||||
public AjaxResult testWxSendHealth() {
|
||||
try {
|
||||
IWxSendService.HealthCheckResult r = wxSendService.checkHealth();
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("manualOnly", true);
|
||||
m.put("healthy", r.isHealthy());
|
||||
m.put("status", r.getStatus());
|
||||
m.put("message", r.getMessage());
|
||||
m.put("serviceUrl", r.getServiceUrl());
|
||||
return AjaxResult.success(m);
|
||||
} catch (Exception e) {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("manualOnly", true);
|
||||
m.put("healthy", false);
|
||||
m.put("status", "异常");
|
||||
m.put("message", "检测异常: " + e.getMessage());
|
||||
m.put("serviceUrl", wxSendService.getHealthCheckServiceUrl());
|
||||
return AjaxResult.success(m);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动测试企微闲鱼通知(经 wxSend POST /wx/send/goofish,与 /send/pdd 相同 vanToken + title/text/touser)
|
||||
*/
|
||||
@PreAuthorize("@ss.hasPermi('monitor:server:list')")
|
||||
@PostMapping("/health/goofish-notify-test")
|
||||
public AjaxResult testGoofishNotify() {
|
||||
String err = wxSendGoofishNotifyClient.testGoofishNotify();
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("manualOnly", true);
|
||||
m.put("serviceUrl", wxSendGoofishNotifyClient.getGoofishPushEndpointDisplay());
|
||||
if (err == null) {
|
||||
m.put("healthy", true);
|
||||
m.put("status", "正常");
|
||||
m.put("message", "闲鱼通知测试消息已发送");
|
||||
} else {
|
||||
m.put("healthy", false);
|
||||
m.put("status", "异常");
|
||||
m.put("message", err);
|
||||
}
|
||||
return AjaxResult.success(m);
|
||||
}
|
||||
|
||||
private void putOllamaUnhealthy(Map<String, Object> healthMap, String url, String message) {
|
||||
Map<String, Object> ollamaMap = new HashMap<>();
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package com.ruoyi.web.controller.system;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import com.ruoyi.jarvis.domain.GroupRebateExcelUpload;
|
||||
import com.ruoyi.jarvis.domain.OrderRows;
|
||||
import com.ruoyi.jarvis.service.IGroupRebateExcelUploadService;
|
||||
import com.ruoyi.jarvis.service.impl.GroupRebateExcelImportService;
|
||||
import com.ruoyi.jarvis.service.IOrderRowsService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.common.utils.file.FileUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import com.ruoyi.common.annotation.Log;
|
||||
import com.ruoyi.common.annotation.Anonymous;
|
||||
import com.ruoyi.common.core.controller.BaseController;
|
||||
@@ -16,6 +26,8 @@ import com.ruoyi.common.core.domain.AjaxResult;
|
||||
import com.ruoyi.common.enums.BusinessType;
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.domain.dto.JDOrderSimpleDTO;
|
||||
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||
import com.ruoyi.jarvis.service.IJDOrderProfitService;
|
||||
import com.ruoyi.jarvis.service.IJDOrderService;
|
||||
import com.ruoyi.jarvis.service.IInstructionService;
|
||||
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||
@@ -32,13 +44,23 @@ public class JDOrderListController extends BaseController
|
||||
{
|
||||
|
||||
private final IJDOrderService jdOrderService;
|
||||
private final IJDOrderProfitService jdOrderProfitService;
|
||||
private final IOrderRowsService orderRowsService;
|
||||
private final IInstructionService instructionService;
|
||||
private final GroupRebateExcelImportService groupRebateExcelImportService;
|
||||
private final IGroupRebateExcelUploadService groupRebateExcelUploadService;
|
||||
|
||||
public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, IInstructionService instructionService) {
|
||||
public JDOrderListController(IJDOrderService jdOrderService, IJDOrderProfitService jdOrderProfitService,
|
||||
IOrderRowsService orderRowsService,
|
||||
IInstructionService instructionService,
|
||||
GroupRebateExcelImportService groupRebateExcelImportService,
|
||||
IGroupRebateExcelUploadService groupRebateExcelUploadService) {
|
||||
this.jdOrderService = jdOrderService;
|
||||
this.jdOrderProfitService = jdOrderProfitService;
|
||||
this.orderRowsService = orderRowsService;
|
||||
this.instructionService = instructionService;
|
||||
this.groupRebateExcelImportService = groupRebateExcelImportService;
|
||||
this.groupRebateExcelUploadService = groupRebateExcelUploadService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -74,7 +96,18 @@ public class JDOrderListController extends BaseController
|
||||
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
|
||||
query.getParams().put("orderSearch", orderSearch.trim());
|
||||
}
|
||||
|
||||
|
||||
String rebateRemarkAbnormal = request.getParameter("rebateRemarkHasAbnormal");
|
||||
if (rebateRemarkAbnormal != null && !rebateRemarkAbnormal.isEmpty()) {
|
||||
query.setRebateRemarkHasAbnormal(Integer.valueOf(rebateRemarkAbnormal));
|
||||
}
|
||||
if ("true".equalsIgnoreCase(request.getParameter("hasRebateRemark"))) {
|
||||
query.getParams().put("hasRebateRemark", true);
|
||||
}
|
||||
if ("true".equalsIgnoreCase(request.getParameter("rebateWithoutUploadLink"))) {
|
||||
query.getParams().put("rebateWithoutUploadLink", true);
|
||||
}
|
||||
|
||||
java.util.List<JDOrder> list;
|
||||
if (orderBy != null && !orderBy.isEmpty()) {
|
||||
// 设置排序参数
|
||||
@@ -125,6 +158,97 @@ public class JDOrderListController extends BaseController
|
||||
return dataTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷录单页:型号下拉数据;每型号取 jd_order 主键最大的一条的付款与后返(通常即最近落库单)
|
||||
*/
|
||||
@GetMapping("/quickRecord/modelOptions")
|
||||
public AjaxResult quickRecordModelOptions() {
|
||||
List<QuickRecordModelOption> options = jdOrderService.selectQuickRecordModelOptions();
|
||||
return AjaxResult.success(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入跟团返现类 Excel:按「单号/订单号」匹配系统订单,将「是否返现」「总共返现」等写入后返备注(可多次导入累加);文件落盘并记上传记录。
|
||||
*/
|
||||
@Log(title = "JD订单后返表导入", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/importGroupRebateExcel")
|
||||
public AjaxResult importGroupRebateExcel(@RequestParam("file") MultipartFile file,
|
||||
@RequestParam(value = "documentTitle", required = false) String documentTitle) {
|
||||
try {
|
||||
Map<String, Object> data = groupRebateExcelImportService.importExcel(file, documentTitle);
|
||||
return AjaxResult.success(data);
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error("导入失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入后返表:请求内按顺序处理多个文件,前端仅需一次提交;结果概要返回,明细见上传记录。
|
||||
*/
|
||||
@Log(title = "JD订单后返表批量导入", businessType = BusinessType.IMPORT)
|
||||
@PostMapping("/importGroupRebateExcelBatch")
|
||||
public AjaxResult importGroupRebateExcelBatch(@RequestParam("files") MultipartFile[] files,
|
||||
@RequestParam(value = "documentTitle", required = false) String documentTitle) {
|
||||
try {
|
||||
Map<String, Object> data = groupRebateExcelImportService.importExcelBatch(files, documentTitle);
|
||||
if (Boolean.FALSE.equals(data.get("success"))) {
|
||||
return AjaxResult.error((String) data.get("message"));
|
||||
}
|
||||
return AjaxResult.success(data);
|
||||
} catch (Exception e) {
|
||||
return AjaxResult.error("批量导入失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后返表上传记录(分页)
|
||||
*/
|
||||
@GetMapping("/groupRebateUpload/list")
|
||||
public TableDataInfo groupRebateUploadList(GroupRebateExcelUpload query, HttpServletRequest request) {
|
||||
startPage();
|
||||
String beginTimeStr = request.getParameter("beginTime");
|
||||
String endTimeStr = request.getParameter("endTime");
|
||||
if (beginTimeStr != null && !beginTimeStr.isEmpty()) {
|
||||
query.getParams().put("beginTime", beginTimeStr);
|
||||
}
|
||||
if (endTimeStr != null && !endTimeStr.isEmpty()) {
|
||||
query.getParams().put("endTime", endTimeStr);
|
||||
}
|
||||
List<GroupRebateExcelUpload> list = groupRebateExcelUploadService.selectGroupRebateExcelUploadList(query);
|
||||
return getDataTable(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除一条后返表上传记录,并撤销写入订单的后返备注(依赖 uploadRecordId / affectedOrderIds;历史导入可能仅删记录)
|
||||
*/
|
||||
@Log(title = "后返表上传记录", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/groupRebateUpload/{id}")
|
||||
public AjaxResult deleteGroupRebateUpload(@PathVariable("id") Long id) {
|
||||
return AjaxResult.success(groupRebateExcelImportService.deleteUploadRecord(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新下载已上传的后返表原件(与通用 download 一致,使用 POST + blob)
|
||||
*/
|
||||
@Log(title = "后返表上传记录下载", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/groupRebateUpload/download/{id}")
|
||||
public void downloadGroupRebateUpload(@PathVariable("id") Long id, HttpServletResponse response) throws Exception {
|
||||
GroupRebateExcelUpload rec = groupRebateExcelUploadService.selectGroupRebateExcelUploadById(id);
|
||||
if (rec == null || StringUtils.isEmpty(rec.getFilePath())) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
File file = GroupRebateExcelImportService.resolveDiskFile(rec.getFilePath());
|
||||
if (!file.isFile()) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
return;
|
||||
}
|
||||
String downloadName = StringUtils.isNotEmpty(rec.getOriginalFilename()) ? rec.getOriginalFilename() : file.getName();
|
||||
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
|
||||
FileUtils.setAttachmentResponseHeader(response, downloadName);
|
||||
FileUtils.writeBytes(file.getAbsolutePath(), response.getOutputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出JD订单列表
|
||||
*/
|
||||
@@ -174,6 +298,8 @@ public class JDOrderListController extends BaseController
|
||||
@PutMapping
|
||||
public AjaxResult edit(@RequestBody JDOrder jdOrder)
|
||||
{
|
||||
jdOrderProfitService.recalculate(jdOrder);
|
||||
jdOrder.getParams().put("applyProfitFields", Boolean.TRUE);
|
||||
return toAjax(jdOrderService.updateJDOrder(jdOrder));
|
||||
}
|
||||
|
||||
@@ -265,9 +391,75 @@ public class JDOrderListController extends BaseController
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次性批量更新历史订单:将赔付金额大于0的订单标记为后返到账
|
||||
* 此方法只应执行一次,用于处理历史数据
|
||||
* 列表刷新后:对利润未手动的订单按规则重算,仅结果变化时落库(不解除售价/利润锁定)
|
||||
*/
|
||||
@Log(title = "JD订单同步自动利润", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/tools/sync-auto-profit")
|
||||
@SuppressWarnings("unchecked")
|
||||
public AjaxResult syncAutoProfit(@RequestBody(required = false) Map<String, Object> body) {
|
||||
if (body == null || !body.containsKey("ids")) {
|
||||
return AjaxResult.error("请传入 ids 数组");
|
||||
}
|
||||
Object raw = body.get("ids");
|
||||
if (!(raw instanceof List)) {
|
||||
return AjaxResult.error("ids 须为数组");
|
||||
}
|
||||
List<?> idList = (List<?>) raw;
|
||||
if (idList.isEmpty()) {
|
||||
Map<String, Object> empty = new HashMap<>(2);
|
||||
empty.put("updated", 0);
|
||||
return AjaxResult.success(empty);
|
||||
}
|
||||
List<Long> ids = new ArrayList<>(idList.size());
|
||||
for (Object o : idList) {
|
||||
if (o == null) {
|
||||
continue;
|
||||
}
|
||||
ids.add(((Number) o).longValue());
|
||||
}
|
||||
int n = jdOrderProfitService.syncAutoProfitIfChanged(ids);
|
||||
Map<String, Object> data = new HashMap<>(2);
|
||||
data.put("updated", n);
|
||||
return AjaxResult.success(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 ID 批量重算售价(自动从型号配置回填)与利润(清除手动锁定后按规则计算)
|
||||
*/
|
||||
@Log(title = "JD订单批量重算利润", businessType = BusinessType.UPDATE)
|
||||
@PostMapping("/tools/recalc-profit")
|
||||
@SuppressWarnings("unchecked")
|
||||
public AjaxResult recalcProfitBatch(@RequestBody(required = false) Map<String, Object> body) {
|
||||
if (body == null || !body.containsKey("ids")) {
|
||||
return AjaxResult.error("请传入 ids 数组");
|
||||
}
|
||||
Object raw = body.get("ids");
|
||||
if (!(raw instanceof List)) {
|
||||
return AjaxResult.error("ids 须为数组");
|
||||
}
|
||||
List<?> idList = (List<?>) raw;
|
||||
if (idList.isEmpty()) {
|
||||
return AjaxResult.error("ids 不能为空");
|
||||
}
|
||||
int affected = 0;
|
||||
for (Object o : idList) {
|
||||
if (o == null) {
|
||||
continue;
|
||||
}
|
||||
long id = ((Number) o).longValue();
|
||||
JDOrder order = jdOrderService.selectJDOrderById(id);
|
||||
if (order == null) {
|
||||
continue;
|
||||
}
|
||||
order.setProfitManual(0);
|
||||
order.setSellingPriceManual(0);
|
||||
jdOrderProfitService.recalculate(order);
|
||||
order.getParams().put("applyProfitFields", Boolean.TRUE);
|
||||
affected += jdOrderService.updateJDOrder(order);
|
||||
}
|
||||
return AjaxResult.success("已更新 " + affected + " 条订单的售价/利润字段");
|
||||
}
|
||||
|
||||
@Log(title = "批量标记后返到账", businessType = BusinessType.UPDATE)
|
||||
@RequestMapping(value = "/tools/batch-mark-rebate-received", method = {RequestMethod.POST, RequestMethod.GET})
|
||||
public AjaxResult batchMarkRebateReceivedForCompensation() {
|
||||
@@ -308,7 +500,18 @@ public class JDOrderListController extends BaseController
|
||||
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
|
||||
query.getParams().put("orderSearch", orderSearch.trim());
|
||||
}
|
||||
|
||||
|
||||
String rebateRemarkAbnormal = request.getParameter("rebateRemarkHasAbnormal");
|
||||
if (rebateRemarkAbnormal != null && !rebateRemarkAbnormal.isEmpty()) {
|
||||
query.setRebateRemarkHasAbnormal(Integer.valueOf(rebateRemarkAbnormal));
|
||||
}
|
||||
if ("true".equalsIgnoreCase(request.getParameter("hasRebateRemark"))) {
|
||||
query.getParams().put("hasRebateRemark", true);
|
||||
}
|
||||
if ("true".equalsIgnoreCase(request.getParameter("rebateWithoutUploadLink"))) {
|
||||
query.getParams().put("rebateWithoutUploadLink", true);
|
||||
}
|
||||
|
||||
// 处理其他查询参数
|
||||
if (query.getRemark() != null && !query.getRemark().trim().isEmpty()) {
|
||||
query.setRemark(query.getRemark().trim());
|
||||
@@ -319,6 +522,9 @@ public class JDOrderListController extends BaseController
|
||||
if (query.getModelNumber() != null && !query.getModelNumber().trim().isEmpty()) {
|
||||
query.setModelNumber(query.getModelNumber().trim());
|
||||
}
|
||||
if (query.getModelNumberExclude() != null && !query.getModelNumberExclude().trim().isEmpty()) {
|
||||
query.setModelNumberExclude(query.getModelNumberExclude().trim());
|
||||
}
|
||||
if (query.getBuyer() != null && !query.getBuyer().trim().isEmpty()) {
|
||||
query.setBuyer(query.getBuyer().trim());
|
||||
}
|
||||
|
||||
@@ -200,11 +200,51 @@ jarvis:
|
||||
# 物流接口服务地址
|
||||
logistics:
|
||||
base-url: http://192.168.8.88:5001
|
||||
# 同机多进程多端口时配置逗号分隔列表;非空时仅按下列地址轮询,不再使用 base-url
|
||||
base-urls: http://192.168.8.88:5001,http://192.168.8.88:5002,http://192.168.8.88:5003
|
||||
fetch-path: /fetch_logistics
|
||||
health-path: /health
|
||||
# 每次定时任务最多处理多少条企微分享链待队列(RPUSH 入队、LPOP 出队)
|
||||
adhoc-pending-batch-size: 50
|
||||
# 物流扫描(LogisticsScanTask):轮询 JD 单拉运单 + drain 分享链队列
|
||||
scan:
|
||||
cron: "0 */20 * * * ?"
|
||||
order-delay-ms: 250
|
||||
# 0=不限制;例如 40 可控制单轮最长耗时(余下下轮再扫)
|
||||
max-orders-per-round: 0
|
||||
# 获取评论接口服务地址(后端转发,避免前端跨域)
|
||||
fetch-comments:
|
||||
base-url: http://192.168.8.60:5008
|
||||
# 企微经 wxSend 调用本接口时校验(须与 wxSend 配置一致)
|
||||
wecom:
|
||||
inbound-secret: jarvis_wecom_bridge_change_me
|
||||
# wxSend 根地址(无尾斜杠),用于 F 录单等第二条起主动推送;与 wxSend server.port 一致
|
||||
wxsend-base-url: http://127.0.0.1:36699
|
||||
# 须与 wxSend jarvis.wecom.push-secret 一致(Header X-WxSend-WeCom-Push-Secret)
|
||||
push-secret: jarvis_wecom_push_change_me
|
||||
# 与 /wx/send/pdd、/wx/send/goofish 请求头 vanToken 一致(wxSend TokenUtil)
|
||||
wxsend-van-token: super_token_b62190c26
|
||||
# 接收企微通知的成员 UserID,多个逗号或 |;留空则不推送
|
||||
goofish-notify-touser: "LinPinFan"
|
||||
# 多轮会话:与 JDUtil interaction_state 类似,TTL 与空闲超时(分钟)
|
||||
session-ttl-minutes: 30
|
||||
session-idle-timeout-minutes: 30
|
||||
session-sweep-ms: 60000
|
||||
# 企微「开」/「慢开」+ 手机号:POST body 含 text(手机号)与 bot;响应 reply_text 被动回复用户
|
||||
phone-forward:
|
||||
enabled: true
|
||||
base-url: http://192.168.8.60:18080
|
||||
path: /v1/forward
|
||||
connect-timeout-ms: 8000
|
||||
# wait_reply 时服务端会等多条 Bot 回复,宜适当加大
|
||||
read-timeout-ms: 120000
|
||||
wait-reply: true
|
||||
# 多台企微线程同时触发时串行调用 tg_bridge;排队超过该毫秒则提示「正忙」(0 表示一直等到上一条结束)
|
||||
lock-acquire-timeout-ms: 180000
|
||||
# 连续失败后熔断,不再发起 HTTP(与 tg_bridge 侧熔断互不替代)
|
||||
circuit-failure-threshold: 5
|
||||
circuit-open-ms: 120000
|
||||
# reply_take_nth:仅「开」用 2;「慢开」由 tg_bridge reply_adaptive_skip_middle_ad 在 2/3 条间自适应
|
||||
# Ollama 大模型服务(监控健康度调试用)
|
||||
ollama:
|
||||
base-url: http://192.168.8.34:11434
|
||||
@@ -231,5 +271,32 @@ tencent:
|
||||
# 刷新Token地址(用于通过refresh_token刷新access_token)
|
||||
refresh-token-url: https://docs.qq.com/oauth/v2/token
|
||||
|
||||
# 闲管家订单:RocketMQ(配置后订单推送走 MQ;不配则走线程池异步)
|
||||
#rocketmq:
|
||||
# name-server: 127.0.0.1:9876
|
||||
# producer:
|
||||
# group: jarvis-goofish-producer
|
||||
# send-message-timeout: 3000
|
||||
|
||||
jarvis:
|
||||
goofish-order:
|
||||
mq-topic: jarvis-goofish-erp-order
|
||||
consumer-group: jarvis-goofish-order-consumer
|
||||
pull-lookback-hours: 72
|
||||
pull-cron: "0 * * * * ?"
|
||||
auto-ship-cron: "0 2/10 * * * ?"
|
||||
# 订单列表:每页条数(最大 100)
|
||||
pull-page-size: 100
|
||||
# 每授权单次最大页数(最大 100;与 page_size 乘积勿超 10000)
|
||||
pull-max-pages-per-shop: 100
|
||||
# 全量拉单按 update_time 分段(秒),默认 7 天(且不超过 pull-max-update-time-range-seconds)
|
||||
pull-time-chunk-seconds: 604800
|
||||
# 单次列表请求 update_time 最大跨度(秒),须满足平台「6个月内」;默认 180 天
|
||||
pull-max-update-time-range-seconds: 15552000
|
||||
# 全量拉单起点:距今多少天(默认约 3 年)
|
||||
pull-full-history-days: 1095
|
||||
# true=仅拉 auto-ship-order-statuses(省调用,其它状态依赖推送);false=时间窗内全状态(推荐,与本地对齐)
|
||||
pull-list-only-auto-ship-statuses: false
|
||||
auto-ship-batch-size: 20
|
||||
|
||||
|
||||
|
||||
@@ -200,15 +200,42 @@ jarvis:
|
||||
# 物流接口服务地址
|
||||
logistics:
|
||||
base-url: http://127.0.0.1:5001
|
||||
# 同机多进程多端口时配置逗号分隔列表;非空时仅按下列地址轮询,不再使用 base-url
|
||||
base-urls: http://127.0.0.1:5001,http://127.0.0.1:5002,http://127.0.0.1:5003
|
||||
fetch-path: /fetch_logistics
|
||||
health-path: /health
|
||||
adhoc-pending-batch-size: 50
|
||||
scan:
|
||||
cron: "0 */20 * * * ?"
|
||||
order-delay-ms: 250
|
||||
max-orders-per-round: 0
|
||||
# 获取评论接口服务地址(后端转发)
|
||||
fetch-comments:
|
||||
base-url: http://192.168.8.60:5008
|
||||
wecom:
|
||||
inbound-secret: jarvis_wecom_bridge_change_me
|
||||
wxsend-base-url: https://wxts.van333.cn
|
||||
push-secret: jarvis_wecom_push_change_me
|
||||
wxsend-van-token: super_token_b62190c26
|
||||
goofish-notify-touser: "LinPingFan"
|
||||
session-ttl-minutes: 30
|
||||
session-idle-timeout-minutes: 30
|
||||
session-sweep-ms: 60000
|
||||
phone-forward:
|
||||
enabled: true
|
||||
base-url: http://192.168.8.60:18080
|
||||
path: /v1/forward
|
||||
connect-timeout-ms: 8000
|
||||
read-timeout-ms: 120000
|
||||
wait-reply: true
|
||||
lock-acquire-timeout-ms: 180000
|
||||
circuit-failure-threshold: 5
|
||||
circuit-open-ms: 120000
|
||||
# 「开」取第 2 条;「慢开」由桥接自适应第 2/3 条
|
||||
# Ollama 大模型服务(监控健康度调试用)
|
||||
ollama:
|
||||
base-url: http://192.168.8.34:11434
|
||||
model: qwen3.5:9b
|
||||
model: qwen3.5:9b-32k
|
||||
# 腾讯文档开放平台配置
|
||||
# 文档地址:https://docs.qq.com/open/document/app/openapi/v3/sheet/model/spreadsheet.html
|
||||
tencent:
|
||||
|
||||
@@ -13,8 +13,8 @@ tencent:
|
||||
kdocs:
|
||||
api-host: https://developer.kdocs.cn
|
||||
# 在开发者后台创建应用后填写 app_id / app_key
|
||||
app-id: ""
|
||||
app-key: ""
|
||||
app-id: AK20260114NNQJKV
|
||||
app-key: 4c58bc1642e5e8fa731f75af9370496a
|
||||
# 与后台登记的回调一致,建议使用独立路径(勿被前端路由拦截)
|
||||
redirect-uri: https://jarvis.van333.cn/kdocs-callback
|
||||
# 逗号分隔,须与应用申请权限一致:https://developer.kdocs.cn/server/guide/permission.html
|
||||
|
||||
114
ruoyi-admin/src/main/resources/sql/erp_goofish_init.sql
Normal file
114
ruoyi-admin/src/main/resources/sql/erp_goofish_init.sql
Normal file
@@ -0,0 +1,114 @@
|
||||
-- 闲管家开放平台:应用配置中心 + ERP 订单落库(执行前请备份)
|
||||
-- 说明:菜单需在「系统管理-菜单管理」中自行新增,组件路径示例:
|
||||
-- 配置中心 system/goofish/erpOpenConfig/index
|
||||
-- 订单跟踪 system/goofish/erpGoofishOrder/index
|
||||
-- 变更日志(跨单排查,对接 GET /jarvis/erpGoofishOrder/eventLog/list)system/goofish/erpGoofishEventLog/index
|
||||
|
||||
CREATE TABLE IF NOT EXISTS erp_open_config (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
app_key varchar(64) NOT NULL COMMENT '开放平台 AppKey(appid)',
|
||||
app_secret varchar(128) NOT NULL COMMENT '开放平台 AppSecret',
|
||||
xy_user_name varchar(128) DEFAULT NULL COMMENT '默认闲鱼会员名(展示)',
|
||||
express_code varchar(64) DEFAULT NULL COMMENT '发货用快递公司编码(日日顺物流在官方列表中为 rrs,以「查询快递公司」接口为准)',
|
||||
express_name varchar(64) DEFAULT NULL COMMENT '快递公司名称(展示)',
|
||||
status char(1) NOT NULL DEFAULT '0' COMMENT '0正常 1停用',
|
||||
order_num int(11) NOT NULL DEFAULT 0 COMMENT '排序(小优先)',
|
||||
create_by varchar(64) DEFAULT '',
|
||||
create_time datetime DEFAULT NULL,
|
||||
update_by varchar(64) DEFAULT '',
|
||||
update_time datetime DEFAULT NULL,
|
||||
remark varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_app_key (app_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲管家开放平台应用配置';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS erp_goofish_order (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
app_key varchar(64) NOT NULL COMMENT 'AppKey',
|
||||
seller_id bigint(20) DEFAULT NULL COMMENT '商家ID',
|
||||
user_name varchar(128) DEFAULT NULL COMMENT '闲鱼会员名',
|
||||
order_no varchar(64) NOT NULL COMMENT '闲鱼订单号',
|
||||
order_type int(11) DEFAULT NULL COMMENT '订单类型',
|
||||
order_status int(11) DEFAULT NULL COMMENT '订单状态(推送/列表)',
|
||||
refund_status int(11) DEFAULT NULL COMMENT '退款状态',
|
||||
modify_time bigint(20) DEFAULT NULL COMMENT '订单更新时间(秒)',
|
||||
product_id bigint(20) DEFAULT NULL COMMENT '管家商品ID',
|
||||
item_id bigint(20) DEFAULT NULL COMMENT '闲鱼商品ID',
|
||||
goods_title varchar(512) DEFAULT NULL COMMENT '商品标题(详情 goods.title)',
|
||||
goods_image_url varchar(1024) DEFAULT NULL COMMENT '商品主图 URL(goods.images 首图)',
|
||||
buyer_nick varchar(256) DEFAULT NULL COMMENT '买家昵称(详情 buyer_nick)',
|
||||
pay_amount bigint(20) DEFAULT NULL COMMENT '实付金额(分) pay_amount',
|
||||
detail_waybill_no varchar(128) DEFAULT NULL COMMENT '闲管家详情回传运单号 waybill_no',
|
||||
detail_express_code varchar(64) DEFAULT NULL COMMENT '详情快递编码 express_code',
|
||||
detail_express_name varchar(128) DEFAULT NULL COMMENT '详情快递名称 express_name',
|
||||
receiver_name varchar(128) DEFAULT NULL COMMENT '收货人(详情有则落库)',
|
||||
receiver_mobile varchar(64) DEFAULT NULL COMMENT '收货手机',
|
||||
receiver_address varchar(1000) DEFAULT NULL COMMENT '收货详细地址(address)',
|
||||
receiver_region varchar(256) DEFAULT NULL COMMENT '省市区街道等拼接展示',
|
||||
recv_prov_name varchar(64) DEFAULT NULL COMMENT 'prov_name',
|
||||
recv_city_name varchar(64) DEFAULT NULL COMMENT 'city_name',
|
||||
recv_area_name varchar(64) DEFAULT NULL COMMENT 'area_name',
|
||||
recv_town_name varchar(128) DEFAULT NULL COMMENT 'town_name',
|
||||
detail_json longtext COMMENT '订单详情接口全量 JSON',
|
||||
last_notify_json longtext COMMENT '最近一次推送原文 JSON',
|
||||
jd_order_id bigint(20) DEFAULT NULL COMMENT '关联 jd_order.id(第三方单号=闲鱼单号)',
|
||||
local_waybill_no varchar(128) DEFAULT NULL COMMENT '本地物流扫描得到的运单号',
|
||||
ship_status tinyint(4) NOT NULL DEFAULT '0' COMMENT '0未发货 1已调用发货成功 2发货失败',
|
||||
ship_error varchar(500) DEFAULT NULL COMMENT '发货失败原因',
|
||||
ship_time datetime DEFAULT NULL COMMENT '发货调用成功时间',
|
||||
ship_express_code varchar(64) DEFAULT NULL COMMENT '实际发货使用的快递编码',
|
||||
create_time datetime DEFAULT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_app_order (app_key, order_no),
|
||||
KEY idx_jd_order (jd_order_id),
|
||||
KEY idx_order_status (order_status),
|
||||
KEY idx_modify_time (modify_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲管家 ERP 订单(全量跟踪)';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS erp_goofish_order_event_log (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
order_id bigint(20) NOT NULL COMMENT 'erp_goofish_order.id',
|
||||
app_key varchar(64) DEFAULT NULL,
|
||||
order_no varchar(64) NOT NULL,
|
||||
event_type varchar(32) NOT NULL COMMENT 'ORDER_SYNC/LOGISTICS_SYNC/SHIP',
|
||||
source varchar(64) NULL COMMENT 'NOTIFY/LIST/DETAIL_REFRESH 等',
|
||||
message varchar(1024) NOT NULL,
|
||||
create_time datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_goofish_evt_order (order_id),
|
||||
KEY idx_goofish_evt_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲管家订单状态/物流/发货变更日志';
|
||||
|
||||
-- 可选:从旧枚举迁入两条示例(密钥请上线后立即修改)
|
||||
-- INSERT INTO erp_open_config (app_key,app_secret,xy_user_name,remark,express_code,express_name,status,order_num)
|
||||
-- VALUES ('1016208368633221','***','余生请多关照66','海尔胡歌',NULL,'日日顺','0',1);
|
||||
|
||||
-- 若依菜单(在「菜单管理」手工添加时参考)
|
||||
-- 父菜单:系统管理下新增目录「闲管家ERP」
|
||||
-- 子菜单1:组件 system/goofish/erpOpenConfig/index 权限前缀 jarvis:erpOpenConfig
|
||||
-- 子菜单2:组件 system/goofish/erpGoofishOrder/index 权限前缀 jarvis:erpGoofishOrder
|
||||
-- 子菜单3:组件 system/goofish/erpGoofishEventLog/index 权限沿用 jarvis:erpGoofishOrder:list 即可(仅列表查询)
|
||||
-- 路由地址建议与订单页同级,如订单为 …/erpGoofishOrder 则本页 …/erpGoofishEventLog(订单页「变更日志排查」按钮依赖此规则)
|
||||
-- 按钮权限示例:
|
||||
-- jarvis:erpOpenConfig:list,query,add,edit,remove
|
||||
-- jarvis:erpGoofishOrder:list,query,edit
|
||||
|
||||
-- —— 已建表升级:详情摘要字段(若列已存在会报错,可逐条执行并忽略)——
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN goods_title varchar(512) NULL COMMENT '商品标题' AFTER item_id;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN goods_image_url varchar(1024) NULL COMMENT '商品主图URL' AFTER goods_title;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN buyer_nick varchar(256) NULL COMMENT '买家昵称' AFTER goods_image_url;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN pay_amount bigint(20) NULL COMMENT '实付金额(分)' AFTER buyer_nick;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN detail_waybill_no varchar(128) NULL COMMENT '详情运单号' AFTER pay_amount;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN detail_express_code varchar(64) NULL COMMENT '详情快递编码' AFTER detail_waybill_no;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN detail_express_name varchar(128) NULL COMMENT '详情快递名称' AFTER detail_express_code;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN receiver_name varchar(128) NULL COMMENT '收货人' AFTER detail_express_name;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN receiver_mobile varchar(64) NULL COMMENT '收货手机' AFTER receiver_name;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN receiver_address varchar(1000) NULL COMMENT '收货地址' AFTER receiver_mobile;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN receiver_region varchar(256) NULL COMMENT '省市区' AFTER receiver_address;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN recv_prov_name varchar(64) NULL COMMENT 'prov_name' AFTER receiver_region;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN recv_city_name varchar(64) NULL COMMENT 'city_name' AFTER recv_prov_name;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN recv_area_name varchar(64) NULL COMMENT 'area_name' AFTER recv_city_name;
|
||||
-- ALTER TABLE erp_goofish_order ADD COLUMN recv_town_name varchar(128) NULL COMMENT 'town_name' AFTER recv_area_name;
|
||||
|
||||
-- 已建库一键升级(可重复执行、自动判存):请使用同目录 erp_goofish_upgrade.sql
|
||||
212
ruoyi-admin/src/main/resources/sql/erp_goofish_upgrade.sql
Normal file
212
ruoyi-admin/src/main/resources/sql/erp_goofish_upgrade.sql
Normal file
@@ -0,0 +1,212 @@
|
||||
-- =============================================================================
|
||||
-- 闲管家 ERP:erp_open_config / erp_goofish_order 一键升级脚本
|
||||
-- =============================================================================
|
||||
-- 用法:
|
||||
-- 1. 先备份数据库;连接目标库后执行(或在文件开头加 USE your_database;)
|
||||
-- 2. 可重复执行:已存在的列/索引会自动跳过
|
||||
-- 3. ADD COLUMN 一律不指定 AFTER,避免旧表缺中间列时升级失败(新列落在表尾,不影响业务)
|
||||
-- 环境:MySQL 5.7+ / 8.x(MariaDB 未逐项验证)
|
||||
-- =============================================================================
|
||||
|
||||
SET @schema := DATABASE();
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 1) 表不存在时创建完整结构(与 erp_goofish_init.sql 一致)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS erp_open_config (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
app_key varchar(64) NOT NULL COMMENT '开放平台 AppKey(appid)',
|
||||
app_secret varchar(128) NOT NULL COMMENT '开放平台 AppSecret',
|
||||
xy_user_name varchar(128) DEFAULT NULL COMMENT '默认闲鱼会员名(展示)',
|
||||
express_code varchar(64) DEFAULT NULL COMMENT '发货用快递公司编码(日日顺物流在官方列表中为 rrs)',
|
||||
express_name varchar(64) DEFAULT NULL COMMENT '快递公司名称(展示)',
|
||||
status char(1) NOT NULL DEFAULT '0' COMMENT '0正常 1停用',
|
||||
order_num int(11) NOT NULL DEFAULT 0 COMMENT '排序(小优先)',
|
||||
create_by varchar(64) DEFAULT '',
|
||||
create_time datetime DEFAULT NULL,
|
||||
update_by varchar(64) DEFAULT '',
|
||||
update_time datetime DEFAULT NULL,
|
||||
remark varchar(500) DEFAULT NULL COMMENT '备注',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_app_key (app_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲管家开放平台应用配置';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS erp_goofish_order (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
app_key varchar(64) NOT NULL COMMENT 'AppKey',
|
||||
seller_id bigint(20) DEFAULT NULL COMMENT '商家ID',
|
||||
user_name varchar(128) DEFAULT NULL COMMENT '闲鱼会员名',
|
||||
order_no varchar(64) NOT NULL COMMENT '闲鱼订单号',
|
||||
order_type int(11) DEFAULT NULL COMMENT '订单类型',
|
||||
order_status int(11) DEFAULT NULL COMMENT '订单状态(推送/列表)',
|
||||
refund_status int(11) DEFAULT NULL COMMENT '退款状态',
|
||||
modify_time bigint(20) DEFAULT NULL COMMENT '订单更新时间(秒)',
|
||||
product_id bigint(20) DEFAULT NULL COMMENT '管家商品ID',
|
||||
item_id bigint(20) DEFAULT NULL COMMENT '闲鱼商品ID',
|
||||
goods_title varchar(512) DEFAULT NULL COMMENT '商品标题(详情 goods.title)',
|
||||
goods_image_url varchar(1024) DEFAULT NULL COMMENT '商品主图 URL(goods.images 首图)',
|
||||
buyer_nick varchar(256) DEFAULT NULL COMMENT '买家昵称(详情 buyer_nick)',
|
||||
pay_amount bigint(20) DEFAULT NULL COMMENT '实付金额(分) pay_amount',
|
||||
detail_waybill_no varchar(128) DEFAULT NULL COMMENT '闲管家详情回传运单号 waybill_no',
|
||||
detail_express_code varchar(64) DEFAULT NULL COMMENT '详情快递编码 express_code',
|
||||
detail_express_name varchar(128) DEFAULT NULL COMMENT '详情快递名称 express_name',
|
||||
receiver_name varchar(128) DEFAULT NULL COMMENT '收货人(详情有则落库)',
|
||||
receiver_mobile varchar(64) DEFAULT NULL COMMENT '收货手机',
|
||||
receiver_address varchar(1000) DEFAULT NULL COMMENT '收货详细地址(address)',
|
||||
receiver_region varchar(256) DEFAULT NULL COMMENT '省市区街道等拼接展示',
|
||||
recv_prov_name varchar(64) DEFAULT NULL COMMENT 'prov_name',
|
||||
recv_city_name varchar(64) DEFAULT NULL COMMENT 'city_name',
|
||||
recv_area_name varchar(64) DEFAULT NULL COMMENT 'area_name',
|
||||
recv_town_name varchar(128) DEFAULT NULL COMMENT 'town_name',
|
||||
detail_json longtext COMMENT '订单详情接口全量 JSON',
|
||||
last_notify_json longtext COMMENT '最近一次推送原文 JSON',
|
||||
jd_order_id bigint(20) DEFAULT NULL COMMENT '关联 jd_order.id(第三方单号=闲鱼单号)',
|
||||
local_waybill_no varchar(128) DEFAULT NULL COMMENT '本地物流扫描得到的运单号',
|
||||
ship_status tinyint(4) NOT NULL DEFAULT '0' COMMENT '0未发货 1已调用发货成功 2发货失败',
|
||||
ship_error varchar(500) DEFAULT NULL COMMENT '发货失败原因',
|
||||
ship_time datetime DEFAULT NULL COMMENT '发货调用成功时间',
|
||||
ship_express_code varchar(64) DEFAULT NULL COMMENT '实际发货使用的快递编码',
|
||||
create_time datetime DEFAULT NULL,
|
||||
update_time datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_app_order (app_key, order_no),
|
||||
KEY idx_jd_order (jd_order_id),
|
||||
KEY idx_order_status (order_status),
|
||||
KEY idx_modify_time (modify_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲管家 ERP 订单(全量跟踪)';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 2) 存储过程:列不存在则 ADD
|
||||
-- -----------------------------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS jarvis_erp_goofish_add_column;
|
||||
DROP PROCEDURE IF EXISTS jarvis_erp_goofish_add_index;
|
||||
|
||||
DELIMITER $$
|
||||
|
||||
CREATE PROCEDURE jarvis_erp_goofish_add_column(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_column VARCHAR(64),
|
||||
IN p_ddl TEXT
|
||||
)
|
||||
BEGIN
|
||||
DECLARE col_exists INT DEFAULT 0;
|
||||
SELECT COUNT(*) INTO col_exists
|
||||
FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = @schema
|
||||
AND TABLE_NAME = p_table
|
||||
AND COLUMN_NAME = p_column;
|
||||
IF col_exists = 0 THEN
|
||||
SET @stmt := CONCAT('ALTER TABLE `', p_table, '` ADD COLUMN `', p_column, '` ', p_ddl);
|
||||
PREPARE ps FROM @stmt;
|
||||
EXECUTE ps;
|
||||
DEALLOCATE PREPARE ps;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
CREATE PROCEDURE jarvis_erp_goofish_add_index(
|
||||
IN p_table VARCHAR(64),
|
||||
IN p_index VARCHAR(64),
|
||||
IN p_columns VARCHAR(200)
|
||||
)
|
||||
BEGIN
|
||||
DECLARE idx_exists INT DEFAULT 0;
|
||||
SELECT COUNT(*) INTO idx_exists
|
||||
FROM information_schema.statistics
|
||||
WHERE TABLE_SCHEMA = @schema
|
||||
AND TABLE_NAME = p_table
|
||||
AND INDEX_NAME = p_index;
|
||||
IF idx_exists = 0 THEN
|
||||
SET @stmt := CONCAT('ALTER TABLE `', p_table, '` ADD INDEX `', p_index, '` (', p_columns, ')');
|
||||
PREPARE ps FROM @stmt;
|
||||
EXECUTE ps;
|
||||
DEALLOCATE PREPARE ps;
|
||||
END IF;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 3) erp_goofish_order:补齐缺列(与当前 Java 实体 / erp_goofish_init.sql 一致)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'seller_id', 'bigint(20) NULL COMMENT ''商家ID''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'user_name', 'varchar(128) NULL COMMENT ''闲鱼会员名''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'order_type', 'int(11) NULL COMMENT ''订单类型''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'order_status', 'int(11) NULL COMMENT ''订单状态(推送/列表)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'refund_status', 'int(11) NULL COMMENT ''退款状态''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'modify_time', 'bigint(20) NULL COMMENT ''订单更新时间(秒)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'product_id', 'bigint(20) NULL COMMENT ''管家商品ID''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'item_id', 'bigint(20) NULL COMMENT ''闲鱼商品ID''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'goods_title', 'varchar(512) NULL COMMENT ''商品标题(详情 goods.title)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'goods_image_url', 'varchar(1024) NULL COMMENT ''商品主图 URL(goods.images 首图)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'buyer_nick', 'varchar(256) NULL COMMENT ''买家昵称(详情 buyer_nick)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'pay_amount', 'bigint(20) NULL COMMENT ''实付金额(分) pay_amount''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'detail_waybill_no', 'varchar(128) NULL COMMENT ''闲管家详情回传运单号 waybill_no''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'detail_express_code', 'varchar(64) NULL COMMENT ''详情快递编码 express_code''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'detail_express_name', 'varchar(128) NULL COMMENT ''详情快递名称 express_name''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'receiver_name', 'varchar(128) NULL COMMENT ''收货人(详情有则落库)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'receiver_mobile', 'varchar(64) NULL COMMENT ''收货手机''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'receiver_address', 'varchar(1000) NULL COMMENT ''收货详细地址(address)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'receiver_region', 'varchar(256) NULL COMMENT ''省市区街道等拼接展示''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'recv_prov_name', 'varchar(64) NULL COMMENT ''prov_name''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'recv_city_name', 'varchar(64) NULL COMMENT ''city_name''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'recv_area_name', 'varchar(64) NULL COMMENT ''area_name''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'recv_town_name', 'varchar(128) NULL COMMENT ''town_name''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'detail_json', 'longtext NULL COMMENT ''订单详情接口全量 JSON''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'last_notify_json', 'longtext NULL COMMENT ''最近一次推送原文 JSON''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'jd_order_id', 'bigint(20) NULL COMMENT ''关联 jd_order.id(第三方单号=闲鱼单号)''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'local_waybill_no', 'varchar(128) NULL COMMENT ''本地物流扫描得到的运单号''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'ship_status', 'tinyint(4) NOT NULL DEFAULT 0 COMMENT ''0未发货 1已调用发货成功 2发货失败''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'ship_error', 'varchar(500) NULL COMMENT ''发货失败原因''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'ship_time', 'datetime NULL COMMENT ''发货调用成功时间''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'ship_express_code', 'varchar(64) NULL COMMENT ''实际发货使用的快递编码''');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'create_time', 'datetime NULL');
|
||||
CALL jarvis_erp_goofish_add_column('erp_goofish_order', 'update_time', 'datetime NULL');
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 4) 索引(手工建表可能缺)
|
||||
-- -----------------------------------------------------------------------------
|
||||
CALL jarvis_erp_goofish_add_index('erp_goofish_order', 'idx_jd_order', '`jd_order_id`');
|
||||
CALL jarvis_erp_goofish_add_index('erp_goofish_order', 'idx_order_status', '`order_status`');
|
||||
CALL jarvis_erp_goofish_add_index('erp_goofish_order', 'idx_modify_time', '`modify_time`');
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5) 唯一键 uk_app_order
|
||||
-- 若已存在同名约束则跳过;若 (app_key,order_no) 有重复行会报错,需先清洗数据。
|
||||
-- 若已通过其它名称建了 (app_key,order_no) 唯一索引,请勿重复执行本节(可能报 Duplicate key)。
|
||||
-- -----------------------------------------------------------------------------
|
||||
SET @uk := (
|
||||
SELECT COUNT(*) FROM information_schema.table_constraints
|
||||
WHERE table_schema = @schema AND table_name = 'erp_goofish_order'
|
||||
AND constraint_name = 'uk_app_order' AND constraint_type = 'UNIQUE'
|
||||
);
|
||||
SET @sql_uk := IF(@uk = 0,
|
||||
'ALTER TABLE erp_goofish_order ADD UNIQUE KEY uk_app_order (app_key, order_no)',
|
||||
'SELECT ''uk_app_order 已存在,跳过'' AS note');
|
||||
PREPARE puk FROM @sql_uk;
|
||||
EXECUTE puk;
|
||||
DEALLOCATE PREPARE puk;
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 5.5) 订单变更事件日志表
|
||||
-- -----------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS erp_goofish_order_event_log (
|
||||
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
|
||||
order_id bigint(20) NOT NULL COMMENT 'erp_goofish_order.id',
|
||||
app_key varchar(64) DEFAULT NULL,
|
||||
order_no varchar(64) NOT NULL,
|
||||
event_type varchar(32) NOT NULL COMMENT 'ORDER_SYNC/LOGISTICS_SYNC/SHIP',
|
||||
source varchar(64) NULL COMMENT 'NOTIFY/LIST/DETAIL_REFRESH 等',
|
||||
message varchar(1024) NOT NULL,
|
||||
create_time datetime DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_goofish_evt_order (order_id),
|
||||
KEY idx_goofish_evt_time (create_time)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲管家订单状态/物流/发货变更日志';
|
||||
|
||||
-- -----------------------------------------------------------------------------
|
||||
-- 6) 清理存储过程
|
||||
-- -----------------------------------------------------------------------------
|
||||
DROP PROCEDURE IF EXISTS jarvis_erp_goofish_add_column;
|
||||
DROP PROCEDURE IF EXISTS jarvis_erp_goofish_add_index;
|
||||
|
||||
SELECT 'erp_goofish_upgrade.sql 执行结束' AS message;
|
||||
@@ -122,6 +122,8 @@ public class SecurityConfig
|
||||
.antMatchers("/kdocs-callback").permitAll()
|
||||
// 旧 WPS 回调路径:重定向到新路径,便于后台仍登记旧 URL 时可用
|
||||
.antMatchers("/wps365-callback").permitAll()
|
||||
// 企微消息经 wxSend 转发的桥接(依赖请求头共享密钥)
|
||||
.antMatchers("/jarvis/wecom/inbound").permitAll()
|
||||
// 静态资源,可匿名访问
|
||||
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
|
||||
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
|
||||
|
||||
@@ -28,6 +28,11 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 授权列表查询请求(示例类)
|
||||
*/
|
||||
public class AuthorizeListQueryRequest extends ERPRequestBase {
|
||||
public AuthorizeListQueryRequest(ERPAccount erpAccount) {
|
||||
public AuthorizeListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/user/authorize/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import lombok.Getter;
|
||||
* @description:ERP账户枚举类
|
||||
*/
|
||||
@Getter
|
||||
public enum ERPAccount {
|
||||
public enum ERPAccount implements IERPAccount {
|
||||
// 胡歌1016208368633221
|
||||
ACCOUNT_HUGE("1016208368633221", "waLiRMgFcixLbcLjUSSwo370Hp1nBcBu","余生请多关照66","海尔胡歌"),
|
||||
// 刘强东anotherApiKey
|
||||
|
||||
@@ -18,12 +18,12 @@ public abstract class ERPRequestBase {
|
||||
|
||||
protected String url;
|
||||
protected String sign;
|
||||
protected ERPAccount erpAccount;
|
||||
protected IERPAccount erpAccount;
|
||||
@Setter
|
||||
protected JSONObject requestBody;
|
||||
protected long timestamp; // 统一时间戳字段
|
||||
|
||||
public ERPRequestBase(String url, ERPAccount erpAccount) {
|
||||
public ERPRequestBase(String url, IERPAccount erpAccount) {
|
||||
this.url = url;
|
||||
this.erpAccount = erpAccount;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
package com.ruoyi.erp.request;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
/**
|
||||
* 查询快递公司请求
|
||||
*
|
||||
* 对应接口:POST /api/open/express/companies
|
||||
* <p>
|
||||
* 对应接口:POST /api/open/express/companies<br>
|
||||
* 闲管家规定:无业务参数时应对 <strong>空 JSON 对象</strong> 做 {@code md5("{}")},且 POST 原文必须与之一致;
|
||||
* Apifox 调试时请勿留空 body,须填 {@code {}},否则签名与平台不一致会拉不到数据。
|
||||
*/
|
||||
public class ExpressCompaniesQueryRequest extends ERPRequestBase {
|
||||
public ExpressCompaniesQueryRequest(ERPAccount erpAccount) {
|
||||
public ExpressCompaniesQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/express/companies", erpAccount);
|
||||
this.requestBody = new JSONObject();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.erp.request;
|
||||
|
||||
/**
|
||||
* 闲管家开放平台凭证({@link ERPAccount} 或库表配置行均可实现本接口)
|
||||
*/
|
||||
public interface IERPAccount {
|
||||
|
||||
String getApiKey();
|
||||
|
||||
String getApiKeySecret();
|
||||
|
||||
/** 闲鱼会员名(授权维度展示用,可为空) */
|
||||
String getXyName();
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/order/detail
|
||||
*/
|
||||
public class OrderDetailQueryRequest extends ERPRequestBase {
|
||||
public OrderDetailQueryRequest(ERPAccount erpAccount) {
|
||||
public OrderDetailQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/order/detail", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/order/kam/list
|
||||
*/
|
||||
public class OrderKamListQueryRequest extends ERPRequestBase {
|
||||
public OrderKamListQueryRequest(ERPAccount erpAccount) {
|
||||
public OrderKamListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/order/kam/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/order/list
|
||||
*/
|
||||
public class OrderListQueryRequest extends ERPRequestBase {
|
||||
public OrderListQueryRequest(ERPAccount erpAccount) {
|
||||
public OrderListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/order/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/order/modify/price
|
||||
*/
|
||||
public class OrderModifyPriceRequest extends ERPRequestBase {
|
||||
public OrderModifyPriceRequest(ERPAccount erpAccount) {
|
||||
public OrderModifyPriceRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/order/modify/price", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/order/ship
|
||||
*/
|
||||
public class OrderShipRequest extends ERPRequestBase {
|
||||
public OrderShipRequest(ERPAccount erpAccount) {
|
||||
public OrderShipRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/order/ship", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.util.List;
|
||||
* 限制:每批次最多50个商品
|
||||
*/
|
||||
public class ProductBatchCreateRequest extends ERPRequestBase {
|
||||
public ProductBatchCreateRequest(ERPAccount erpAccount) {
|
||||
public ProductBatchCreateRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/batchCreate", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* - flash_sale_type(选填):闲鱼特卖类型
|
||||
*/
|
||||
public class ProductCategoryListQueryRequest extends ERPRequestBase {
|
||||
public ProductCategoryListQueryRequest(ERPAccount erpAccount) {
|
||||
public ProductCategoryListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/category/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ProductCreateRequest extends ERPRequestBase {
|
||||
private final JSONArray publishShop = new JSONArray();
|
||||
private final JSONArray skuItems = new JSONArray();
|
||||
|
||||
public ProductCreateRequest(ERPAccount erpAccount) {
|
||||
public ProductCreateRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/create", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/product/delete
|
||||
*/
|
||||
public class ProductDeleteRequest extends ERPRequestBase {
|
||||
public ProductDeleteRequest(ERPAccount erpAccount) {
|
||||
public ProductDeleteRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/delete", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* - product_id(必填):管家商品ID
|
||||
*/
|
||||
public class ProductDetailQueryRequest extends ERPRequestBase {
|
||||
public ProductDetailQueryRequest(ERPAccount erpAccount) {
|
||||
public ProductDetailQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/detail", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/product/downShelf
|
||||
*/
|
||||
public class ProductDownShelfRequest extends ERPRequestBase {
|
||||
public ProductDownShelfRequest(ERPAccount erpAccount) {
|
||||
public ProductDownShelfRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/downShelf", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ public class ProductEditRequest extends ERPRequestBase {
|
||||
private final JSONArray publishShop = new JSONArray();
|
||||
private final JSONArray skuItems = new JSONArray();
|
||||
|
||||
public ProductEditRequest(ERPAccount erpAccount) {
|
||||
public ProductEditRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/edit", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/product/edit/stock
|
||||
*/
|
||||
public class ProductEditStockRequest extends ERPRequestBase {
|
||||
public ProductEditStockRequest(ERPAccount erpAccount) {
|
||||
public ProductEditStockRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/edit/stock", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package com.ruoyi.erp.request;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
public class ProductListQueryRequest extends ERPRequestBase {
|
||||
public ProductListQueryRequest(ERPAccount erpAccount) {
|
||||
public ProductListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* - sub_property_id(选填):属性值ID(用于二级属性查询)
|
||||
*/
|
||||
public class ProductPropertyListQueryRequest extends ERPRequestBase {
|
||||
public ProductPropertyListQueryRequest(ERPAccount erpAccount) {
|
||||
public ProductPropertyListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/pv/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import com.alibaba.fastjson2.JSONObject;
|
||||
* 对应接口:POST /api/open/product/publish
|
||||
*/
|
||||
public class ProductPublishRequest extends ERPRequestBase {
|
||||
public ProductPublishRequest(ERPAccount erpAccount) {
|
||||
public ProductPublishRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/publish", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.util.Collection;
|
||||
* - product_id(必填):管家商品ID数组,最多100个
|
||||
*/
|
||||
public class ProductSkuListQueryRequest extends ERPRequestBase {
|
||||
public ProductSkuListQueryRequest(ERPAccount erpAccount) {
|
||||
public ProductSkuListQueryRequest(IERPAccount erpAccount) {
|
||||
super("https://open.goofish.pro/api/open/product/sku/list", erpAccount);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.jarvis.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
@Configuration
|
||||
public class GoofishAsyncConfig {
|
||||
|
||||
@Bean("goofishTaskExecutor")
|
||||
public Executor goofishTaskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
// 仅用于未配置 RocketMQ 时 HTTP 回调路径的 @Async;过小易在拉单/回调并发时排队拖慢企微通知
|
||||
executor.setCorePoolSize(4);
|
||||
executor.setMaxPoolSize(16);
|
||||
executor.setQueueCapacity(500);
|
||||
executor.setThreadNamePrefix("goofish-");
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ruoyi.jarvis.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 闲管家订单:MQ 主题、定时拉单与自动发货调度
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "jarvis.goofish-order")
|
||||
public class JarvisGoofishProperties {
|
||||
|
||||
/** RocketMQ Topic(需配置 rocketmq.name-server 后生效) */
|
||||
private String mqTopic = "jarvis-goofish-erp-order";
|
||||
|
||||
private String consumerGroup = "jarvis-goofish-order-consumer";
|
||||
|
||||
/** 回溯拉单小时数(定时/增量) */
|
||||
private int pullLookbackHours = 72;
|
||||
|
||||
/** 拉单定时 cron */
|
||||
private String pullCron = "0 * * * * ?";
|
||||
|
||||
/** 同步运单 + 自动发货 cron */
|
||||
private String autoShipCron = "0 2/10 * * * ?";
|
||||
|
||||
/**
|
||||
* 订单列表 page_size(开放平台最大 100)
|
||||
*/
|
||||
private int pullPageSize = 100;
|
||||
|
||||
/**
|
||||
* 单次拉单每授权最大页数(开放平台 page_no 最大 100;page_no×page_size 勿超过 10000)
|
||||
*/
|
||||
private int pullMaxPagesPerShop = 100;
|
||||
|
||||
/**
|
||||
* 全量/长历史拉单时,按 update_time 切片的窗口长度(秒),避免单窗内订单量过大触发平台限制
|
||||
*/
|
||||
private int pullTimeChunkSeconds = 604800;
|
||||
|
||||
/**
|
||||
* 订单列表单次请求中 update_time 区间最大跨度(秒)。开放平台返回「只能查询时间范围6个月内的数据」时须≤此值;默认约 180 天留余量
|
||||
*/
|
||||
private int pullMaxUpdateTimeRangeSeconds = 15552000;
|
||||
|
||||
/**
|
||||
* 全量拉单从「当前时间」往前推多少天作为起点(仅 full 接口;可自行改大)
|
||||
*/
|
||||
private int pullFullHistoryDays = 1095;
|
||||
|
||||
private int autoShipBatchSize = 20;
|
||||
|
||||
/**
|
||||
* 允许触发闲鱼开放平台「发货」的本地 order_status(逗号分隔,与推送/列表一致)。默认 12 通常表示待发货。
|
||||
*/
|
||||
private String autoShipOrderStatuses = "12";
|
||||
|
||||
/**
|
||||
* 为 true 时定时/增量列表拉单仅按 {@link #autoShipOrderStatuses} 过滤(减少调用量,但会漏掉其它状态)。
|
||||
* 默认 false:按时间窗拉全状态,与本地 upsert 对齐,推送仍可用于更低延迟。
|
||||
*/
|
||||
private boolean pullListOnlyAutoShipStatuses = false;
|
||||
|
||||
/**
|
||||
* 未在 erp_open_config 配置 express_code 时,自动发货使用的默认快递公司编码(官方列表中日日顺多为 rrs)。
|
||||
*/
|
||||
private String defaultShipExpressCode = "rrs";
|
||||
|
||||
/** 与 defaultShipExpressCode 配套的展示名称 */
|
||||
private String defaultShipExpressName = "日日顺";
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 闲管家 ERP 订单(推送 + 拉单 + 详情全量)
|
||||
*/
|
||||
@Data
|
||||
public class ErpGoofishOrder {
|
||||
|
||||
private Long id;
|
||||
private String appKey;
|
||||
private Long sellerId;
|
||||
private String userName;
|
||||
private String orderNo;
|
||||
private Integer orderType;
|
||||
private Integer orderStatus;
|
||||
private Integer refundStatus;
|
||||
private Long modifyTime;
|
||||
private Long productId;
|
||||
private Long itemId;
|
||||
/** 详情 goods.title */
|
||||
private String goodsTitle;
|
||||
/** goods.images 首张或其它单图字段 */
|
||||
private String goodsImageUrl;
|
||||
private String buyerNick;
|
||||
/** 开放平台 pay_amount,单位:分 */
|
||||
private Long payAmount;
|
||||
/** 闲管家详情 waybill_no */
|
||||
private String detailWaybillNo;
|
||||
private String detailExpressCode;
|
||||
private String detailExpressName;
|
||||
private String receiverName;
|
||||
private String receiverMobile;
|
||||
private String receiverAddress;
|
||||
/** 省市区拼接 */
|
||||
private String receiverRegion;
|
||||
/** 待发货等状态下开放平台返回的分级地址(与 prov_name/city_name/area_name/town_name 一致) */
|
||||
private String recvProvName;
|
||||
private String recvCityName;
|
||||
private String recvAreaName;
|
||||
private String recvTownName;
|
||||
private String detailJson;
|
||||
private String lastNotifyJson;
|
||||
private Long jdOrderId;
|
||||
private String localWaybillNo;
|
||||
/** 0未发货 1成功 2失败 */
|
||||
private Integer shipStatus;
|
||||
private String shipError;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date shipTime;
|
||||
private String shipExpressCode;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date updateTime;
|
||||
|
||||
/** 联查:第三方单号(jd_order) */
|
||||
private String jdThirdPartyOrderNo;
|
||||
/** 联查:内部备注单号 */
|
||||
private String jdRemark;
|
||||
/** 联查:本地京东单收件地址 jd_order.address(闲鱼详情常不返回明文地址) */
|
||||
private String jdAddress;
|
||||
|
||||
// --------- 以下为列表查询扩展条件(不参与 insert/update) ---------
|
||||
/** 运单关键字:命中详情运单号或本地运单号(模糊) */
|
||||
private String waybillKeyword;
|
||||
/** 开放平台 modify_time 下限(Unix 秒,含边界) */
|
||||
private Long modifyTimeBegin;
|
||||
/** 开放平台 modify_time 上限(Unix 秒,含边界) */
|
||||
private Long modifyTimeEnd;
|
||||
/** 是否已关联京东单:1 已关联 jd_order_id 非空;0 未关联;null 不限 */
|
||||
private Integer jdLinkFilter;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 闲管家订单变更日志:状态刷新、物流变动、发货结果等
|
||||
*/
|
||||
@Data
|
||||
public class ErpGoofishOrderEventLog {
|
||||
|
||||
private Long id;
|
||||
/** erp_goofish_order.id */
|
||||
private Long orderId;
|
||||
private String appKey;
|
||||
private String orderNo;
|
||||
/** ORDER_SYNC / LOGISTICS_SYNC / SHIP */
|
||||
private String eventType;
|
||||
/** NOTIFY、LIST、DETAIL_REFRESH、JD_LOGISTICS_PUSH、REDIS_WAYBILL、AUTO_SHIP 等 */
|
||||
private String source;
|
||||
private String message;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 闲管家订单变更日志(全表检索,用于排查)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ErpGoofishOrderEventLogQuery extends BaseEntity {
|
||||
|
||||
private Long orderId;
|
||||
private String appKey;
|
||||
private String orderNo;
|
||||
private String eventType;
|
||||
private String source;
|
||||
/** 模糊匹配 message */
|
||||
private String messageKeyword;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import com.ruoyi.erp.request.IERPAccount;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 闲管家开放平台应用配置(配置中心)
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ErpOpenConfig extends BaseEntity implements IERPAccount {
|
||||
|
||||
private Long id;
|
||||
private String appKey;
|
||||
private String appSecret;
|
||||
private String xyUserName;
|
||||
/** 发货:快递公司编码(如日日顺,需与开放平台一致) */
|
||||
private String expressCode;
|
||||
private String expressName;
|
||||
private String status;
|
||||
private Integer orderNum;
|
||||
|
||||
@Override
|
||||
public String getApiKey() {
|
||||
return appKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getApiKeySecret() {
|
||||
return appSecret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getXyName() {
|
||||
return xyUserName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 闲鱼商品导出专用行(字段偏多,便于给 AI / 离线分析使用)
|
||||
*/
|
||||
public class ErpProductExportRow implements Serializable
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Excel(name = "导出批次时间", width = 22, sort = 1)
|
||||
private String exportBatchAt;
|
||||
|
||||
@Excel(name = "本表主键ID", sort = 2)
|
||||
private Long id;
|
||||
|
||||
@Excel(name = "管家商品ID", sort = 3)
|
||||
private Long productId;
|
||||
|
||||
@Excel(name = "管家商品ID文本", width = 22, sort = 4)
|
||||
private String productIdText;
|
||||
|
||||
@Excel(name = "商品标题", width = 45, sort = 5)
|
||||
private String title;
|
||||
|
||||
@Excel(name = "主图URL", width = 55, sort = 6)
|
||||
private String mainImage;
|
||||
|
||||
@Excel(name = "价格_分_原始整数", sort = 7)
|
||||
private Long priceFen;
|
||||
|
||||
@Excel(name = "价格_元_可读", sort = 8)
|
||||
private String priceYuan;
|
||||
|
||||
@Excel(name = "库存", sort = 9)
|
||||
private Integer stock;
|
||||
|
||||
@Excel(name = "商品状态_码", sort = 10)
|
||||
private Integer productStatusCode;
|
||||
|
||||
@Excel(name = "商品状态_说明", width = 14, sort = 11)
|
||||
private String productStatusLabel;
|
||||
|
||||
@Excel(name = "销售状态_码", sort = 12)
|
||||
private Integer saleStatusCode;
|
||||
|
||||
@Excel(name = "闲鱼会员名", width = 18, sort = 13)
|
||||
private String userName;
|
||||
|
||||
@Excel(name = "ERP应用appid", width = 22, sort = 14)
|
||||
private String appid;
|
||||
|
||||
@Excel(name = "商品链接", width = 55, sort = 15)
|
||||
private String productUrl;
|
||||
|
||||
@Excel(name = "上架时间_unix秒", width = 18, sort = 16)
|
||||
private Long onlineTimeUnix;
|
||||
|
||||
@Excel(name = "上架时间_可读", width = 22, sort = 17)
|
||||
private String onlineTimeReadable;
|
||||
|
||||
@Excel(name = "下架时间_unix秒", width = 18, sort = 18)
|
||||
private Long offlineTimeUnix;
|
||||
|
||||
@Excel(name = "下架时间_可读", width = 22, sort = 19)
|
||||
private String offlineTimeReadable;
|
||||
|
||||
@Excel(name = "售出时间_unix秒", width = 18, sort = 20)
|
||||
private Long soldTimeUnix;
|
||||
|
||||
@Excel(name = "售出时间_可读", width = 22, sort = 21)
|
||||
private String soldTimeReadable;
|
||||
|
||||
@Excel(name = "闲鱼创建_unix秒", width = 18, sort = 22)
|
||||
private Long createTimeXyUnix;
|
||||
|
||||
@Excel(name = "闲鱼创建_可读", width = 22, sort = 23)
|
||||
private String createTimeXyReadable;
|
||||
|
||||
@Excel(name = "闲鱼更新_unix秒", width = 18, sort = 24)
|
||||
private Long updateTimeXyUnix;
|
||||
|
||||
@Excel(name = "闲鱼更新_可读", width = 22, sort = 25)
|
||||
private String updateTimeXyReadable;
|
||||
|
||||
@Excel(name = "备注_本表", width = 30, sort = 26)
|
||||
private String remark;
|
||||
|
||||
@Excel(name = "库创建时间", width = 22, dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 27)
|
||||
private Date dbCreateTime;
|
||||
|
||||
@Excel(name = "库更新时间", width = 22, dateFormat = "yyyy-MM-dd HH:mm:ss", sort = 28)
|
||||
private Date dbUpdateTime;
|
||||
|
||||
public static List<ErpProductExportRow> fromList(List<ErpProduct> list, String exportBatchAt)
|
||||
{
|
||||
List<ErpProductExportRow> rows = new ArrayList<>(list.size());
|
||||
for (ErpProduct p : list)
|
||||
{
|
||||
rows.add(from(p, exportBatchAt));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
public static ErpProductExportRow from(ErpProduct p, String exportBatchAt)
|
||||
{
|
||||
ErpProductExportRow r = new ErpProductExportRow();
|
||||
r.setExportBatchAt(exportBatchAt != null ? exportBatchAt : "");
|
||||
if (p == null)
|
||||
{
|
||||
return r;
|
||||
}
|
||||
r.setId(p.getId());
|
||||
r.setProductId(p.getProductId());
|
||||
r.setProductIdText(p.getProductId() != null ? String.valueOf(p.getProductId()) : "");
|
||||
r.setTitle(p.getTitle());
|
||||
r.setMainImage(p.getMainImage());
|
||||
r.setPriceFen(p.getPrice());
|
||||
if (p.getPrice() != null)
|
||||
{
|
||||
r.setPriceYuan(BigDecimal.valueOf(p.getPrice()).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP).toPlainString());
|
||||
}
|
||||
else
|
||||
{
|
||||
r.setPriceYuan("");
|
||||
}
|
||||
r.setStock(p.getStock());
|
||||
r.setProductStatusCode(p.getProductStatus());
|
||||
r.setProductStatusLabel(productStatusLabel(p.getProductStatus()));
|
||||
r.setSaleStatusCode(p.getSaleStatus());
|
||||
r.setUserName(p.getUserName());
|
||||
r.setAppid(p.getAppid());
|
||||
r.setProductUrl(p.getProductUrl());
|
||||
|
||||
r.setOnlineTimeUnix(p.getOnlineTime());
|
||||
r.setOnlineTimeReadable(formatUnixSeconds(p.getOnlineTime()));
|
||||
r.setOfflineTimeUnix(p.getOfflineTime());
|
||||
r.setOfflineTimeReadable(formatUnixSeconds(p.getOfflineTime()));
|
||||
r.setSoldTimeUnix(p.getSoldTime());
|
||||
r.setSoldTimeReadable(formatUnixSeconds(p.getSoldTime()));
|
||||
r.setCreateTimeXyUnix(p.getCreateTimeXy());
|
||||
r.setCreateTimeXyReadable(formatUnixSeconds(p.getCreateTimeXy()));
|
||||
r.setUpdateTimeXyUnix(p.getUpdateTimeXy());
|
||||
r.setUpdateTimeXyReadable(formatUnixSeconds(p.getUpdateTimeXy()));
|
||||
|
||||
r.setRemark(p.getRemark());
|
||||
r.setDbCreateTime(p.getCreateTime());
|
||||
r.setDbUpdateTime(p.getUpdateTime());
|
||||
return r;
|
||||
}
|
||||
|
||||
private static String formatUnixSeconds(Long unixSeconds)
|
||||
{
|
||||
if (unixSeconds == null || unixSeconds <= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
return DateUtils.parseDateToStr(DateUtils.YYYY_MM_DD_HH_MM_SS, new Date(unixSeconds * 1000L));
|
||||
}
|
||||
|
||||
private static String productStatusLabel(Integer status)
|
||||
{
|
||||
if (status == null)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
switch (status)
|
||||
{
|
||||
case -1:
|
||||
return "删除";
|
||||
case 10:
|
||||
return "其它(10)";
|
||||
case 21:
|
||||
return "待发布";
|
||||
case 22:
|
||||
return "销售中";
|
||||
case 23:
|
||||
return "已售罄";
|
||||
case 31:
|
||||
return "手动下架";
|
||||
case 33:
|
||||
return "售出下架";
|
||||
case 36:
|
||||
return "自动下架";
|
||||
default:
|
||||
return "未知(" + status + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public String getExportBatchAt()
|
||||
{
|
||||
return exportBatchAt;
|
||||
}
|
||||
|
||||
public void setExportBatchAt(String exportBatchAt)
|
||||
{
|
||||
this.exportBatchAt = exportBatchAt;
|
||||
}
|
||||
|
||||
public Long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getProductId()
|
||||
{
|
||||
return productId;
|
||||
}
|
||||
|
||||
public void setProductId(Long productId)
|
||||
{
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
public String getProductIdText()
|
||||
{
|
||||
return productIdText;
|
||||
}
|
||||
|
||||
public void setProductIdText(String productIdText)
|
||||
{
|
||||
this.productIdText = productIdText;
|
||||
}
|
||||
|
||||
public String getTitle()
|
||||
{
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title)
|
||||
{
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getMainImage()
|
||||
{
|
||||
return mainImage;
|
||||
}
|
||||
|
||||
public void setMainImage(String mainImage)
|
||||
{
|
||||
this.mainImage = mainImage;
|
||||
}
|
||||
|
||||
public Long getPriceFen()
|
||||
{
|
||||
return priceFen;
|
||||
}
|
||||
|
||||
public void setPriceFen(Long priceFen)
|
||||
{
|
||||
this.priceFen = priceFen;
|
||||
}
|
||||
|
||||
public String getPriceYuan()
|
||||
{
|
||||
return priceYuan;
|
||||
}
|
||||
|
||||
public void setPriceYuan(String priceYuan)
|
||||
{
|
||||
this.priceYuan = priceYuan;
|
||||
}
|
||||
|
||||
public Integer getStock()
|
||||
{
|
||||
return stock;
|
||||
}
|
||||
|
||||
public void setStock(Integer stock)
|
||||
{
|
||||
this.stock = stock;
|
||||
}
|
||||
|
||||
public Integer getProductStatusCode()
|
||||
{
|
||||
return productStatusCode;
|
||||
}
|
||||
|
||||
public void setProductStatusCode(Integer productStatusCode)
|
||||
{
|
||||
this.productStatusCode = productStatusCode;
|
||||
}
|
||||
|
||||
public String getProductStatusLabel()
|
||||
{
|
||||
return productStatusLabel;
|
||||
}
|
||||
|
||||
public void setProductStatusLabel(String productStatusLabel)
|
||||
{
|
||||
this.productStatusLabel = productStatusLabel;
|
||||
}
|
||||
|
||||
public Integer getSaleStatusCode()
|
||||
{
|
||||
return saleStatusCode;
|
||||
}
|
||||
|
||||
public void setSaleStatusCode(Integer saleStatusCode)
|
||||
{
|
||||
this.saleStatusCode = saleStatusCode;
|
||||
}
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getAppid()
|
||||
{
|
||||
return appid;
|
||||
}
|
||||
|
||||
public void setAppid(String appid)
|
||||
{
|
||||
this.appid = appid;
|
||||
}
|
||||
|
||||
public String getProductUrl()
|
||||
{
|
||||
return productUrl;
|
||||
}
|
||||
|
||||
public void setProductUrl(String productUrl)
|
||||
{
|
||||
this.productUrl = productUrl;
|
||||
}
|
||||
|
||||
public Long getOnlineTimeUnix()
|
||||
{
|
||||
return onlineTimeUnix;
|
||||
}
|
||||
|
||||
public void setOnlineTimeUnix(Long onlineTimeUnix)
|
||||
{
|
||||
this.onlineTimeUnix = onlineTimeUnix;
|
||||
}
|
||||
|
||||
public String getOnlineTimeReadable()
|
||||
{
|
||||
return onlineTimeReadable;
|
||||
}
|
||||
|
||||
public void setOnlineTimeReadable(String onlineTimeReadable)
|
||||
{
|
||||
this.onlineTimeReadable = onlineTimeReadable;
|
||||
}
|
||||
|
||||
public Long getOfflineTimeUnix()
|
||||
{
|
||||
return offlineTimeUnix;
|
||||
}
|
||||
|
||||
public void setOfflineTimeUnix(Long offlineTimeUnix)
|
||||
{
|
||||
this.offlineTimeUnix = offlineTimeUnix;
|
||||
}
|
||||
|
||||
public String getOfflineTimeReadable()
|
||||
{
|
||||
return offlineTimeReadable;
|
||||
}
|
||||
|
||||
public void setOfflineTimeReadable(String offlineTimeReadable)
|
||||
{
|
||||
this.offlineTimeReadable = offlineTimeReadable;
|
||||
}
|
||||
|
||||
public Long getSoldTimeUnix()
|
||||
{
|
||||
return soldTimeUnix;
|
||||
}
|
||||
|
||||
public void setSoldTimeUnix(Long soldTimeUnix)
|
||||
{
|
||||
this.soldTimeUnix = soldTimeUnix;
|
||||
}
|
||||
|
||||
public String getSoldTimeReadable()
|
||||
{
|
||||
return soldTimeReadable;
|
||||
}
|
||||
|
||||
public void setSoldTimeReadable(String soldTimeReadable)
|
||||
{
|
||||
this.soldTimeReadable = soldTimeReadable;
|
||||
}
|
||||
|
||||
public Long getCreateTimeXyUnix()
|
||||
{
|
||||
return createTimeXyUnix;
|
||||
}
|
||||
|
||||
public void setCreateTimeXyUnix(Long createTimeXyUnix)
|
||||
{
|
||||
this.createTimeXyUnix = createTimeXyUnix;
|
||||
}
|
||||
|
||||
public String getCreateTimeXyReadable()
|
||||
{
|
||||
return createTimeXyReadable;
|
||||
}
|
||||
|
||||
public void setCreateTimeXyReadable(String createTimeXyReadable)
|
||||
{
|
||||
this.createTimeXyReadable = createTimeXyReadable;
|
||||
}
|
||||
|
||||
public Long getUpdateTimeXyUnix()
|
||||
{
|
||||
return updateTimeXyUnix;
|
||||
}
|
||||
|
||||
public void setUpdateTimeXyUnix(Long updateTimeXyUnix)
|
||||
{
|
||||
this.updateTimeXyUnix = updateTimeXyUnix;
|
||||
}
|
||||
|
||||
public String getUpdateTimeXyReadable()
|
||||
{
|
||||
return updateTimeXyReadable;
|
||||
}
|
||||
|
||||
public void setUpdateTimeXyReadable(String updateTimeXyReadable)
|
||||
{
|
||||
this.updateTimeXyReadable = updateTimeXyReadable;
|
||||
}
|
||||
|
||||
public String getRemark()
|
||||
{
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark)
|
||||
{
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public Date getDbCreateTime()
|
||||
{
|
||||
return dbCreateTime;
|
||||
}
|
||||
|
||||
public void setDbCreateTime(Date dbCreateTime)
|
||||
{
|
||||
this.dbCreateTime = dbCreateTime;
|
||||
}
|
||||
|
||||
public Date getDbUpdateTime()
|
||||
{
|
||||
return dbUpdateTime;
|
||||
}
|
||||
|
||||
public void setDbUpdateTime(Date dbUpdateTime)
|
||||
{
|
||||
this.dbUpdateTime = dbUpdateTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 后返/跟团返现 Excel 上传记录
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class GroupRebateExcelUpload extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String documentTitle;
|
||||
private String originalFilename;
|
||||
/** 若依资源路径,如 /profile/upload/group-rebate-excel/... */
|
||||
private String filePath;
|
||||
private Long fileSize;
|
||||
/** 1 解析失败或未处理 2 已成功解析并写订单 */
|
||||
private Integer importStatus;
|
||||
private Integer dataRows;
|
||||
private Integer updatedOrders;
|
||||
private Integer notFoundCount;
|
||||
private String resultDetailJson;
|
||||
}
|
||||
@@ -30,6 +30,10 @@ public class JDOrder extends BaseEntity {
|
||||
@Excel(name = "型号")
|
||||
private String modelNumber;
|
||||
|
||||
/** 列表筛选:型号不含此子串(对应 SQL NOT LIKE %值%),不入库 */
|
||||
@Transient
|
||||
private String modelNumberExclude;
|
||||
|
||||
/** 链接 */
|
||||
@Excel(name = "链接")
|
||||
private String link;
|
||||
@@ -145,6 +149,30 @@ public class JDOrder extends BaseEntity {
|
||||
@Excel(name = "评价日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date reviewPostedDate;
|
||||
|
||||
/**
|
||||
* 后返备注(多次导入跟团返现等 Excel 的记录,JSON 数组字符串)
|
||||
* @see com.ruoyi.jarvis.domain.dto.RebateRemarkItem
|
||||
*/
|
||||
private String rebateRemarkJson;
|
||||
|
||||
/** 后返备注中是否存在异常项(1是 0否),便于列表筛选 */
|
||||
private Integer rebateRemarkHasAbnormal;
|
||||
|
||||
/** 售价渠道:direct 直款,xianyu 闲鱼(仅 F 单使用) */
|
||||
private String sellingPriceType;
|
||||
|
||||
/** 售价(对客成交价,可手动改) */
|
||||
private Double sellingPrice;
|
||||
|
||||
/** 利润(可手动改;非 H-TF/F 一般为空) */
|
||||
private Double profit;
|
||||
|
||||
/** 售价是否手动锁定(1 是:不再按型号配置自动回填) */
|
||||
private Integer sellingPriceManual;
|
||||
|
||||
/** 利润是否手动锁定(1 是:保存时不再自动重算) */
|
||||
private Integer profitManual;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ public class ProductJdConfig extends BaseEntity
|
||||
/** 佣金(支付) - 支付给下单人的佣金 */
|
||||
private BigDecimal commissionPay;
|
||||
|
||||
/** 参考售价(直款渠道) */
|
||||
private BigDecimal sellingPriceDirect;
|
||||
|
||||
/** 参考售价(闲鱼渠道,订单侧仍会 ×0.984 计算实收) */
|
||||
private BigDecimal sellingPriceXianyu;
|
||||
|
||||
public ProductJdConfig() {
|
||||
}
|
||||
|
||||
@@ -76,6 +82,22 @@ public class ProductJdConfig extends BaseEntity
|
||||
this.commissionPay = commissionPay;
|
||||
}
|
||||
|
||||
public BigDecimal getSellingPriceDirect() {
|
||||
return sellingPriceDirect;
|
||||
}
|
||||
|
||||
public void setSellingPriceDirect(BigDecimal sellingPriceDirect) {
|
||||
this.sellingPriceDirect = sellingPriceDirect;
|
||||
}
|
||||
|
||||
public BigDecimal getSellingPriceXianyu() {
|
||||
return sellingPriceXianyu;
|
||||
}
|
||||
|
||||
public void setSellingPriceXianyu(BigDecimal sellingPriceXianyu) {
|
||||
this.sellingPriceXianyu = sellingPriceXianyu;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ProductJdConfig{" +
|
||||
@@ -84,6 +106,8 @@ public class ProductJdConfig extends BaseEntity
|
||||
", commission=" + commission +
|
||||
", commissionReceive=" + commissionReceive +
|
||||
", commissionPay=" + commissionPay +
|
||||
", sellingPriceDirect=" + sellingPriceDirect +
|
||||
", sellingPriceXianyu=" + sellingPriceXianyu +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public class TencentDocBatchPushRecord extends BaseEntity {
|
||||
/** 错误数量 */
|
||||
private Integer errorCount;
|
||||
|
||||
/** 状态:RUNNING-执行中,SUCCESS-成功,PARTIAL-部分成功,FAILED-失败 */
|
||||
/** 状态:RUNNING-执行中,SUCCESS-成功,PARTIAL-部分成功,FAILED-失败,INTERRUPTED-已中断(超时/未正常结束) */
|
||||
private String status;
|
||||
|
||||
/** 结果消息 */
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 企微 inbound 消息追踪 wecom_inbound_trace
|
||||
*/
|
||||
public class WeComInboundTrace extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
@Excel(name = "消息ID")
|
||||
private String msgId;
|
||||
|
||||
@Excel(name = "AgentID")
|
||||
private String agentId;
|
||||
|
||||
@Excel(name = "CorpId")
|
||||
private String corpId;
|
||||
|
||||
@Excel(name = "发送人UserID")
|
||||
private String fromUserName;
|
||||
|
||||
private String content;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "微信发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date wxMsgTime;
|
||||
|
||||
private String replyContent;
|
||||
|
||||
@Excel(name = "会话进行中", readConverterExp = "0=否,1=是")
|
||||
private Integer sessionActive;
|
||||
|
||||
@Excel(name = "会话场景")
|
||||
private String sessionScene;
|
||||
|
||||
@Excel(name = "会话步骤")
|
||||
private String sessionStep;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getMsgId() {
|
||||
return msgId;
|
||||
}
|
||||
|
||||
public void setMsgId(String msgId) {
|
||||
this.msgId = msgId;
|
||||
}
|
||||
|
||||
public String getAgentId() {
|
||||
return agentId;
|
||||
}
|
||||
|
||||
public void setAgentId(String agentId) {
|
||||
this.agentId = agentId;
|
||||
}
|
||||
|
||||
public String getCorpId() {
|
||||
return corpId;
|
||||
}
|
||||
|
||||
public void setCorpId(String corpId) {
|
||||
this.corpId = corpId;
|
||||
}
|
||||
|
||||
public String getFromUserName() {
|
||||
return fromUserName;
|
||||
}
|
||||
|
||||
public void setFromUserName(String fromUserName) {
|
||||
this.fromUserName = fromUserName;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public Date getWxMsgTime() {
|
||||
return wxMsgTime;
|
||||
}
|
||||
|
||||
public void setWxMsgTime(Date wxMsgTime) {
|
||||
this.wxMsgTime = wxMsgTime;
|
||||
}
|
||||
|
||||
public String getReplyContent() {
|
||||
return replyContent;
|
||||
}
|
||||
|
||||
public void setReplyContent(String replyContent) {
|
||||
this.replyContent = replyContent;
|
||||
}
|
||||
|
||||
public Integer getSessionActive() {
|
||||
return sessionActive;
|
||||
}
|
||||
|
||||
public void setSessionActive(Integer sessionActive) {
|
||||
this.sessionActive = sessionActive;
|
||||
}
|
||||
|
||||
public String getSessionScene() {
|
||||
return sessionScene;
|
||||
}
|
||||
|
||||
public void setSessionScene(String sessionScene) {
|
||||
this.sessionScene = sessionScene;
|
||||
}
|
||||
|
||||
public String getSessionStep() {
|
||||
return sessionStep;
|
||||
}
|
||||
|
||||
public void setSessionStep(String sessionStep) {
|
||||
this.sessionStep = sessionStep;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.ruoyi.jarvis.domain;
|
||||
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 企微分享链物流任务 wecom_share_link_logistics_job。
|
||||
* 状态含 CANCELLED:不再参与对账入队与队列扫描(订单取消等场景)。
|
||||
*/
|
||||
public class WeComShareLinkLogisticsJob extends BaseEntity {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
@Excel(name = "任务Key")
|
||||
private String jobKey;
|
||||
|
||||
@Excel(name = "发送人UserID")
|
||||
private String fromUserName;
|
||||
|
||||
private String trackingUrl;
|
||||
|
||||
/** 用户填写的备注(表字段 remark,避免与 BaseEntity.remark 混淆) */
|
||||
@Excel(name = "用户备注")
|
||||
private String userRemark;
|
||||
|
||||
@Excel(name = "推送接收人")
|
||||
private String touserPush;
|
||||
|
||||
@Excel(name = "状态")
|
||||
private String status;
|
||||
|
||||
@Excel(name = "运单号")
|
||||
private String waybillNo;
|
||||
|
||||
@Excel(name = "扫描次数")
|
||||
private Integer scanAttempts;
|
||||
|
||||
private String lastNote;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getJobKey() {
|
||||
return jobKey;
|
||||
}
|
||||
|
||||
public void setJobKey(String jobKey) {
|
||||
this.jobKey = jobKey;
|
||||
}
|
||||
|
||||
public String getFromUserName() {
|
||||
return fromUserName;
|
||||
}
|
||||
|
||||
public void setFromUserName(String fromUserName) {
|
||||
this.fromUserName = fromUserName;
|
||||
}
|
||||
|
||||
public String getTrackingUrl() {
|
||||
return trackingUrl;
|
||||
}
|
||||
|
||||
public void setTrackingUrl(String trackingUrl) {
|
||||
this.trackingUrl = trackingUrl;
|
||||
}
|
||||
|
||||
public String getUserRemark() {
|
||||
return userRemark;
|
||||
}
|
||||
|
||||
public void setUserRemark(String userRemark) {
|
||||
this.userRemark = userRemark;
|
||||
}
|
||||
|
||||
public String getTouserPush() {
|
||||
return touserPush;
|
||||
}
|
||||
|
||||
public void setTouserPush(String touserPush) {
|
||||
this.touserPush = touserPush;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getWaybillNo() {
|
||||
return waybillNo;
|
||||
}
|
||||
|
||||
public void setWaybillNo(String waybillNo) {
|
||||
this.waybillNo = waybillNo;
|
||||
}
|
||||
|
||||
public Integer getScanAttempts() {
|
||||
return scanAttempts;
|
||||
}
|
||||
|
||||
public void setScanAttempts(Integer scanAttempts) {
|
||||
this.scanAttempts = scanAttempts;
|
||||
}
|
||||
|
||||
public String getLastNote() {
|
||||
return lastNote;
|
||||
}
|
||||
|
||||
public void setLastNote(String lastNote) {
|
||||
this.lastNote = lastNote;
|
||||
}
|
||||
}
|
||||
@@ -23,9 +23,13 @@ public class KdocsTokenInfo implements Serializable {
|
||||
}
|
||||
|
||||
public boolean isExpired() {
|
||||
if (expiresIn == null || createTime == null) {
|
||||
if (accessToken == null || accessToken.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
// 部分 OAuth 响应不返回 expires_in;若按 null 判过期会导致前端误判「未授权」
|
||||
if (expiresIn == null || createTime == null) {
|
||||
return false;
|
||||
}
|
||||
long expireTime = createTime + (expiresIn * 1000L);
|
||||
return System.currentTimeMillis() >= (expireTime - 5 * 60 * 1000L);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.jarvis.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 快捷录单页:型号下拉项及该型号最近一次落库单的付款 / 后返
|
||||
*/
|
||||
@Data
|
||||
public class QuickRecordModelOption {
|
||||
|
||||
private String modelNumber;
|
||||
|
||||
/** 最近一次订单的下单付款金额 */
|
||||
private Double lastPaymentAmount;
|
||||
|
||||
/** 最近一次订单的后返金额 */
|
||||
private Double lastRebateAmount;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.jarvis.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 订单「后返备注」中单条记录(多次导入会追加多条)
|
||||
*/
|
||||
@Data
|
||||
public class RebateRemarkItem {
|
||||
|
||||
/** 文档标题,如:跟团+返现 260316 */
|
||||
private String documentTitle;
|
||||
|
||||
/** Excel「是否返现」列原文 */
|
||||
private String whetherRebate;
|
||||
|
||||
/** Excel 返现金额列(优先总共返现)展示值 */
|
||||
private String rebateAmount;
|
||||
|
||||
/** 写入时间戳(毫秒) */
|
||||
private Long uploadTime;
|
||||
|
||||
/** 对应 jd_group_rebate_excel_upload.id,用于删除上传记录时精确撤销本条备注 */
|
||||
private Long uploadRecordId;
|
||||
|
||||
/**
|
||||
* 是否异常:导入时「是否返现」列 trim 后不等于「已返现」则为 true,便于列表筛选与着色
|
||||
*/
|
||||
private Boolean abnormal;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ruoyi.jarvis.domain.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 企微侧多轮交互会话(与 Jarvis_java JDUtil 中 interaction_state + Redis 存 JSON 方式对齐)
|
||||
*/
|
||||
public class WeComChatSession {
|
||||
|
||||
public static final String SCENE_JD_LOGISTICS_SHARE = "JD_LOGISTICS_SHARE";
|
||||
public static final String STEP_WAIT_REMARK = "WAIT_REMARK";
|
||||
|
||||
private static final DateTimeFormatter FMT = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||
|
||||
/** 业务场景 */
|
||||
private String scene;
|
||||
/** 当前步骤 */
|
||||
private String step;
|
||||
/** 已识别的京东 3.cn 物流短链 */
|
||||
private String logisticsUrl;
|
||||
/** 最近一次交互时间(超时清理用) */
|
||||
private String lastInteractionTime;
|
||||
|
||||
public static WeComChatSession startLogisticsWaitRemark(String logisticsUrl) {
|
||||
WeComChatSession s = new WeComChatSession();
|
||||
s.setScene(SCENE_JD_LOGISTICS_SHARE);
|
||||
s.setStep(STEP_WAIT_REMARK);
|
||||
s.setLogisticsUrl(logisticsUrl);
|
||||
s.touch();
|
||||
return s;
|
||||
}
|
||||
|
||||
public void touch() {
|
||||
this.lastInteractionTime = LocalDateTime.now().format(FMT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否处于「已收物流链、待备注」步骤。方法名避免使用 isXxx,防止 Fastjson 序列化 Redis JSON 时混入多余布尔字段。
|
||||
*/
|
||||
public boolean matchLogisticsWaitRemark() {
|
||||
return SCENE_JD_LOGISTICS_SHARE.equals(scene) && STEP_WAIT_REMARK.equals(step)
|
||||
&& logisticsUrl != null && !logisticsUrl.isEmpty();
|
||||
}
|
||||
|
||||
public String getScene() {
|
||||
return scene;
|
||||
}
|
||||
|
||||
public void setScene(String scene) {
|
||||
this.scene = scene;
|
||||
}
|
||||
|
||||
public String getStep() {
|
||||
return step;
|
||||
}
|
||||
|
||||
public void setStep(String step) {
|
||||
this.step = step;
|
||||
}
|
||||
|
||||
public String getLogisticsUrl() {
|
||||
return logisticsUrl;
|
||||
}
|
||||
|
||||
public void setLogisticsUrl(String logisticsUrl) {
|
||||
this.logisticsUrl = logisticsUrl;
|
||||
}
|
||||
|
||||
public String getLastInteractionTime() {
|
||||
return lastInteractionTime;
|
||||
}
|
||||
|
||||
public void setLastInteractionTime(String lastInteractionTime) {
|
||||
this.lastInteractionTime = lastInteractionTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.ruoyi.jarvis.domain.dto;
|
||||
|
||||
/**
|
||||
* 企微消息经 wxSend 转发至 Jarvis 的请求体。
|
||||
* <p>
|
||||
* <b>身份约定</b>:{@link #fromUserName} 必须为解密后 XML 中的成员 UserID,节点名 {@code FromUserName}(见 {@link com.ruoyi.jarvis.wecom.WeComConvention}),
|
||||
* 全链路(会话 Redis、权限识别目标)均以此为准,勿用 MsgId、纯展示昵称等。
|
||||
* </p>
|
||||
*/
|
||||
public class WeComInboundRequest {
|
||||
/** 成员 UserID,与 XML {@code FromUserName} 同源 */
|
||||
private String fromUserName;
|
||||
private String content;
|
||||
/** 企微 CorpId */
|
||||
private String toUserName;
|
||||
private String agentId;
|
||||
private String msgId;
|
||||
/** 企微 XML CreateTime,秒级 Unix 时间戳(wxSend 传入) */
|
||||
private Long wxCreateTime;
|
||||
|
||||
public String getFromUserName() {
|
||||
return fromUserName;
|
||||
}
|
||||
|
||||
public void setFromUserName(String fromUserName) {
|
||||
this.fromUserName = fromUserName;
|
||||
}
|
||||
|
||||
public String getContent() {
|
||||
return content;
|
||||
}
|
||||
|
||||
public void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getToUserName() {
|
||||
return toUserName;
|
||||
}
|
||||
|
||||
public void setToUserName(String toUserName) {
|
||||
this.toUserName = toUserName;
|
||||
}
|
||||
|
||||
public String getAgentId() {
|
||||
return agentId;
|
||||
}
|
||||
|
||||
public void setAgentId(String agentId) {
|
||||
this.agentId = agentId;
|
||||
}
|
||||
|
||||
public String getMsgId() {
|
||||
return msgId;
|
||||
}
|
||||
|
||||
public void setMsgId(String msgId) {
|
||||
this.msgId = msgId;
|
||||
}
|
||||
|
||||
public Long getWxCreateTime() {
|
||||
return wxCreateTime;
|
||||
}
|
||||
|
||||
public void setWxCreateTime(Long wxCreateTime) {
|
||||
this.wxCreateTime = wxCreateTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.ruoyi.jarvis.domain.dto;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 企微桥接处理结果:首条走被动回复,其余由 Jarvis 调 wxSend 主动推送。
|
||||
*/
|
||||
public class WeComInboundResult {
|
||||
|
||||
private static final String TRACE_SEP = "\n\n————————————\n\n";
|
||||
|
||||
private final String passiveReply;
|
||||
private final List<String> activePushContents;
|
||||
|
||||
public WeComInboundResult(String passiveReply, List<String> activePushContents) {
|
||||
this.passiveReply = passiveReply != null ? passiveReply : "";
|
||||
this.activePushContents = activePushContents != null
|
||||
? Collections.unmodifiableList(new ArrayList<>(activePushContents))
|
||||
: Collections.emptyList();
|
||||
}
|
||||
|
||||
public static WeComInboundResult empty() {
|
||||
return new WeComInboundResult("", Collections.emptyList());
|
||||
}
|
||||
|
||||
public static WeComInboundResult passiveOnly(String passive) {
|
||||
return new WeComInboundResult(passive, Collections.emptyList());
|
||||
}
|
||||
|
||||
public String getPassiveReply() {
|
||||
return passiveReply;
|
||||
}
|
||||
|
||||
public List<String> getActivePushContents() {
|
||||
return activePushContents;
|
||||
}
|
||||
|
||||
public boolean hasActivePush() {
|
||||
return !activePushContents.isEmpty();
|
||||
}
|
||||
|
||||
/** 追踪表落库:被动 + 主动条文拼在一起便于审计 */
|
||||
public String toTraceFullText() {
|
||||
if (activePushContents.isEmpty()) {
|
||||
return passiveReply;
|
||||
}
|
||||
StringBuilder sb = new StringBuilder(passiveReply);
|
||||
for (String s : activePushContents) {
|
||||
sb.append(TRACE_SEP).append(s != null ? s : "");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.jarvis.domain.dto;
|
||||
|
||||
/**
|
||||
* 企微联调/测试数据清理选项(默认全选)
|
||||
*/
|
||||
public class WeComTestDataCleanRequest {
|
||||
|
||||
private Boolean clearTraceTable = true;
|
||||
private Boolean clearWecomSessions = true;
|
||||
private Boolean clearAdhocQueue = true;
|
||||
|
||||
public Boolean getClearTraceTable() {
|
||||
return clearTraceTable;
|
||||
}
|
||||
|
||||
public void setClearTraceTable(Boolean clearTraceTable) {
|
||||
this.clearTraceTable = clearTraceTable;
|
||||
}
|
||||
|
||||
public Boolean getClearWecomSessions() {
|
||||
return clearWecomSessions;
|
||||
}
|
||||
|
||||
public void setClearWecomSessions(Boolean clearWecomSessions) {
|
||||
this.clearWecomSessions = clearWecomSessions;
|
||||
}
|
||||
|
||||
public Boolean getClearAdhocQueue() {
|
||||
return clearAdhocQueue;
|
||||
}
|
||||
|
||||
public void setClearAdhocQueue(Boolean clearAdhocQueue) {
|
||||
this.clearAdhocQueue = clearAdhocQueue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.ruoyi.jarvis.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class GoofishNotifyMessage {
|
||||
private String appid;
|
||||
private Long timestamp;
|
||||
private String body;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ErpGoofishOrderEventLogMapper {
|
||||
|
||||
int insert(ErpGoofishOrderEventLog row);
|
||||
|
||||
List<ErpGoofishOrderEventLog> selectByOrderId(@Param("orderId") Long orderId);
|
||||
|
||||
List<ErpGoofishOrderEventLog> selectLogList(ErpGoofishOrderEventLogQuery query);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ErpGoofishOrderMapper {
|
||||
|
||||
ErpGoofishOrder selectById(Long id);
|
||||
|
||||
ErpGoofishOrder selectByAppKeyAndOrderNo(@Param("appKey") String appKey, @Param("orderNo") String orderNo);
|
||||
|
||||
List<ErpGoofishOrder> selectList(ErpGoofishOrder query);
|
||||
|
||||
int insert(ErpGoofishOrder row);
|
||||
|
||||
int update(ErpGoofishOrder row);
|
||||
|
||||
List<ErpGoofishOrder> selectPendingShip(@Param("statuses") List<Integer> statuses, @Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 按闲鱼买家订单号(与 jd_order.third_party_order_no 对齐时)检索,用于京东运单就绪后补绑发货。
|
||||
*/
|
||||
List<ErpGoofishOrder> selectByGoofishOrderNo(@Param("orderNo") String orderNo);
|
||||
|
||||
int resetShipForRetry(@Param("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ErpOpenConfigMapper {
|
||||
|
||||
ErpOpenConfig selectById(Long id);
|
||||
|
||||
ErpOpenConfig selectByAppKey(@Param("appKey") String appKey);
|
||||
|
||||
List<ErpOpenConfig> selectList(ErpOpenConfig query);
|
||||
|
||||
List<ErpOpenConfig> selectEnabledOrderBySort();
|
||||
|
||||
int insert(ErpOpenConfig row);
|
||||
|
||||
int update(ErpOpenConfig row);
|
||||
|
||||
int deleteById(Long id);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.GroupRebateExcelUpload;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface GroupRebateExcelUploadMapper {
|
||||
|
||||
int insertGroupRebateExcelUpload(GroupRebateExcelUpload row);
|
||||
|
||||
int updateGroupRebateExcelUpload(GroupRebateExcelUpload row);
|
||||
|
||||
int deleteGroupRebateExcelUploadById(Long id);
|
||||
|
||||
GroupRebateExcelUpload selectGroupRebateExcelUploadById(Long id);
|
||||
|
||||
List<GroupRebateExcelUpload> selectGroupRebateExcelUploadList(GroupRebateExcelUpload query);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -46,6 +47,11 @@ public interface JDOrderMapper {
|
||||
*/
|
||||
JDOrder selectJDOrderByThirdPartyOrderNo(String thirdPartyOrderNo);
|
||||
|
||||
/**
|
||||
* 后返备注 JSON 中含指定 uploadRecordId 的订单主键(撤销导入时用)
|
||||
*/
|
||||
List<Long> selectOrderIdsByRebateRemarkUploadRecordId(Long uploadRecordId);
|
||||
|
||||
/**
|
||||
* 批量删除(根据主键ID)
|
||||
*/
|
||||
@@ -56,6 +62,11 @@ public interface JDOrderMapper {
|
||||
* @return 订单列表
|
||||
*/
|
||||
List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD();
|
||||
|
||||
/**
|
||||
* 每个型号取其主键最大的一条订单的付款 / 后返(用于快捷录单下拉回填)
|
||||
*/
|
||||
List<QuickRecordModelOption> selectQuickRecordModelOptions();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -65,4 +65,9 @@ public interface SuperAdminMapper
|
||||
* @return 结果
|
||||
*/
|
||||
public int deleteSuperAdminByIds(Long[] ids);
|
||||
|
||||
/**
|
||||
* 企微成员 UserID:匹配 super_admin.wxid,或包含在 touser(逗号分隔)中
|
||||
*/
|
||||
SuperAdmin selectSuperAdminByWecomUserId(String wxid);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ruoyi.jarvis.mapper;
|
||||
import com.ruoyi.jarvis.domain.TencentDocBatchPushRecord;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -41,5 +42,11 @@ public interface TencentDocBatchPushRecordMapper {
|
||||
*/
|
||||
TencentDocBatchPushRecord selectLastSuccessRecord(@Param("fileId") String fileId,
|
||||
@Param("sheetId") String sheetId);
|
||||
|
||||
/**
|
||||
* 仍为 RUNNING 且开始时间早于指定时间的批次(用于超时归档)
|
||||
*/
|
||||
List<TencentDocBatchPushRecord> selectRunningRecordsBefore(@Param("fileId") String fileId,
|
||||
@Param("beforeTime") Date beforeTime);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComInboundTrace;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface WeComInboundTraceMapper {
|
||||
|
||||
int insertWeComInboundTrace(WeComInboundTrace trace);
|
||||
|
||||
WeComInboundTrace selectWeComInboundTraceById(Long id);
|
||||
|
||||
List<WeComInboundTrace> selectWeComInboundTraceList(WeComInboundTrace query);
|
||||
|
||||
int deleteWeComInboundTraceByIds(Long[] ids);
|
||||
|
||||
int deleteAllWeComInboundTrace();
|
||||
|
||||
/**
|
||||
* reply 中含「已加入查询队列」的消息(企微分享链备注提交成功后的被动回复)
|
||||
*/
|
||||
List<WeComInboundTrace> selectTracesShareLinkRemarkDone(@Param("replyMark") String replyMark);
|
||||
|
||||
/**
|
||||
* 同一用户、更早的一条含 3.cn 的消息(用于与备注消息配对出短链)
|
||||
*/
|
||||
WeComInboundTrace selectLatestPriorTraceWith3cnLink(@Param("fromUserName") String fromUserName,
|
||||
@Param("beforeId") long beforeId);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.ruoyi.jarvis.mapper;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface WeComShareLinkLogisticsJobMapper {
|
||||
|
||||
int insertWeComShareLinkLogisticsJob(WeComShareLinkLogisticsJob job);
|
||||
|
||||
int updateByJobKey(@Param("jobKey") String jobKey,
|
||||
@Param("status") String status,
|
||||
@Param("lastNote") String lastNote,
|
||||
@Param("scanAttempts") Integer scanAttempts,
|
||||
@Param("waybillNo") String waybillNo);
|
||||
|
||||
WeComShareLinkLogisticsJob selectByJobKey(String jobKey);
|
||||
|
||||
List<WeComShareLinkLogisticsJob> selectWeComShareLinkLogisticsJobList(WeComShareLinkLogisticsJob query);
|
||||
|
||||
/**
|
||||
* 待自动扫描但可能未在 Redis 队列中的任务(如纯 SQL 补录的 IMPORTED、入队失败后的 PENDING 等)。
|
||||
* 含曾 {@code ABANDONED} 的任务(对账时会先归零扫描次数并改回 {@code WAITING} 再入队)。
|
||||
* 仅 {@code create_time} 在最近一个月内的记录,避免扫到过旧历史。
|
||||
*/
|
||||
List<WeComShareLinkLogisticsJob> selectJobsNeedingQueueReconcile(@Param("limit") int limit);
|
||||
|
||||
int deleteByJobKey(@Param("jobKey") String jobKey);
|
||||
|
||||
/**
|
||||
* 机器人「京外物列表」:最近若干天内的任务,按 id 倒序,可选备注子串筛选。
|
||||
*/
|
||||
List<WeComShareLinkLogisticsJob> selectRecentForInstruction(@Param("remarkKeyword") String remarkKeyword,
|
||||
@Param("days") int days, @Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 机器人「京外物删」:按备注与短链精确匹配(trim)物理删除,返回删除行数。
|
||||
*/
|
||||
int deleteByRemarkAndTrackingUrl(@Param("remark") String remark, @Param("trackingUrl") String trackingUrl);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.ruoyi.jarvis.mq;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.jarvis.dto.GoofishNotifyMessage;
|
||||
import com.ruoyi.jarvis.service.goofish.GoofishOrderPipeline;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 闲管家订单推送消费(需配置 rocketmq.name-server)。
|
||||
* <p>
|
||||
* 在此线程内同步执行 {@link GoofishOrderPipeline#runFullPipeline},避免再投递 {@code @Async} 线程池造成
|
||||
* 「MQ 堆积 + goofishTaskExecutor 排队」的双重延迟;回调 HTTP 已在 {@link com.ruoyi.jarvis.service.impl.ErpGoofishOrderServiceImpl#publishOrProcessNotify} 中快速返回。
|
||||
*/
|
||||
@Component
|
||||
@ConditionalOnProperty(name = "rocketmq.name-server")
|
||||
@RocketMQMessageListener(
|
||||
nameServer = "${rocketmq.name-server}",
|
||||
topic = "${jarvis.goofish-order.mq-topic:jarvis-goofish-erp-order}",
|
||||
consumerGroup = "${jarvis.goofish-order.consumer-group:jarvis-goofish-order-consumer}"
|
||||
)
|
||||
public class GoofishOrderNotifyConsumer implements RocketMQListener<String> {
|
||||
|
||||
@Resource
|
||||
private GoofishOrderPipeline goofishOrderPipeline;
|
||||
|
||||
@Override
|
||||
public void onMessage(String message) {
|
||||
GoofishNotifyMessage m = JSON.parseObject(message, GoofishNotifyMessage.class);
|
||||
if (m == null || m.getAppid() == null) {
|
||||
return;
|
||||
}
|
||||
JSONObject body = m.getBody() == null ? new JSONObject() : JSON.parseObject(m.getBody());
|
||||
if (body == null) {
|
||||
body = new JSONObject();
|
||||
}
|
||||
goofishOrderPipeline.runFullPipeline(m.getAppid(), body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLogQuery;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IErpGoofishOrderService {
|
||||
|
||||
void publishOrProcessNotify(String appid, Long timestamp, JSONObject body);
|
||||
|
||||
/**
|
||||
* MQ 消费者或线程池:写库、拉详情、关联京东单、同步运单、尝试发货
|
||||
*/
|
||||
void asyncPipelineAfterNotify(String appid, JSONObject notifyBody);
|
||||
|
||||
List<ErpGoofishOrder> selectList(ErpGoofishOrder query);
|
||||
|
||||
ErpGoofishOrder selectById(Long id);
|
||||
|
||||
int pullOrdersForAppKey(String appKey, int lookbackHours);
|
||||
|
||||
int pullAllEnabled(int lookbackHours);
|
||||
|
||||
/** 按配置的起始天数 + 时间分段,尽量拉全历史订单(update_time) */
|
||||
int pullOrdersForAppKeyFullHistory(String appKey);
|
||||
|
||||
int pullAllEnabledFullHistory();
|
||||
|
||||
void refreshDetail(Long id);
|
||||
|
||||
void retryShip(Long id);
|
||||
|
||||
int syncWaybillAndTryShipBatch(int limit);
|
||||
|
||||
void applyListOrNotifyItem(String appKey, JSONObject item, String lastNotifyJson);
|
||||
|
||||
/**
|
||||
* 京东单物流扫描已得到运单号并写入 Redis 后调用:同步到闲鱼单并尝试开放平台发货。
|
||||
*/
|
||||
void notifyJdWaybillReady(Long jdOrderId);
|
||||
|
||||
/** 京东物流服务在写 Redis / 企微货主推送后、触发闲鱼同步前记一笔(source=JD_LOGISTICS_PUSH)。 */
|
||||
void traceJdLogisticsPushForGoofish(Long jdOrderId, String waybillNo, String summary);
|
||||
|
||||
/** 是否存在关联本京东单的闲管家订单(用于物流企微走闲鱼自建应用)。 */
|
||||
boolean hasLinkedGoofishOrder(Long jdOrderId);
|
||||
|
||||
/** 订单状态 / 物流 / 发货 变更日志(新→旧) */
|
||||
List<ErpGoofishOrderEventLog> listEventLogsByOrderId(Long orderId);
|
||||
|
||||
/** 全表分页检索(排查用,配合 PageHelper) */
|
||||
List<ErpGoofishOrderEventLog> selectEventLogList(ErpGoofishOrderEventLogQuery query);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IErpOpenConfigService {
|
||||
|
||||
ErpOpenConfig selectById(Long id);
|
||||
|
||||
ErpOpenConfig selectByAppKey(String appKey);
|
||||
|
||||
ErpOpenConfig selectFirstEnabled();
|
||||
|
||||
List<ErpOpenConfig> selectList(ErpOpenConfig query);
|
||||
|
||||
List<ErpOpenConfig> selectEnabledOrderBySort();
|
||||
|
||||
int insert(ErpOpenConfig row);
|
||||
|
||||
int update(ErpOpenConfig row);
|
||||
|
||||
int deleteById(Long id);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.GroupRebateExcelUpload;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface IGroupRebateExcelUploadService {
|
||||
|
||||
/**
|
||||
* 独立事务写入上传记录(导入主流程回滚时仍保留审计记录)
|
||||
*/
|
||||
void saveRecordInNewTransaction(GroupRebateExcelUpload row);
|
||||
|
||||
void updateRecordInNewTransaction(GroupRebateExcelUpload row);
|
||||
|
||||
GroupRebateExcelUpload selectGroupRebateExcelUploadById(Long id);
|
||||
|
||||
List<GroupRebateExcelUpload> selectGroupRebateExcelUploadList(GroupRebateExcelUpload query);
|
||||
}
|
||||
@@ -27,7 +27,13 @@ public interface IInstructionService {
|
||||
* @return 执行结果文本列表(可能为单条或多条)
|
||||
*/
|
||||
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole);
|
||||
|
||||
|
||||
/**
|
||||
* 执行文本指令(支持传入企微成员 UserID,用于「京」统计按绑定联盟过滤)
|
||||
* @param wecomUserId 企业微信成员 UserID;控制台等非企微入口传 null(按全局规则统计)
|
||||
*/
|
||||
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole, String wecomUserId);
|
||||
|
||||
/**
|
||||
* 获取历史消息记录
|
||||
* @param type 消息类型:request(请求) 或 response(响应)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 订单利润/售价:按分销标识规则计算并写回订单对象(由列表保存前调用)。
|
||||
*/
|
||||
public interface IJDOrderProfitService {
|
||||
|
||||
/**
|
||||
* 根据分销标识、型号配置、手动标记等,填充售价(自动时)并计算利润。
|
||||
* F / H-TF:利润 = 对客实收(直款=售价,闲鱼=扣点后的到账)-(下单付款 - 后返金额);
|
||||
* H-TF 未配置型号直款价时回退为固定 15 / 凡- 开头 65。
|
||||
* 会修改传入的 {@code order}。
|
||||
*/
|
||||
void recalculate(JDOrder order);
|
||||
|
||||
/**
|
||||
* 对「利润未手动锁定」的订单按当前库内数据重算售价/利润字段;仅当计算结果与库中不一致时才 UPDATE。
|
||||
*
|
||||
* @return 实际执行 UPDATE 的条数
|
||||
*/
|
||||
int syncAutoProfitIfChanged(List<Long> ids);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.domain.dto.QuickRecordModelOption;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -48,6 +49,9 @@ public interface IJDOrderService {
|
||||
|
||||
/** 查询分销标记为F或PDD且有物流链接的订单列表 */
|
||||
java.util.List<JDOrder> selectJDOrderListByDistributionMarkFOrPDD();
|
||||
|
||||
/** 快捷录单:型号及最近一次单的付款 / 后返 */
|
||||
List<QuickRecordModelOption> selectQuickRecordModelOptions();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.JDOrder;
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 物流信息服务接口
|
||||
@@ -12,6 +15,46 @@ public interface ILogisticsService {
|
||||
* @return 是否成功获取并推送
|
||||
*/
|
||||
boolean fetchLogisticsAndPush(JDOrder order);
|
||||
|
||||
/**
|
||||
* 分享类京东物流短链:查运单并通过 wxts 推送到指定 touser(不依赖订单表)
|
||||
*/
|
||||
boolean fetchLogisticsByShareLinkAndPush(String trackingUrl, String remark, String touser);
|
||||
|
||||
/**
|
||||
* 企微备注提交后仅入队,由 {@link com.ruoyi.jarvis.task.LogisticsScanTask} 与订单扫描一并拉取物流,避免阻塞 HTTP 回调。
|
||||
*
|
||||
* @param fromWecomUserId 企微消息 FromUserName,入库监控并在未配 touser 时作为推送目标
|
||||
*/
|
||||
void enqueueShareLinkForScan(String trackingUrl, String remark, String touser, String fromWecomUserId);
|
||||
|
||||
/**
|
||||
* 将已落库任务按与 {@link #enqueueShareLinkForScan} 相同的 JSON 格式推入 Redis(补录、手动失败后重入队等)。
|
||||
* Redis 项中的 attempts 与定时 drain 重试上限对齐,取自 {@link WeComShareLinkLogisticsJob#getScanAttempts()}(仅由 drain 回写递增;管理端手动拉取失败不会垫高该计数)。
|
||||
*/
|
||||
void pushShareLinkJobToRedis(WeComShareLinkLogisticsJob job);
|
||||
|
||||
/**
|
||||
* 扫描落库表中仍待处理的任务(含 {@code ABANDONED},对账时会重置计数后再入队;仅 {@code create_time} 一个月内),在限频下补入 Redis。
|
||||
*
|
||||
* @return 本次获得限频锁并成功推入队列的条数
|
||||
*/
|
||||
int reconcileShareLinkJobsIntoPendingQueue();
|
||||
|
||||
/**
|
||||
* 定时任务内:依次弹出队列并调用 {@link #fetchLogisticsByShareLinkAndPush}。
|
||||
*
|
||||
* @return 本轮从 Redis 弹出并尝试处理的条数(与定时任务尾段逻辑一致)
|
||||
*/
|
||||
int drainPendingShareLinkQueue();
|
||||
|
||||
/**
|
||||
* 管理端调试:立刻请求物流接口并尝试推送分享链模板,返回字段与订单「手动获取物流」相近(requestUrl、responseRaw、responseData、pushSent 等)。
|
||||
*/
|
||||
Map<String, Object> adminFetchShareLinkLogisticsDebug(String trackingUrl, String remark, String touser);
|
||||
|
||||
/** 测试清理:删除分享链待扫描队列键 */
|
||||
void clearAdhocPendingQueue();
|
||||
|
||||
/**
|
||||
* 检查订单是否已处理过(Redis中是否有运单号)
|
||||
@@ -25,6 +68,14 @@ public interface ILogisticsService {
|
||||
* @return 健康状态信息,包含是否健康、状态描述等
|
||||
*/
|
||||
HealthCheckResult checkHealth();
|
||||
|
||||
/**
|
||||
* 构造调用物流解析服务的完整 GET URL(路径与编码与 {@link #fetchLogisticsAndPush} 一致)。
|
||||
* 配置多个 {@code jarvis.server.logistics.base-urls} 时按轮询选取 base,便于内网多实例并行。
|
||||
*
|
||||
* @param logisticsLink 原始物流追踪链接(未编码)
|
||||
*/
|
||||
String buildFetchLogisticsRequestUrl(String logisticsLink);
|
||||
|
||||
/**
|
||||
* 健康检测结果
|
||||
|
||||
@@ -65,12 +65,43 @@ public interface ISocialMediaService
|
||||
com.ruoyi.common.core.domain.AjaxResult deletePromptTemplate(String key);
|
||||
|
||||
/**
|
||||
* 根据标题(+可选型号备注)生成闲鱼文案(代下单、教你下单),不依赖JD接口
|
||||
* 生成闲鱼文案(代下单、教你下单),并可选生成种草文案。
|
||||
*
|
||||
* @param title 商品标题(必填)
|
||||
* @param remark 型号/备注(可选)
|
||||
* @return 包含代下单、教你下单两种文案的 Map
|
||||
* request 常用字段:
|
||||
* - title/remark(兼容旧版手动入口)
|
||||
* - generateSeedNote(boolean) 是否额外生成种草文案
|
||||
* - goods_title/goods_model/goods_type(种草文案必传)
|
||||
* - goods_brand/channel_source/official_price/sell_price/warranty/delivery_install(可选)
|
||||
*
|
||||
* @return 包含代下单、教你下单,以及可选 seedNote 的 Map
|
||||
*/
|
||||
Map<String, Object> generateXianyuWenan(String title, String remark);
|
||||
Map<String, Object> generateXianyuWenan(Map<String, Object> request);
|
||||
|
||||
/** 列出多套大模型接入配置及当前激活的 id(与 Jarvis 共用 Redis) */
|
||||
com.ruoyi.common.core.domain.AjaxResult listLlmProfiles();
|
||||
|
||||
/** 获取单套配置(编辑用,密钥脱敏) */
|
||||
com.ruoyi.common.core.domain.AjaxResult getLlmProfile(String id);
|
||||
|
||||
/** 新增一套配置 */
|
||||
com.ruoyi.common.core.domain.AjaxResult createLlmProfile(Map<String, Object> request);
|
||||
|
||||
/** 更新一套配置 */
|
||||
com.ruoyi.common.core.domain.AjaxResult updateLlmProfile(String id, Map<String, Object> request);
|
||||
|
||||
/** 删除一套配置 */
|
||||
com.ruoyi.common.core.domain.AjaxResult deleteLlmProfile(String id);
|
||||
|
||||
/** 激活指定 id,Jarvis 调用时使用该套 */
|
||||
com.ruoyi.common.core.domain.AjaxResult setActiveLlmProfile(String id);
|
||||
|
||||
/** 取消激活(Jarvis 回退 yml 默认 Ollama) */
|
||||
com.ruoyi.common.core.domain.AjaxResult clearActiveLlmProfile();
|
||||
|
||||
/** 清空所有套及旧版单键,Jarvis 回退默认 */
|
||||
com.ruoyi.common.core.domain.AjaxResult resetAllLlmConfig();
|
||||
|
||||
/** 转发 Jarvis 做一次 LLM 连通测试(可指定 profileId 与自定义问题) */
|
||||
com.ruoyi.common.core.domain.AjaxResult testLlmProfile(Map<String, Object> request);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,5 +41,10 @@ public interface ITencentDocBatchPushService {
|
||||
* 获取推送状态和倒计时信息
|
||||
*/
|
||||
Map<String, Object> getPushStatusAndCountdown();
|
||||
|
||||
/**
|
||||
* 将长时间仍处于 RUNNING 的批次归档为 INTERRUPTED(并可选发企微告警,见实现类配置)
|
||||
*/
|
||||
void reconcileStaleRunningRecords(String fileId);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.dto.WeComChatSession;
|
||||
import com.ruoyi.jarvis.wecom.WeComConvention;
|
||||
|
||||
/**
|
||||
* 企微多轮会话(Redis JSON)。键由 {@link WeComConvention#sessionRedisKey(String)} 生成,
|
||||
* 参数 wecomUserId 必须为明文 XML 中的 <b>FromUserName</b>(成员 UserID),与 wxSend 转发字段一致。
|
||||
*/
|
||||
public interface IWeComChatSessionService {
|
||||
|
||||
/** @param wecomUserId 成员 UserID,同 FromUserName */
|
||||
WeComChatSession get(String wecomUserId);
|
||||
|
||||
/** @param wecomUserId 成员 UserID,同 FromUserName */
|
||||
void put(String wecomUserId, WeComChatSession session);
|
||||
|
||||
/** @param wecomUserId 成员 UserID,同 FromUserName */
|
||||
void delete(String wecomUserId);
|
||||
|
||||
/** 测试清理:删除所有 interaction_state:wecom:* */
|
||||
int deleteAllWecomSessionsForTest();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
|
||||
import com.ruoyi.jarvis.domain.dto.WeComInboundResult;
|
||||
|
||||
/**
|
||||
* 企微文本消息业务入口(由 wxSend 通过 HTTPS + 共享密钥调用)
|
||||
*/
|
||||
public interface IWeComInboundService {
|
||||
|
||||
/**
|
||||
* 长文本按企微上限拆成多段(每段 ≤2048 UTF-8 字节):首段被动回复,后续段由控制器异步调 wxSend /wecom/active-push。
|
||||
*/
|
||||
WeComInboundResult handleInbound(WeComInboundRequest request);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComInboundTrace;
|
||||
import com.ruoyi.jarvis.domain.dto.WeComInboundRequest;
|
||||
import com.ruoyi.jarvis.domain.dto.WeComTestDataCleanRequest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IWeComInboundTraceService {
|
||||
|
||||
void recordInbound(WeComInboundRequest request, String reply);
|
||||
|
||||
WeComInboundTrace selectWeComInboundTraceById(Long id);
|
||||
|
||||
List<WeComInboundTrace> selectWeComInboundTraceList(WeComInboundTrace query);
|
||||
|
||||
int deleteWeComInboundTraceByIds(Long[] ids);
|
||||
|
||||
/**
|
||||
* 清理联调/测试数据,选项见 {@link WeComTestDataCleanRequest}
|
||||
*/
|
||||
Map<String, Object> cleanTestData(WeComTestDataCleanRequest options);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.ruoyi.jarvis.service;
|
||||
|
||||
import com.ruoyi.jarvis.domain.WeComShareLinkLogisticsJob;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface IWeComShareLinkLogisticsJobService {
|
||||
|
||||
WeComShareLinkLogisticsJob selectByJobKey(String jobKey);
|
||||
|
||||
List<WeComShareLinkLogisticsJob> selectList(WeComShareLinkLogisticsJob query);
|
||||
|
||||
/**
|
||||
* 从 wecom_inbound_trace 补录:reply 含「已加入查询队列」且能解析出 3.cn 短链。
|
||||
* jobKey 固定为 tracebf{traceId},可重复执行跳过已存在项。
|
||||
*/
|
||||
Map<String, Object> backfillImportedFromInboundTrace();
|
||||
|
||||
List<WeComShareLinkLogisticsJob> selectRecentForInstruction(String remarkKeyword, int days, int limit);
|
||||
|
||||
int deleteByJobKey(String jobKey);
|
||||
|
||||
int deleteByRemarkAndTrackingUrl(String remark, String trackingUrl);
|
||||
}
|
||||
@@ -5,10 +5,15 @@ package com.ruoyi.jarvis.service;
|
||||
*/
|
||||
public interface IWxSendService {
|
||||
/**
|
||||
* 检查微信推送服务健康状态
|
||||
* 检查微信推送服务健康状态(会真实下发一条测试消息,仅用于服务监控页「手动测试」)
|
||||
* @return 健康状态信息,包含是否健康、状态描述等
|
||||
*/
|
||||
HealthCheckResult checkHealth();
|
||||
|
||||
/**
|
||||
* 已配置的微信推送健康检查 URL(展示用,不发起请求)
|
||||
*/
|
||||
String getHealthCheckServiceUrl();
|
||||
|
||||
/**
|
||||
* 健康检测结果
|
||||
|
||||
@@ -59,4 +59,6 @@ public interface SuperAdminService
|
||||
public int deleteSuperAdminById(Long id);
|
||||
|
||||
SuperAdmin selectSuperAdminByUnionId(Long unionId);
|
||||
|
||||
SuperAdmin selectSuperAdminByWecomUserId(String wxid);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.ruoyi.jarvis.service.erp;
|
||||
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.erp.request.ERPAccount;
|
||||
import com.ruoyi.erp.request.IERPAccount;
|
||||
import com.ruoyi.jarvis.domain.ErpOpenConfig;
|
||||
import com.ruoyi.jarvis.service.IErpOpenConfigService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class ErpAccountResolver {
|
||||
|
||||
@Resource
|
||||
private IErpOpenConfigService erpOpenConfigService;
|
||||
|
||||
/**
|
||||
* 优先库表启用配置,其次兼容历史枚举,最后默认胡歌
|
||||
*/
|
||||
public IERPAccount resolve(String appid) {
|
||||
if (StringUtils.isNotEmpty(appid)) {
|
||||
ErpOpenConfig cfg = erpOpenConfigService.selectByAppKey(appid.trim());
|
||||
if (cfg != null && "0".equals(cfg.getStatus())) {
|
||||
return cfg;
|
||||
}
|
||||
for (ERPAccount a : ERPAccount.values()) {
|
||||
if (a.getApiKey().equals(appid.trim())) {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
}
|
||||
ErpOpenConfig first = erpOpenConfigService.selectFirstEnabled();
|
||||
if (first != null) {
|
||||
return first;
|
||||
}
|
||||
return ERPAccount.ACCOUNT_HUGE;
|
||||
}
|
||||
|
||||
/** 校验回调签名时须严格对应 appid,不允许回落到默认账号 */
|
||||
public IERPAccount resolveStrict(String appid) {
|
||||
if (StringUtils.isEmpty(appid)) {
|
||||
return null;
|
||||
}
|
||||
ErpOpenConfig cfg = erpOpenConfigService.selectByAppKey(appid.trim());
|
||||
if (cfg != null && "0".equals(cfg.getStatus())) {
|
||||
return cfg;
|
||||
}
|
||||
for (ERPAccount a : ERPAccount.values()) {
|
||||
if (a.getApiKey().equals(appid.trim())) {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ruoyi.jarvis.service.goofish;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
@Component
|
||||
public class GoofishNotifyAsyncFacade {
|
||||
|
||||
@Resource
|
||||
private GoofishOrderPipeline goofishOrderPipeline;
|
||||
|
||||
@Async("goofishTaskExecutor")
|
||||
public void afterNotify(String appid, JSONObject body) {
|
||||
goofishOrderPipeline.runFullPipeline(appid, body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.ruoyi.jarvis.service.goofish;
|
||||
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.StringUtils;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
|
||||
import com.ruoyi.jarvis.domain.ErpGoofishOrderEventLog;
|
||||
import com.ruoyi.jarvis.mapper.ErpGoofishOrderEventLogMapper;
|
||||
import com.ruoyi.jarvis.wecom.WxSendGoofishNotifyClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 闲管家订单:状态 / 物流 / 发货 变更落库 + SLF4J
|
||||
*/
|
||||
@Component
|
||||
public class GoofishOrderChangeLogger {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(GoofishOrderChangeLogger.class);
|
||||
private static final int MESSAGE_MAX = 1000;
|
||||
|
||||
public static final String TYPE_ORDER = "ORDER_SYNC";
|
||||
public static final String TYPE_LOGISTICS = "LOGISTICS_SYNC";
|
||||
public static final String TYPE_SHIP = "SHIP";
|
||||
|
||||
/**
|
||||
* 京东物流扫描:货主通知已由 LogisticsServiceImpl 走闲鱼自建应用或 PDD;此处仅落库,避免再调 wxSend 重复推送。
|
||||
*/
|
||||
public static final String SOURCE_JD_LOGISTICS_PUSH = "JD_LOGISTICS_PUSH";
|
||||
|
||||
/**
|
||||
* Redis 写入本地运单:与后续详情运单合并、发货成功通知重复度高,仅落库便于对账,不推企微。
|
||||
*/
|
||||
public static final String SOURCE_REDIS_WAYBILL = "REDIS_WAYBILL";
|
||||
|
||||
@Resource
|
||||
private ErpGoofishOrderEventLogMapper erpGoofishOrderEventLogMapper;
|
||||
|
||||
@Resource
|
||||
private WxSendGoofishNotifyClient wxSendGoofishNotifyClient;
|
||||
|
||||
public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message) {
|
||||
append(orderId, appKey, orderNo, eventType, source, message, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param notifyWecom 为 false 时仅写事件日志,不推企微(用于过滤纯对齐类状态)。
|
||||
*/
|
||||
public void append(Long orderId, String appKey, String orderNo, String eventType, String source, String message,
|
||||
boolean notifyWecom) {
|
||||
if (orderId == null || StringUtils.isEmpty(message)) {
|
||||
return;
|
||||
}
|
||||
String msg = message.length() > MESSAGE_MAX ? message.substring(0, MESSAGE_MAX - 1) + "…" : message;
|
||||
ErpGoofishOrderEventLog row = new ErpGoofishOrderEventLog();
|
||||
row.setOrderId(orderId);
|
||||
row.setAppKey(appKey);
|
||||
row.setOrderNo(orderNo != null ? orderNo : "");
|
||||
row.setEventType(eventType != null ? eventType : "UNKNOWN");
|
||||
row.setSource(source != null ? source : "");
|
||||
row.setMessage(msg);
|
||||
row.setCreateTime(DateUtils.getNowDate());
|
||||
try {
|
||||
erpGoofishOrderEventLogMapper.insert(row);
|
||||
} catch (Exception e) {
|
||||
log.warn("闲管家订单事件日志写入失败 orderId={} {}", orderId, e.toString());
|
||||
return;
|
||||
}
|
||||
log.info("[goofish-order-event] orderId={} orderNo={} type={} source={} {}", orderId, orderNo, eventType, source, msg);
|
||||
if (SOURCE_JD_LOGISTICS_PUSH.equals(source) || SOURCE_REDIS_WAYBILL.equals(source)) {
|
||||
return;
|
||||
}
|
||||
if (!notifyWecom) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
wxSendGoofishNotifyClient.notifyGoofishEvent(orderNo, eventType, source, msg);
|
||||
} catch (Exception ex) {
|
||||
log.debug("闲鱼订单事件 wxSend 通知跳过 err={}", ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并摘要前后对比:订单状态/退款状态、平台运单与快递、本地运单。
|
||||
*
|
||||
* @param beforeSnap 合并前从 row 拷贝的跟踪字段快照(可不含 id)
|
||||
* @param afterRow 合并并 apply 后的 row
|
||||
*/
|
||||
public void logSummaryMergeDiff(ErpGoofishOrder beforeSnap, ErpGoofishOrder afterRow, String source) {
|
||||
if (afterRow == null || afterRow.getId() == null) {
|
||||
return;
|
||||
}
|
||||
RowSnap b = RowSnap.from(beforeSnap);
|
||||
RowSnap a = RowSnap.from(afterRow);
|
||||
|
||||
boolean refundDiff = !Objects.equals(b.refundStatus, a.refundStatus);
|
||||
boolean orderDiff = !Objects.equals(b.orderStatus, a.orderStatus);
|
||||
|
||||
List<String> orderParts = new ArrayList<>();
|
||||
if (orderDiff) {
|
||||
orderParts.add("订单状态 " + GoofishStatusLabels.orderStatusChangeForNotify(b.orderStatus, a.orderStatus));
|
||||
}
|
||||
if (refundDiff) {
|
||||
orderParts.add("退款状态 " + GoofishStatusLabels.refundStatusChange(b.refundStatus, a.refundStatus));
|
||||
}
|
||||
if (!orderParts.isEmpty()) {
|
||||
boolean notifyWecom = refundDiff || (orderDiff
|
||||
&& GoofishStatusLabels.isWxNotifiableOrderStatusChange(b.orderStatus, a.orderStatus));
|
||||
append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_ORDER, source,
|
||||
String.join(";", orderParts), notifyWecom);
|
||||
}
|
||||
|
||||
List<String> logParts = new ArrayList<>();
|
||||
if (!eqStr(b.detailWaybillNo, a.detailWaybillNo)) {
|
||||
logParts.add("平台运单 " + str(b.detailWaybillNo) + "→" + str(a.detailWaybillNo));
|
||||
}
|
||||
if (!eqStr(b.detailExpressCode, a.detailExpressCode)) {
|
||||
logParts.add("快递编码 " + str(b.detailExpressCode) + "→" + str(a.detailExpressCode));
|
||||
}
|
||||
if (!eqStr(b.detailExpressName, a.detailExpressName)) {
|
||||
logParts.add("快递名称 " + str(b.detailExpressName) + "→" + str(a.detailExpressName));
|
||||
}
|
||||
if (!eqStr(b.localWaybillNo, a.localWaybillNo)) {
|
||||
logParts.add("本地运单 " + str(b.localWaybillNo) + "→" + str(a.localWaybillNo));
|
||||
}
|
||||
if (!logParts.isEmpty()) {
|
||||
append(afterRow.getId(), afterRow.getAppKey(), afterRow.getOrderNo(), TYPE_LOGISTICS, source,
|
||||
String.join(";", logParts));
|
||||
}
|
||||
}
|
||||
|
||||
private static String str(Object o) {
|
||||
return o == null ? "null" : String.valueOf(o);
|
||||
}
|
||||
|
||||
private static boolean eqStr(String x, String y) {
|
||||
String a = x == null ? "" : x.trim();
|
||||
String b = y == null ? "" : y.trim();
|
||||
return Objects.equals(a, b);
|
||||
}
|
||||
|
||||
private static final class RowSnap {
|
||||
Integer orderStatus;
|
||||
Integer refundStatus;
|
||||
String detailWaybillNo;
|
||||
String detailExpressCode;
|
||||
String detailExpressName;
|
||||
String localWaybillNo;
|
||||
|
||||
static RowSnap from(ErpGoofishOrder r) {
|
||||
RowSnap s = new RowSnap();
|
||||
if (r == null) {
|
||||
return s;
|
||||
}
|
||||
s.orderStatus = r.getOrderStatus();
|
||||
s.refundStatus = r.getRefundStatus();
|
||||
s.detailWaybillNo = r.getDetailWaybillNo();
|
||||
s.detailExpressCode = r.getDetailExpressCode();
|
||||
s.detailExpressName = r.getDetailExpressName();
|
||||
s.localWaybillNo = r.getLocalWaybillNo();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user