This commit is contained in:
van
2026-04-09 00:09:09 +08:00
parent c9876df3de
commit e94f17973c
50 changed files with 1637 additions and 72 deletions

View File

@@ -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);
}

View File

@@ -9,7 +9,7 @@ import lombok.Getter;
* @descriptionERP账户枚举类
*/
@Getter
public enum ERPAccount {
public enum ERPAccount implements IERPAccount {
// 胡歌1016208368633221
ACCOUNT_HUGE("1016208368633221", "waLiRMgFcixLbcLjUSSwo370Hp1nBcBu","余生请多关照66","海尔胡歌"),
// 刘强东anotherApiKey

View File

@@ -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;
}

View File

@@ -6,7 +6,7 @@ package com.ruoyi.erp.request;
* 对应接口POST /api/open/express/companies
*/
public class ExpressCompaniesQueryRequest extends ERPRequestBase {
public ExpressCompaniesQueryRequest(ERPAccount erpAccount) {
public ExpressCompaniesQueryRequest(IERPAccount erpAccount) {
super("https://open.goofish.pro/api/open/express/companies", erpAccount);
}
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.erp.request;
/**
* 闲管家开放平台凭证({@link ERPAccount} 或库表配置行均可实现本接口)
*/
public interface IERPAccount {
String getApiKey();
String getApiKeySecret();
/** 闲鱼会员名(授权维度展示用,可为空) */
String getXyName();
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,22 @@
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();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("goofish-");
executor.initialize();
return executor;
}
}

View File

@@ -0,0 +1,33 @@
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 0/15 * * * ?";
/** 同步运单 + 自动发货 cron */
private String autoShipCron = "0 2/10 * * * ?";
/** 单次拉单每店最大页数防护 */
private int pullMaxPagesPerShop = 30;
private int autoShipBatchSize = 20;
}

View File

@@ -0,0 +1,44 @@
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;
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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,23 @@
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("limit") int limit);
int resetShipForRetry(@Param("id") Long id);
}

View File

@@ -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);
}

View File

@@ -0,0 +1,41 @@
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.GoofishNotifyAsyncFacade;
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
*/
@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 GoofishNotifyAsyncFacade goofishNotifyAsyncFacade;
@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();
}
goofishNotifyAsyncFacade.afterNotify(m.getAppid(), body);
}
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.jarvis.service;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
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);
void refreshDetail(Long id);
void retryShip(Long id);
int syncWaybillAndTryShipBatch(int limit);
void applyListOrNotifyItem(String appKey, JSONObject item, String lastNotifyJson);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,479 @@
package com.ruoyi.jarvis.service.goofish;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.erp.request.AuthorizeListQueryRequest;
import com.ruoyi.erp.request.IERPAccount;
import com.ruoyi.erp.request.OrderDetailQueryRequest;
import com.ruoyi.erp.request.OrderListQueryRequest;
import com.ruoyi.erp.request.OrderShipRequest;
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
import com.ruoyi.jarvis.domain.ErpOpenConfig;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.mapper.ErpGoofishOrderMapper;
import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.service.IErpOpenConfigService;
import com.ruoyi.jarvis.service.erp.ErpAccountResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 闲管家:推送/拉单后的落库、详情、关联京东单、同步运单、发货
*/
@Component
public class GoofishOrderPipeline {
private static final Logger log = LoggerFactory.getLogger(GoofishOrderPipeline.class);
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
@Resource
private ErpGoofishOrderMapper erpGoofishOrderMapper;
@Resource
private ErpAccountResolver erpAccountResolver;
@Resource
private IErpOpenConfigService erpOpenConfigService;
@Resource
private IJDOrderService jdOrderService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private JarvisGoofishProperties goofishProperties;
public void runFullPipeline(String appid, JSONObject notifyBody) {
try {
ErpGoofishOrder row = upsertFromNotify(appid, notifyBody, notifyBody.toJSONString());
tryLinkJdOrder(row);
refreshDetail(row);
syncWaybillFromRedis(row);
tryAutoShip(row);
} catch (Exception e) {
log.error("闲管家订单流水线异常 appid={} body={}", appid, notifyBody, e);
}
}
public void applyListOrNotifyItem(String appKey, JSONObject item, String lastNotifyJson) {
if (item == null || StringUtils.isEmpty(appKey)) {
return;
}
ErpGoofishOrder row = upsertFromNotify(appKey, item, lastNotifyJson);
tryLinkJdOrder(row);
refreshDetail(row);
syncWaybillFromRedis(row);
tryAutoShip(row);
}
public ErpGoofishOrder upsertFromNotify(String appKey, JSONObject body, String lastNotifyJson) {
Date now = DateUtils.getNowDate();
String orderNo = body.getString("order_no");
if (StringUtils.isEmpty(orderNo)) {
throw new IllegalArgumentException("缺少 order_no");
}
ErpGoofishOrder existing = erpGoofishOrderMapper.selectByAppKeyAndOrderNo(appKey, orderNo);
ErpGoofishOrder e = new ErpGoofishOrder();
e.setAppKey(appKey);
e.setSellerId(body.getLong("seller_id"));
e.setUserName(body.getString("user_name"));
e.setOrderNo(orderNo);
e.setOrderType(body.getInteger("order_type"));
e.setOrderStatus(body.getInteger("order_status"));
e.setRefundStatus(body.getInteger("refund_status"));
e.setModifyTime(body.getLong("modify_time"));
e.setProductId(body.getLong("product_id"));
e.setItemId(body.getLong("item_id"));
e.setLastNotifyJson(lastNotifyJson);
e.setUpdateTime(now);
if (existing == null) {
e.setCreateTime(now);
if (e.getShipStatus() == null) {
e.setShipStatus(0);
}
erpGoofishOrderMapper.insert(e);
return erpGoofishOrderMapper.selectByAppKeyAndOrderNo(appKey, orderNo);
}
e.setId(existing.getId());
e.setDetailJson(existing.getDetailJson());
e.setJdOrderId(existing.getJdOrderId());
e.setLocalWaybillNo(existing.getLocalWaybillNo());
e.setShipStatus(existing.getShipStatus());
e.setShipTime(existing.getShipTime());
e.setShipError(existing.getShipError());
e.setShipExpressCode(existing.getShipExpressCode());
erpGoofishOrderMapper.update(e);
return erpGoofishOrderMapper.selectByAppKeyAndOrderNo(appKey, orderNo);
}
public void tryLinkJdOrder(ErpGoofishOrder row) {
if (row == null || row.getId() == null || row.getJdOrderId() != null) {
return;
}
String orderNo = row.getOrderNo();
if (StringUtils.isEmpty(orderNo)) {
return;
}
JDOrder jd = jdOrderService.selectJDOrderByThirdPartyOrderNo(orderNo);
if (jd == null || jd.getId() == null) {
return;
}
ErpGoofishOrder patch = new ErpGoofishOrder();
patch.setId(row.getId());
patch.setJdOrderId(jd.getId());
patch.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(patch);
row.setJdOrderId(jd.getId());
}
public void refreshDetail(ErpGoofishOrder row) {
if (row == null || row.getId() == null) {
return;
}
try {
IERPAccount cred = erpAccountResolver.resolve(row.getAppKey());
OrderDetailQueryRequest req = new OrderDetailQueryRequest(cred);
req.setOrderNo(row.getOrderNo());
String resp = req.getResponseBody();
ErpGoofishOrder patch = new ErpGoofishOrder();
patch.setId(row.getId());
patch.setDetailJson(resp);
patch.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(patch);
row.setDetailJson(resp);
JSONObject jo = JSON.parseObject(resp);
if (jo != null && jo.getIntValue("code") == 0) {
JSONObject data = jo.getJSONObject("data");
if (data != null) {
mergeSummaryFromDetail(row, data);
}
}
} catch (Exception ex) {
log.warn("拉取闲管家订单详情失败 id={} orderNo={} err={}", row.getId(), row.getOrderNo(), ex.getMessage());
}
}
private void mergeSummaryFromDetail(ErpGoofishOrder row, JSONObject data) {
ErpGoofishOrder patch = new ErpGoofishOrder();
patch.setId(row.getId());
boolean any = false;
Integer os = data.getInteger("order_status");
if (os != null) {
patch.setOrderStatus(os);
row.setOrderStatus(os);
any = true;
}
Integer rs = data.getInteger("refund_status");
if (rs != null) {
patch.setRefundStatus(rs);
row.setRefundStatus(rs);
any = true;
}
Long mt = data.getLong("modify_time");
if (mt == null) {
mt = data.getLong("order_modify_time");
}
if (mt != null) {
patch.setModifyTime(mt);
row.setModifyTime(mt);
any = true;
}
if (any) {
patch.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(patch);
}
}
public void syncWaybillFromRedis(ErpGoofishOrder row) {
if (row == null || row.getId() == null || row.getJdOrderId() == null) {
return;
}
String key = REDIS_WAYBILL_KEY_PREFIX + row.getJdOrderId();
String wb = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isEmpty(wb)) {
return;
}
if (wb.equals(row.getLocalWaybillNo())) {
return;
}
ErpGoofishOrder patch = new ErpGoofishOrder();
patch.setId(row.getId());
patch.setLocalWaybillNo(wb.trim());
patch.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(patch);
row.setLocalWaybillNo(wb.trim());
}
public void tryAutoShip(ErpGoofishOrder row) {
if (row == null || row.getId() == null) {
return;
}
if (row.getOrderStatus() == null || row.getOrderStatus() != 12) {
return;
}
if (row.getRefundStatus() != null && row.getRefundStatus() != 0) {
return;
}
if (row.getShipStatus() != null && row.getShipStatus() == 1) {
return;
}
if (StringUtils.isEmpty(row.getLocalWaybillNo())) {
return;
}
ErpOpenConfig cfg = erpOpenConfigService.selectByAppKey(row.getAppKey());
String expressCode = cfg != null ? cfg.getExpressCode() : null;
String expressName = cfg != null && StringUtils.isNotEmpty(cfg.getExpressName()) ? cfg.getExpressName() : "日日顺";
if (StringUtils.isEmpty(expressCode)) {
log.info("闲管家自动发货跳过:未配置 express_code appKey={} orderNo={}", row.getAppKey(), row.getOrderNo());
return;
}
ShipAddressParts addr = parseShipAddress(row.getDetailJson());
if (addr == null || StringUtils.isEmpty(addr.shipName) || StringUtils.isEmpty(addr.shipMobile)
|| StringUtils.isEmpty(addr.shipAddress)) {
patchShipError(row, "详情中缺少收货人/手机/地址,请核对开放平台订单详情 JSON 字段");
return;
}
try {
IERPAccount cred = erpAccountResolver.resolve(row.getAppKey());
OrderShipRequest ship = new OrderShipRequest(cred);
ship.setOrderNo(row.getOrderNo());
ship.setShipName(addr.shipName);
ship.setShipMobile(addr.shipMobile);
ship.setShipAddress(addr.shipAddress);
if (addr.shipDistrictId != null) {
ship.setShipDistrictId(addr.shipDistrictId);
}
if (StringUtils.isNotEmpty(addr.shipProvName)) {
ship.setShipProvName(addr.shipProvName);
}
if (StringUtils.isNotEmpty(addr.shipCityName)) {
ship.setShipCityName(addr.shipCityName);
}
if (StringUtils.isNotEmpty(addr.shipAreaName)) {
ship.setShipAreaName(addr.shipAreaName);
}
ship.setWaybillNo(row.getLocalWaybillNo());
ship.setExpressCode(expressCode);
ship.setExpressName(expressName);
String resp = ship.getResponseBody();
JSONObject r = JSON.parseObject(resp);
if (r != null && r.getIntValue("code") == 0) {
ErpGoofishOrder ok = new ErpGoofishOrder();
ok.setId(row.getId());
ok.setShipStatus(1);
ok.setShipTime(DateUtils.getNowDate());
ok.setShipExpressCode(expressCode);
ok.setShipError(null);
ok.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(ok);
row.setShipStatus(1);
} else {
String msg = r != null ? r.getString("msg") : "unknown";
patchShipError(row, msg != null ? msg : "发货接口返回失败");
}
} catch (Exception ex) {
patchShipError(row, ex.getMessage());
log.warn("闲管家发货异常 orderNo={}", row.getOrderNo(), ex);
}
}
private void patchShipError(ErpGoofishOrder row, String err) {
ErpGoofishOrder p = new ErpGoofishOrder();
p.setId(row.getId());
p.setShipStatus(2);
String m = err == null ? "" : err;
if (m.length() > 480) {
m = m.substring(0, 480) + "";
}
p.setShipError(m);
p.setUpdateTime(DateUtils.getNowDate());
erpGoofishOrderMapper.update(p);
row.setShipStatus(2);
row.setShipError(m);
}
private static class ShipAddressParts {
String shipName;
String shipMobile;
String shipAddress;
Integer shipDistrictId;
String shipProvName;
String shipCityName;
String shipAreaName;
}
private ShipAddressParts parseShipAddress(String detailJson) {
if (StringUtils.isEmpty(detailJson)) {
return null;
}
JSONObject root = JSON.parseObject(detailJson);
if (root == null) {
return null;
}
JSONObject data = root.getJSONObject("data");
if (data == null) {
data = root;
}
ShipAddressParts p = new ShipAddressParts();
p.shipName = firstNonEmpty(data, "ship_name", "receiver_name", "consignee", "contact_name");
p.shipMobile = firstNonEmpty(data, "ship_mobile", "receiver_mobile", "receiver_phone", "contact_mobile");
p.shipAddress = firstNonEmpty(data, "ship_address", "receiver_address", "detail_address");
p.shipDistrictId = firstInt(data, "ship_district_id", "receiver_district_id");
p.shipProvName = firstNonEmpty(data, "ship_prov_name", "receiver_province", "prov");
p.shipCityName = firstNonEmpty(data, "ship_city_name", "receiver_city", "city");
p.shipAreaName = firstNonEmpty(data, "ship_area_name", "receiver_area", "area", "district");
JSONObject recv = data.getJSONObject("receiver");
if (recv != null) {
if (StringUtils.isEmpty(p.shipName)) {
p.shipName = recv.getString("name");
}
if (StringUtils.isEmpty(p.shipMobile)) {
p.shipMobile = recv.getString("mobile");
if (StringUtils.isEmpty(p.shipMobile)) {
p.shipMobile = recv.getString("phone");
}
}
if (StringUtils.isEmpty(p.shipAddress)) {
p.shipAddress = recv.getString("address");
}
}
return p;
}
private static String firstNonEmpty(JSONObject o, String... keys) {
if (o == null) {
return null;
}
for (String k : keys) {
String v = o.getString(k);
if (StringUtils.isNotEmpty(v)) {
return v.trim();
}
}
return null;
}
private static Integer firstInt(JSONObject o, String... keys) {
if (o == null) {
return null;
}
for (String k : keys) {
Integer v = o.getInteger(k);
if (v != null) {
return v;
}
}
return null;
}
public int pullForAppKey(String appKey, int lookbackHours) {
if (StringUtils.isEmpty(appKey)) {
return 0;
}
IERPAccount cred = erpAccountResolver.resolve(appKey);
long now = System.currentTimeMillis() / 1000;
long start = now - (long) lookbackHours * 3600L;
List<Long> authorizeIds = fetchAuthorizeIds(cred);
int saved = 0;
int maxPages = goofishProperties.getPullMaxPagesPerShop();
for (Long aid : authorizeIds) {
int page = 1;
while (page <= maxPages) {
OrderListQueryRequest q = new OrderListQueryRequest(cred);
q.setAuthorizeId(aid);
q.setUpdateTime(start, now);
q.setPage(page, 20);
String resp;
try {
resp = q.getResponseBody();
} catch (Exception ex) {
log.warn("闲管家拉单失败 aid={} page={} {}", aid, page, ex.getMessage());
break;
}
JSONObject root = JSON.parseObject(resp);
if (root == null || root.getIntValue("code") != 0) {
log.warn("闲管家拉单接口异常 aid={} page={} resp={}", aid, page, resp);
break;
}
JSONObject data = root.getJSONObject("data");
JSONArray list = data == null ? null : data.getJSONArray("list");
if (list == null || list.isEmpty()) {
break;
}
for (int i = 0; i < list.size(); i++) {
JSONObject item = list.getJSONObject(i);
applyListOrNotifyItem(appKey, item, item.toJSONString());
saved++;
}
if (list.size() < 20) {
break;
}
page++;
try {
Thread.sleep(200);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
}
return saved;
}
private List<Long> fetchAuthorizeIds(IERPAccount cred) {
List<Long> ids = new ArrayList<>();
try {
AuthorizeListQueryRequest auth = new AuthorizeListQueryRequest(cred);
auth.setPagination(1, 100);
String resp = auth.getResponseBody();
JSONObject root = JSON.parseObject(resp);
if (root == null || root.getIntValue("code") != 0) {
log.warn("授权列表查询异常 {}", resp);
return ids;
}
JSONObject data = root.getJSONObject("data");
JSONArray list = data == null ? null : data.getJSONArray("list");
if (list == null) {
return ids;
}
for (int i = 0; i < list.size(); i++) {
JSONObject it = list.getJSONObject(i);
Long aid = it.getLong("authorize_id");
if (aid == null) {
aid = it.getLong("authorizeId");
}
if (aid != null) {
ids.add(aid);
}
}
} catch (Exception e) {
log.warn("拉取授权列表异常 {}", e.getMessage());
}
return ids;
}
public int syncWaybillAndTryShipBatch(int limit) {
List<ErpGoofishOrder> rows = erpGoofishOrderMapper.selectPendingShip(limit);
if (rows == null) {
return 0;
}
int n = 0;
for (ErpGoofishOrder row : rows) {
ErpGoofishOrder full = erpGoofishOrderMapper.selectById(row.getId());
if (full == null) {
continue;
}
syncWaybillFromRedis(full);
tryAutoShip(full);
n++;
}
return n;
}
}

View File

@@ -0,0 +1,120 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
import com.ruoyi.jarvis.domain.ErpGoofishOrder;
import com.ruoyi.jarvis.domain.ErpOpenConfig;
import com.ruoyi.jarvis.dto.GoofishNotifyMessage;
import com.ruoyi.jarvis.mapper.ErpGoofishOrderMapper;
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
import com.ruoyi.jarvis.service.IErpOpenConfigService;
import com.ruoyi.jarvis.service.goofish.GoofishNotifyAsyncFacade;
import com.ruoyi.jarvis.service.goofish.GoofishOrderPipeline;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class ErpGoofishOrderServiceImpl implements IErpGoofishOrderService {
@Autowired
private ObjectProvider<RocketMQTemplate> rocketMQTemplate;
@Resource
private JarvisGoofishProperties goofishProperties;
@Resource
private GoofishNotifyAsyncFacade goofishNotifyAsyncFacade;
@Resource
private GoofishOrderPipeline goofishOrderPipeline;
@Resource
private ErpGoofishOrderMapper erpGoofishOrderMapper;
@Resource
private IErpOpenConfigService erpOpenConfigService;
@Override
public void publishOrProcessNotify(String appid, Long timestamp, JSONObject body) {
RocketMQTemplate mq = rocketMQTemplate.getIfAvailable();
if (mq != null) {
GoofishNotifyMessage m = new GoofishNotifyMessage();
m.setAppid(appid);
m.setTimestamp(timestamp);
m.setBody(body.toJSONString());
mq.syncSend(goofishProperties.getMqTopic(), JSON.toJSONString(m));
} else {
goofishNotifyAsyncFacade.afterNotify(appid, body);
}
}
@Override
public void asyncPipelineAfterNotify(String appid, JSONObject notifyBody) {
goofishOrderPipeline.runFullPipeline(appid, notifyBody);
}
@Override
public List<ErpGoofishOrder> selectList(ErpGoofishOrder query) {
return erpGoofishOrderMapper.selectList(query);
}
@Override
public ErpGoofishOrder selectById(Long id) {
return erpGoofishOrderMapper.selectById(id);
}
@Override
public int pullOrdersForAppKey(String appKey, int lookbackHours) {
return goofishOrderPipeline.pullForAppKey(appKey, lookbackHours);
}
@Override
public int pullAllEnabled(int lookbackHours) {
int total = 0;
List<ErpOpenConfig> cfgs = erpOpenConfigService.selectEnabledOrderBySort();
if (cfgs == null) {
return 0;
}
for (ErpOpenConfig c : cfgs) {
if (c.getAppKey() == null) {
continue;
}
total += goofishOrderPipeline.pullForAppKey(c.getAppKey(), lookbackHours);
}
return total;
}
@Override
public void refreshDetail(Long id) {
ErpGoofishOrder row = erpGoofishOrderMapper.selectById(id);
if (row != null) {
goofishOrderPipeline.refreshDetail(row);
}
}
@Override
public void retryShip(Long id) {
erpGoofishOrderMapper.resetShipForRetry(id);
ErpGoofishOrder row = erpGoofishOrderMapper.selectById(id);
if (row != null) {
goofishOrderPipeline.syncWaybillFromRedis(row);
goofishOrderPipeline.tryAutoShip(row);
}
}
@Override
public int syncWaybillAndTryShipBatch(int limit) {
return goofishOrderPipeline.syncWaybillAndTryShipBatch(limit);
}
@Override
public void applyListOrNotifyItem(String appKey, JSONObject item, String lastNotifyJson) {
goofishOrderPipeline.applyListOrNotifyItem(appKey, item, lastNotifyJson);
}
}

View File

@@ -0,0 +1,63 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.jarvis.domain.ErpOpenConfig;
import com.ruoyi.jarvis.mapper.ErpOpenConfigMapper;
import com.ruoyi.jarvis.service.IErpOpenConfigService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class ErpOpenConfigServiceImpl implements IErpOpenConfigService {
@Resource
private ErpOpenConfigMapper erpOpenConfigMapper;
@Override
public ErpOpenConfig selectById(Long id) {
return erpOpenConfigMapper.selectById(id);
}
@Override
public ErpOpenConfig selectByAppKey(String appKey) {
if (appKey == null) {
return null;
}
return erpOpenConfigMapper.selectByAppKey(appKey.trim());
}
@Override
public ErpOpenConfig selectFirstEnabled() {
List<ErpOpenConfig> list = erpOpenConfigMapper.selectEnabledOrderBySort();
return list == null || list.isEmpty() ? null : list.get(0);
}
@Override
public List<ErpOpenConfig> selectList(ErpOpenConfig query) {
return erpOpenConfigMapper.selectList(query);
}
@Override
public List<ErpOpenConfig> selectEnabledOrderBySort() {
return erpOpenConfigMapper.selectEnabledOrderBySort();
}
@Override
public int insert(ErpOpenConfig row) {
row.setCreateTime(DateUtils.getNowDate());
return erpOpenConfigMapper.insert(row);
}
@Override
public int update(ErpOpenConfig row) {
row.setUpdateTime(DateUtils.getNowDate());
return erpOpenConfigMapper.update(row);
}
@Override
public int deleteById(Long id) {
return erpOpenConfigMapper.deleteById(id);
}
}

View File

@@ -7,7 +7,8 @@ import org.springframework.stereotype.Service;
import com.ruoyi.jarvis.mapper.ErpProductMapper;
import com.ruoyi.jarvis.domain.ErpProduct;
import com.ruoyi.jarvis.service.IErpProductService;
import com.ruoyi.erp.request.ERPAccount;
import com.ruoyi.erp.request.IERPAccount;
import com.ruoyi.jarvis.service.erp.ErpAccountResolver;
import com.ruoyi.erp.request.ProductListQueryRequest;
import com.ruoyi.erp.request.ProductDeleteRequest;
import com.alibaba.fastjson2.JSONArray;
@@ -32,6 +33,9 @@ public class ErpProductServiceImpl implements IErpProductService
@Autowired
private ErpProductMapper erpProductMapper;
@Autowired
private ErpAccountResolver erpAccountResolver;
/**
* 查询闲鱼商品
*
@@ -126,7 +130,7 @@ public class ErpProductServiceImpl implements IErpProductService
{
try {
// 解析ERP账号
ERPAccount account = resolveAccount(appid);
IERPAccount account = erpAccountResolver.resolve(appid);
// 创建查询请求
ProductListQueryRequest request = new ProductListQueryRequest(account);
@@ -319,7 +323,7 @@ public class ErpProductServiceImpl implements IErpProductService
int updated = 0;
try {
ERPAccount account = resolveAccount(appid);
IERPAccount account = erpAccountResolver.resolve(appid);
// 第一步:遍历所有页码,拉取并保存所有商品
log.info("开始全量同步商品,账号:{}", appid);
@@ -463,18 +467,5 @@ public class ErpProductServiceImpl implements IErpProductService
}
}
/**
* 解析ERP账号
*/
private ERPAccount resolveAccount(String appid) {
if (appid != null && !appid.isEmpty()) {
for (ERPAccount account : ERPAccount.values()) {
if (account.getApiKey().equals(appid)) {
return account;
}
}
}
return ERPAccount.ACCOUNT_HUGE; // 默认账号
}
}

View File

@@ -0,0 +1,63 @@
package com.ruoyi.jarvis.task;
import com.ruoyi.jarvis.config.JarvisGoofishProperties;
import com.ruoyi.jarvis.domain.ErpOpenConfig;
import com.ruoyi.jarvis.service.IErpGoofishOrderService;
import com.ruoyi.jarvis.service.IErpOpenConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 闲管家 ERP定时拉单与运单同步/自动发货
*/
@Component
public class GoofishScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(GoofishScheduledTasks.class);
@Resource
private IErpOpenConfigService erpOpenConfigService;
@Resource
private IErpGoofishOrderService erpGoofishOrderService;
@Resource
private JarvisGoofishProperties goofishProperties;
@Scheduled(cron = "${jarvis.goofish-order.pull-cron:0 0/15 * * * ?}")
public void scheduledPull() {
List<ErpOpenConfig> cfgs = erpOpenConfigService.selectEnabledOrderBySort();
if (cfgs == null || cfgs.isEmpty()) {
return;
}
int hours = goofishProperties.getPullLookbackHours();
int n = 0;
for (ErpOpenConfig c : cfgs) {
try {
n += erpGoofishOrderService.pullOrdersForAppKey(c.getAppKey(), hours);
} catch (Exception e) {
log.warn("定时拉单失败 appKey={} {}", c.getAppKey(), e.getMessage());
}
}
if (n > 0) {
log.info("闲管家定时拉单本轮处理约 {} 条子订单项", n);
}
}
@Scheduled(cron = "${jarvis.goofish-order.auto-ship-cron:0 2/10 * * * ?}")
public void scheduledWaybillAndShip() {
try {
int k = erpGoofishOrderService.syncWaybillAndTryShipBatch(goofishProperties.getAutoShipBatchSize());
if (k > 0) {
log.info("闲管家运单同步与发货扫描处理 {} 条", k);
}
} catch (Exception e) {
log.warn("闲管家自动发货扫描异常 {}", e.getMessage());
}
}
}