Compare commits

..

72 Commits

Author SHA1 Message Date
Leo
632b9f7eb1 1 2025-12-06 17:08:38 +08:00
Leo
eb53915bcd 1 2025-12-05 22:48:22 +08:00
Leo
4dd3e9dd70 1 2025-12-05 22:35:55 +08:00
Leo
9206824efb 1 2025-12-05 22:20:17 +08:00
Leo
2524461ff4 1 2025-12-05 22:16:25 +08:00
Leo
7581cc02a9 1 2025-12-02 17:39:27 +08:00
Leo
1dc91a6bb0 1 2025-12-02 01:41:51 +08:00
Leo
6b3c2b17c8 1 2025-11-29 23:39:40 +08:00
Leo
e890b18e3e 1 2025-11-29 23:28:53 +08:00
Leo
9b2b770e29 1 2025-11-26 15:01:30 +08:00
Leo
047575ea42 1 2025-11-25 21:27:15 +08:00
Leo
702463b856 1 2025-11-25 18:56:37 +08:00
Leo
3aa3da8ade 1 2025-11-24 19:02:07 +08:00
Leo
20861d270a 1 2025-11-24 18:55:02 +08:00
Leo
e7c991ed9c 1 2025-11-21 23:26:29 +08:00
Leo
2ead103faa 1 2025-11-20 23:38:04 +08:00
Leo
c541beb413 1 2025-11-19 22:29:52 +08:00
Leo
083bcca270 1 2025-11-19 16:02:30 +08:00
Leo
35dcb20e4a 1 2025-11-19 15:58:40 +08:00
Leo
7648b934ed 1 2025-11-16 00:28:52 +08:00
Leo
01f0be6198 1 2025-11-16 00:12:07 +08:00
Leo
276fb49354 1 2025-11-15 23:59:36 +08:00
Leo
4f917dce10 1 2025-11-15 23:45:41 +08:00
Leo
98b56ab11b 1 2025-11-15 17:48:17 +08:00
Leo
b495431b7e 1 2025-11-15 17:42:56 +08:00
Leo
7f4b0dd986 1 2025-11-15 17:39:42 +08:00
Leo
79c5bf266f 1 2025-11-15 17:33:03 +08:00
Leo
04156492a6 1 2025-11-15 15:15:09 +08:00
Leo
f578b9b2c9 1 2025-11-15 15:08:02 +08:00
Leo
6b07fa1d75 1 2025-11-15 11:26:01 +08:00
Leo
978da7042d 1 2025-11-15 01:45:20 +08:00
Leo
66ac54ca70 1 2025-11-15 00:45:52 +08:00
Leo
026c6bf2a3 1 2025-11-14 23:55:59 +08:00
Leo
2b0587d4e1 1 2025-11-14 23:48:19 +08:00
Leo
0880628c93 1 2025-11-14 23:43:41 +08:00
Leo
2e59f49677 1 2025-11-14 23:42:26 +08:00
Leo
a54c8cc0cd 1 2025-11-14 00:13:18 +08:00
Leo
8a23c4d3f7 1 2025-11-14 00:02:40 +08:00
Leo
b8981ffc98 1 2025-11-13 23:55:25 +08:00
Leo
9e69230948 1 2025-11-13 23:54:14 +08:00
Leo
64ce923631 1 2025-11-13 23:51:44 +08:00
Leo
2cd3a0a798 1 2025-11-13 23:38:30 +08:00
Leo
8889791a83 1 2025-11-13 16:08:45 +08:00
Leo
e184c7926f 1 2025-11-13 11:38:26 +08:00
Leo
d73c7b6560 1 2025-11-11 18:18:19 +08:00
Leo
9d8f2ded0c 1 2025-11-11 14:13:11 +08:00
Leo
7294748ae9 1 2025-11-11 14:06:34 +08:00
Leo
142b395dbe 1 2025-11-11 12:41:21 +08:00
Leo
c8b15275a4 Revert "1"
This reverts commit e79e7081ee.
2025-11-11 12:30:45 +08:00
Leo
a61003fb7c 1 2025-11-11 00:42:05 +08:00
Leo
939d03e192 1 2025-11-11 00:24:09 +08:00
Leo
e2facc3099 1 2025-11-10 23:32:04 +08:00
Leo
af68b529b0 1 2025-11-10 21:21:01 +08:00
Leo
185483dace 1 2025-11-10 21:02:44 +08:00
Leo
e79e7081ee 1 2025-11-10 19:07:24 +08:00
Leo
3176e45057 1 2025-11-10 18:55:03 +08:00
Leo
72b3458ef9 1 2025-11-09 23:54:38 +08:00
Leo
00149dc198 1 2025-11-09 16:00:45 +08:00
Leo
10020e6d52 1 2025-11-09 15:59:42 +08:00
Leo
c0908690b4 1 2025-11-09 00:46:10 +08:00
Leo
70ea908c23 1 2025-11-09 00:43:36 +08:00
Leo
18d2fb8dee 1 2025-11-09 00:00:44 +08:00
Leo
a8c948e958 1 2025-11-08 15:46:40 +08:00
Leo
654a496478 1 2025-11-08 15:42:19 +08:00
Leo
79082adf8c 1 2025-11-08 15:33:06 +08:00
Leo
287bf75d77 1 2025-11-08 15:25:48 +08:00
8ba4c4e383 1 2025-11-07 21:24:20 +08:00
0b5f054286 1 2025-11-07 21:09:11 +08:00
652824b84a 1 2025-11-07 16:11:17 +08:00
d1a1100064 1 2025-11-07 15:59:52 +08:00
4430351e69 1 2025-11-07 15:54:16 +08:00
7b7f8de2de 1 2025-11-07 15:48:43 +08:00
33 changed files with 4599 additions and 433 deletions

View File

@@ -16,6 +16,7 @@ import com.ruoyi.erp.request.ProductCategoryListQueryRequest;
import com.ruoyi.erp.request.ProductPropertyListQueryRequest;
import com.ruoyi.erp.request.AuthorizeListQueryRequest;
import com.ruoyi.erp.request.ProductPublishRequest;
import com.ruoyi.erp.request.ProductDownShelfRequest;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@@ -550,6 +551,217 @@ public class ProductController extends BaseController {
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
/**
* 下架商品(单个)
*/
@PostMapping("/downShelf")
public R<?> downShelf(@RequestBody @Validated DownShelfRequest req) {
try {
ERPAccount account = resolveAccount(req.getAppid());
ProductDownShelfRequest downShelfRequest = new ProductDownShelfRequest(account);
downShelfRequest.setProductId(req.getProductId());
String resp = downShelfRequest.getResponseBody();
JSONObject jo = JSONObject.parseObject(resp);
return R.ok(jo);
} catch (Exception e) {
log.error("下架商品失败: productId={}", req.getProductId(), e);
return R.fail("下架失败: " + e.getMessage());
}
}
/**
* 批量上架商品
*/
@PostMapping("/batchPublish")
public R<?> batchPublish(@RequestBody @Validated BatchPublishRequest req) {
try {
ERPAccount account = resolveAccount(req.getAppid());
List<Long> productIds = req.getProductIds();
if (productIds == null || productIds.isEmpty()) {
return R.fail("商品ID列表不能为空");
}
if (req.getUserName() == null || req.getUserName().isEmpty()) {
return R.fail("闲鱼会员名不能为空");
}
List<HashMap<String, Object>> results = new ArrayList<>();
int successCount = 0;
int failCount = 0;
for (Long productId : productIds) {
HashMap<String, Object> result = new HashMap<>();
result.put("productId", productId);
try {
ProductPublishRequest publishRequest = new ProductPublishRequest(account);
publishRequest.setProductId(productId);
publishRequest.setUserName(req.getUserName());
if (req.getSpecifyPublishTime() != null) {
publishRequest.setSpecifyPublishTime(req.getSpecifyPublishTime());
}
String resp = publishRequest.getResponseBody();
JSONObject jo = JSONObject.parseObject(resp);
if (jo != null && jo.getInteger("code") != null && jo.getInteger("code") == 0) {
result.put("success", true);
result.put("msg", "上架成功");
result.put("response", jo);
successCount++;
} else {
result.put("success", false);
result.put("msg", jo != null ? jo.getString("msg") : "上架失败");
result.put("response", jo);
failCount++;
}
} catch (Exception e) {
log.error("批量上架商品失败: productId={}", productId, e);
result.put("success", false);
result.put("msg", "上架异常: " + e.getMessage());
result.put("response", null);
failCount++;
}
results.add(result);
}
HashMap<String, Object> summary = new HashMap<>();
summary.put("total", productIds.size());
summary.put("success", successCount);
summary.put("fail", failCount);
summary.put("results", results);
JSONObject response = new JSONObject();
response.put("code", failCount == 0 ? 0 : 500);
response.put("msg", failCount == 0 ? "全部上架成功" : String.format("成功: %d, 失败: %d", successCount, failCount));
response.put("data", summary);
return R.ok(response);
} catch (Exception e) {
log.error("批量上架商品异常", e);
return R.fail("批量上架失败: " + e.getMessage());
}
}
/**
* 批量下架商品
*/
@PostMapping("/batchDownShelf")
public R<?> batchDownShelf(@RequestBody @Validated BatchDownShelfRequest req) {
try {
ERPAccount account = resolveAccount(req.getAppid());
List<Long> productIds = req.getProductIds();
if (productIds == null || productIds.isEmpty()) {
return R.fail("商品ID列表不能为空");
}
List<HashMap<String, Object>> results = new ArrayList<>();
int successCount = 0;
int failCount = 0;
for (Long productId : productIds) {
HashMap<String, Object> result = new HashMap<>();
result.put("productId", productId);
try {
ProductDownShelfRequest downShelfRequest = new ProductDownShelfRequest(account);
downShelfRequest.setProductId(productId);
String resp = downShelfRequest.getResponseBody();
JSONObject jo = JSONObject.parseObject(resp);
if (jo != null && jo.getInteger("code") != null && jo.getInteger("code") == 0) {
result.put("success", true);
result.put("msg", "下架成功");
result.put("response", jo);
successCount++;
} else {
result.put("success", false);
result.put("msg", jo != null ? jo.getString("msg") : "下架失败");
result.put("response", jo);
failCount++;
}
} catch (Exception e) {
log.error("批量下架商品失败: productId={}", productId, e);
result.put("success", false);
result.put("msg", "下架异常: " + e.getMessage());
result.put("response", null);
failCount++;
}
results.add(result);
}
HashMap<String, Object> summary = new HashMap<>();
summary.put("total", productIds.size());
summary.put("success", successCount);
summary.put("fail", failCount);
summary.put("results", results);
JSONObject response = new JSONObject();
response.put("code", failCount == 0 ? 0 : 500);
response.put("msg", failCount == 0 ? "全部下架成功" : String.format("成功: %d, 失败: %d", successCount, failCount));
response.put("data", summary);
return R.ok(response);
} catch (Exception e) {
log.error("批量下架商品异常", e);
return R.fail("批量下架失败: " + e.getMessage());
}
}
/**
* 下架请求体
*/
public static class DownShelfRequest {
@NotNull
private Long productId;
private String appid;
public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
/**
* 批量上架请求体
*/
public static class BatchPublishRequest {
@NotNull
@Size(min = 1, message = "商品ID列表不能为空")
private List<Long> productIds;
@NotBlank(message = "闲鱼会员名不能为空")
private String userName;
private String specifyPublishTime;
private String appid;
public List<Long> getProductIds() { return productIds; }
public void setProductIds(List<Long> productIds) { this.productIds = productIds; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getSpecifyPublishTime() { return specifyPublishTime; }
public void setSpecifyPublishTime(String specifyPublishTime) { this.specifyPublishTime = specifyPublishTime; }
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
/**
* 批量下架请求体
*/
public static class BatchDownShelfRequest {
@NotNull
@Size(min = 1, message = "商品ID列表不能为空")
private List<Long> productIds;
private String appid;
public List<Long> getProductIds() { return productIds; }
public void setProductIds(List<Long> productIds) { this.productIds = productIds; }
public String getAppid() { return appid; }
public void setAppid(String appid) { this.appid = appid; }
}
}

View File

@@ -0,0 +1,234 @@
package com.ruoyi.web.controller.jarvis;
import java.util.List;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
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.service.IErpProductService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 闲鱼商品Controller
*
* @author ruoyi
* @date 2024-01-01
*/
@RestController
@RequestMapping("/jarvis/erpProduct")
public class ErpProductController extends BaseController
{
@Autowired
private IErpProductService erpProductService;
/**
* 查询闲鱼商品列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:list')")
@GetMapping("/list")
public TableDataInfo list(ErpProduct erpProduct)
{
startPage();
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
return getDataTable(list);
}
/**
* 导出闲鱼商品列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:export')")
@Log(title = "闲鱼商品", businessType = BusinessType.EXPORT)
@GetMapping("/export")
public AjaxResult export(ErpProduct erpProduct)
{
List<ErpProduct> list = erpProductService.selectErpProductList(erpProduct);
ExcelUtil<ErpProduct> util = new ExcelUtil<ErpProduct>(ErpProduct.class);
return util.exportExcel(list, "闲鱼商品数据");
}
/**
* 获取闲鱼商品详细信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:query')")
@GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id)
{
return success(erpProductService.selectErpProductById(id));
}
/**
* 新增闲鱼商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:add')")
@Log(title = "闲鱼商品", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody ErpProduct erpProduct)
{
return toAjax(erpProductService.insertErpProduct(erpProduct));
}
/**
* 修改闲鱼商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:edit')")
@Log(title = "闲鱼商品", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody ErpProduct erpProduct)
{
return toAjax(erpProductService.updateErpProduct(erpProduct));
}
/**
* 删除闲鱼商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:remove')")
@Log(title = "闲鱼商品", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids)
{
return toAjax(erpProductService.deleteErpProductByIds(ids));
}
/**
* 从闲鱼ERP拉取商品列表并保存单页保留用于兼容
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:pull')")
@Log(title = "拉取闲鱼商品", businessType = BusinessType.INSERT)
@PostMapping("/pull")
public AjaxResult pullProductList(
@RequestParam(required = false) String appid,
@RequestParam(defaultValue = "1") Integer pageNo,
@RequestParam(defaultValue = "50") Integer pageSize,
@RequestParam(required = false) Integer productStatus)
{
try {
int count = erpProductService.pullAndSaveProductList(appid, pageNo, pageSize, productStatus);
if (count > 0) {
return success("成功拉取并保存 " + count + " 个商品");
} else {
String statusText = getStatusText(productStatus);
String message = "拉取完成,但没有获取到商品数据";
if (productStatus != null) {
message += "(筛选条件:状态=" + statusText + "";
}
message += "。建议:使用全量同步功能自动遍历所有页码";
return success(message);
}
} catch (Exception e) {
return error("拉取商品列表失败: " + e.getMessage());
}
}
/**
* 全量同步商品(自动遍历所有页码,同步更新和删除)
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:pull')")
@Log(title = "全量同步闲鱼商品", businessType = BusinessType.UPDATE)
@PostMapping("/syncAll")
public AjaxResult syncAllProducts(
@RequestParam(required = false) String appid,
@RequestParam(required = false) Integer productStatus)
{
try {
IErpProductService.SyncResult result = erpProductService.syncAllProducts(appid, productStatus);
return success(result.getMessage());
} catch (Exception e) {
return error("全量同步失败: " + e.getMessage());
}
}
/**
* 获取状态文本(用于提示信息)
*/
private String getStatusText(Integer status) {
if (status == null) {
return "全部";
}
switch (status) {
case -1:
return "删除";
case 21:
return "待发布";
case 22:
return "销售中";
case 23:
return "已售罄";
case 31:
return "手动下架";
case 33:
return "售出下架";
case 36:
return "自动下架";
default:
return String.valueOf(status);
}
}
/**
* 批量上架商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:publish')")
@Log(title = "批量上架商品", businessType = BusinessType.UPDATE)
@PostMapping("/batchPublish")
public AjaxResult batchPublish(@RequestBody BatchOperationRequest request)
{
try {
return success("批量上架功能请调用 /erp/product/batchPublish 接口");
} catch (Exception e) {
return error("批量上架失败: " + e.getMessage());
}
}
/**
* 批量下架商品
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:downShelf')")
@Log(title = "批量下架商品", businessType = BusinessType.UPDATE)
@PostMapping("/batchDownShelf")
public AjaxResult batchDownShelf(@RequestBody BatchOperationRequest request)
{
try {
return success("批量下架功能请调用 /erp/product/batchDownShelf 接口");
} catch (Exception e) {
return error("批量下架失败: " + e.getMessage());
}
}
/**
* 批量操作请求体
*/
public static class BatchOperationRequest {
private java.util.List<Long> productIds;
private String appid;
public java.util.List<Long> getProductIds() {
return productIds;
}
public void setProductIds(java.util.List<Long> productIds) {
this.productIds = productIds;
}
public String getAppid() {
return appid;
}
public void setAppid(String appid) {
this.appid = appid;
}
}
}

View File

@@ -21,13 +21,15 @@ public class InstructionController extends BaseController {
}
/**
* 执行文本指令
* body: { command: "京今日统计" }
* 执行文本指令(控制台入口,需要权限)
* body: { command: "京今日统计", forceGenerate: false }
*/
@PostMapping("/execute")
public AjaxResult execute(@RequestBody Map<String, String> body) {
String cmd = body != null ? body.get("command") : null;
java.util.List<String> result = instructionService.execute(cmd);
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);
return AjaxResult.success(result);
}

View File

@@ -13,6 +13,8 @@ import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.service.IOrderRowsService;
import com.ruoyi.jarvis.service.IGiftCouponService;
import com.ruoyi.jarvis.domain.GiftCoupon;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -37,11 +39,24 @@ public class JDOrderController extends BaseController {
private final IJDOrderService jdOrderService;
private final IOrderRowsService orderRowsService;
private final IGiftCouponService giftCouponService;
private final ISysConfigService sysConfigService;
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]+)",
java.util.regex.Pattern.CASE_INSENSITIVE);
private static final java.util.regex.Pattern UJD_LINK_PATTERN = java.util.regex.Pattern.compile(
"^https?://u\\.jd\\.com/[A-Za-z0-9]+[A-Za-z0-9_-]*$",
java.util.regex.Pattern.CASE_INSENSITIVE);
private static final java.util.regex.Pattern JINGFEN_LINK_PATTERN = java.util.regex.Pattern.compile(
"^https?://jingfen\\.jd\\.com/detail/[A-Za-z0-9]+\\.html$",
java.util.regex.Pattern.CASE_INSENSITIVE);
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, IGiftCouponService giftCouponService) {
public JDOrderController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService,
IGiftCouponService giftCouponService, ISysConfigService sysConfigService) {
this.jdOrderService = jdOrderService;
this.orderRowsService = orderRowsService;
this.giftCouponService = giftCouponService;
this.sysConfigService = sysConfigService;
}
private final static String skey = "2192057370ef8140c201079969c956a3";
@@ -134,10 +149,16 @@ public class JDOrderController extends BaseController {
if (w instanceof JSONObject) {
JSONObject wj = (JSONObject) w;
Object content = wj.get("content");
if (content instanceof String) {
String cleaned = stripUrls((String) content);
wj.put("content", cleaned);
if (!(content instanceof String)) {
continue;
}
String type = wj.getString("type");
if ("通用文案".equals(type)) {
// 通用文案需要保留链接,用于后续替换/复制
continue;
}
String cleaned = stripUrls((String) content);
wj.put("content", cleaned);
}
}
}
@@ -543,8 +564,8 @@ public class JDOrderController extends BaseController {
}
/**
* 文本替换:根据选中的商品创建礼金并替换文案中的URL
* 入参:{ content, selectedProducts: [{skuId/materialUrl, amount, quantity, owner, skuName, originalUrl}], ... }
* 文本替换:为每个URL单独查询商品创建礼金券,然后替换
* 入参:{ content, amount, quantity, owner }
* 返回:替换后的文本和礼金信息
*/
@PostMapping("/replaceUrlsWithGiftCoupons")
@@ -555,115 +576,309 @@ public class JDOrderController extends BaseController {
return AjaxResult.error("content is required");
}
logger.info("文本URL替换请求 - content长度={}, 参数: materialUrl={}, skuId={}, amount={}, quantity={}, owner={}",
content.length(), body.get("materialUrl"), body.get("skuId"), body.get("amount"),
body.get("quantity"), body.get("owner"));
Object amountObj = body.get("amount");
Object quantityObj = body.get("quantity");
String owner = body.get("owner") != null ? String.valueOf(body.get("owner")) : "g";
double amount = amountObj != null ? Double.parseDouble(String.valueOf(amountObj)) : 1.8;
int quantity = quantityObj != null ? Integer.parseInt(String.valueOf(quantityObj)) : 12;
logger.info("文本URL替换请求 - content长度={}, amount={}, quantity={}, owner={}",
content.length(), amount, quantity, owner);
// 提取文本中的所有URL京东链接
java.util.List<String> urls = new java.util.ArrayList<>();
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile(
"(https?://[^\\s]+)|(u\\.jd\\.com/[^\\s]+)",
java.util.regex.Pattern.CASE_INSENSITIVE);
java.util.regex.Matcher matcher = urlPattern.matcher(content);
java.util.List<UrlSegment> urlSegments = new java.util.ArrayList<>();
java.util.regex.Matcher matcher = URL_DETECT_PATTERN.matcher(content);
while (matcher.find()) {
String url = matcher.group(0);
if (url != null && !url.trim().isEmpty()) {
urls.add(url.trim());
String segment = matcher.group(0);
UrlSegment urlInfo = parseUrlSegment(segment);
if (urlInfo != null) {
urlSegments.add(urlInfo);
}
}
logger.info("文本URL替换 - 提取到{}个URL", urls.size());
logger.info("文本URL替换 - 提取到{}个URL", urlSegments.size());
if (urls.isEmpty()) {
return AjaxResult.success(new JSONObject().fluentPut("replacedContent", content)
if (urlSegments.isEmpty()) {
return AjaxResult.success(new JSONObject()
.fluentPut("replacedContent", content)
.fluentPut("originalContent", content)
.fluentPut("replacements", new JSONArray())
.fluentPut("totalUrls", 0)
.fluentPut("replacedCount", 0)
.fluentPut("message", "未找到URL无需替换"));
}
// 批量创建礼金券
Map<String, Object> batchParams = new java.util.HashMap<>();
batchParams.put("materialUrl", body.get("materialUrl"));
batchParams.put("skuId", body.get("skuId"));
batchParams.put("amount", body.get("amount"));
batchParams.put("quantity", body.get("quantity"));
batchParams.put("batchSize", urls.size());
batchParams.put("owner", body.get("owner"));
batchParams.put("skuName", body.get("skuName"));
AjaxResult batchResult = batchCreateGiftCoupons(batchParams);
Integer code = (Integer) batchResult.get(AjaxResult.CODE_TAG);
if (code == null || code != 200) {
String msg = (String) batchResult.get(AjaxResult.MSG_TAG);
return AjaxResult.error("批量创建礼金失败: " + (msg != null ? msg : "未知错误"));
}
// 解析批量创建结果
Object data = batchResult.get(AjaxResult.DATA_TAG);
JSONObject batchData = null;
if (data instanceof JSONObject) {
batchData = (JSONObject) data;
} else if (data instanceof String) {
try {
batchData = JSON.parseObject((String) data);
} catch (Exception e) {
logger.error("解析批量创建结果失败", e);
return AjaxResult.error("解析批量创建结果失败");
}
}
if (batchData == null || !batchData.containsKey("results")) {
return AjaxResult.error("批量创建结果格式错误");
}
JSONArray results = batchData.getJSONArray("results");
if (results == null || results.size() != urls.size()) {
logger.warn("批量创建礼金数量不匹配 - 期望={}, 实际={}", urls.size(), results != null ? results.size() : 0);
}
if (results == null) {
return AjaxResult.error("批量创建结果为空");
}
// 替换文本中的URL
// 为每个URL单独处理查询商品 → 创建礼金 → 转链 → 替换
String replacedContent = content;
JSONArray replacementInfo = new JSONArray();
int minSize = Math.min(urls.size(), results.size());
for (int i = 0; i < minSize; i++) {
String originalUrl = urls.get(i);
JSONObject result = results.getJSONObject(i);
String newUrl = null;
if (Boolean.TRUE.equals(result.getBoolean("success"))) {
newUrl = result.getString("shortURL");
if (newUrl == null || newUrl.trim().isEmpty()) {
newUrl = originalUrl; // 如果转链失败保持原URL
}
} else {
newUrl = originalUrl; // 创建失败保持原URL
}
int successCount = 0;
// 替换文本中的URL支持多种格式
replacedContent = replacedContent.replace(originalUrl, newUrl != null ? newUrl : originalUrl);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", originalUrl);
info.put("newUrl", newUrl);
info.put("success", Boolean.TRUE.equals(result.getBoolean("success")));
info.put("giftCouponKey", result.getString("giftCouponKey"));
replacementInfo.add(info);
for (int i = 0; i < urlSegments.size(); i++) {
UrlSegment urlSegment = urlSegments.get(i);
logger.info("处理第{}/{}个URL: {}", i + 1, urlSegments.size(), urlSegment.urlPart);
JSONObject product = null; // 在try外部声明以便在catch中使用
try {
if (urlSegment.normalizedJdUrl == null) {
logger.warn("URL{}不是京东推广链接,跳过: {}", i + 1, urlSegment.original);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", "非京东推广链接或格式不支持");
replacementInfo.add(info);
continue;
}
// 1. 查询该URL的商品信息
String queryUrl = requestUrl + "generatePromotionContent";
JSONObject queryParam = new JSONObject();
queryParam.put("skey", skey);
queryParam.put("promotionContent", urlSegment.normalizedJdUrl);
String queryResult = HttpUtils.sendJsonPost(queryUrl, queryParam.toJSONString());
logger.debug("商品查询响应: {}", queryResult);
Object parsed = JSON.parse(queryResult);
// 解析商品信息
if (parsed instanceof JSONArray) {
JSONArray arr = (JSONArray) parsed;
if (arr.size() > 0 && arr.get(0) instanceof JSONObject) {
product = arr.getJSONObject(0);
}
} else if (parsed instanceof JSONObject) {
JSONObject obj = (JSONObject) parsed;
if (obj.get("list") instanceof JSONArray) {
JSONArray list = obj.getJSONArray("list");
if (list.size() > 0) product = list.getJSONObject(0);
} else if (obj.get("data") instanceof JSONArray) {
JSONArray data = obj.getJSONArray("data");
if (data.size() > 0) product = data.getJSONObject(0);
} else if (obj.containsKey("materialUrl") || obj.containsKey("skuName")) {
product = obj;
}
}
if (product == null || product.containsKey("error")) {
String errorMsg = "商品信息查询失败";
if (product != null) {
String apiError = product.getString("error");
String apiMessage = product.getString("message");
if (apiMessage != null && !apiMessage.trim().isEmpty()) {
errorMsg = apiMessage;
} else if (apiError != null && !apiError.trim().isEmpty()) {
errorMsg = apiError;
}
}
logger.warn("URL{}商品信息查询失败: {}", urlSegment.urlPart, errorMsg);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", errorMsg);
replacementInfo.add(info);
continue;
}
// 2. 为该商品创建礼金券
String createUrl = requestUrl + "createGiftCoupon";
JSONObject createParam = new JSONObject();
createParam.put("skey", skey);
createParam.put("amount", amount);
createParam.put("quantity", quantity);
createParam.put("skuName", product.getString("skuName"));
// 处理owner字段p -> pop, g -> g
String productOwner = product.getString("owner");
if (productOwner == null || productOwner.trim().isEmpty()) {
productOwner = owner; // 使用用户选择的
} else if ("p".equalsIgnoreCase(productOwner)) {
productOwner = "pop";
}
createParam.put("owner", productOwner);
// 设置skuId或materialUrl注意POP商品优先使用materialUrl或oriItemId
String skuId = product.getString("skuId");
String materialUrl = product.getString("materialUrl");
String oriItemId = product.getString("oriItemId");
// 判断skuId是否有效排除字符串"null"
boolean hasValidSkuId = skuId != null && !skuId.trim().isEmpty() &&
!"null".equalsIgnoreCase(skuId) &&
!"undefined".equalsIgnoreCase(skuId);
// 判断materialUrl是否有效
boolean hasValidMaterialUrl = materialUrl != null && !materialUrl.trim().isEmpty() &&
!"null".equalsIgnoreCase(materialUrl);
// 判断oriItemId是否有效
boolean hasValidOriItemId = oriItemId != null && !oriItemId.trim().isEmpty() &&
!"null".equalsIgnoreCase(oriItemId);
if ("pop".equalsIgnoreCase(productOwner)) {
// POP商品优先使用oriItemId或materialUrl
if (hasValidOriItemId) {
createParam.put("materialUrl", oriItemId);
logger.info("POP商品使用oriItemId: {}", oriItemId);
} else if (hasValidMaterialUrl) {
createParam.put("materialUrl", materialUrl);
logger.info("POP商品使用materialUrl: {}", materialUrl);
} else if (hasValidSkuId) {
createParam.put("skuId", skuId);
logger.info("POP商品使用skuId: {}", skuId);
} else {
logger.warn("POP商品{}缺少有效的oriItemId/materialUrl/skuId", product.getString("skuName"));
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", "POP商品信息不完整");
replacementInfo.add(info);
continue;
}
} else {
// 自营商品优先使用skuId
if (hasValidSkuId) {
createParam.put("skuId", skuId);
logger.info("自营商品使用skuId: {}", skuId);
} else if (hasValidMaterialUrl) {
createParam.put("materialUrl", materialUrl);
logger.info("自营商品使用materialUrl: {}", materialUrl);
} else {
logger.warn("自营商品{}缺少有效的skuId/materialUrl", product.getString("skuName"));
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", "自营商品信息不完整");
replacementInfo.add(info);
continue;
}
}
String createResult = HttpUtils.sendJsonPost(createUrl, createParam.toJSONString());
logger.debug("礼金创建响应: {}", createResult);
JSONObject createData = JSON.parseObject(createResult);
if (createData == null || createData.containsKey("error") ||
createData.getString("giftCouponKey") == null ||
createData.getString("giftCouponKey").trim().isEmpty()) {
String errorMsg = "礼金创建失败";
if (createData != null) {
String apiError = createData.getString("error");
String apiMessage = createData.getString("message");
if (apiMessage != null && !apiMessage.trim().isEmpty()) {
errorMsg = apiMessage;
} else if (apiError != null && !apiError.trim().isEmpty()) {
errorMsg = apiError;
}
}
logger.warn("URL{}礼金创建失败: {}", urlSegment.urlPart, errorMsg);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", errorMsg);
if (product != null && product.getString("skuName") != null) {
info.put("skuName", product.getString("skuName"));
}
replacementInfo.add(info);
continue;
}
String giftCouponKey = createData.getString("giftCouponKey");
// 3. 转链(带礼金)
String transferUrl = requestUrl + "transfer";
JSONObject transferParam = new JSONObject();
transferParam.put("skey", skey);
transferParam.put("materialUrl", urlSegment.normalizedJdUrl);
transferParam.put("giftCouponKey", giftCouponKey);
String transferResult = HttpUtils.sendJsonPost(transferUrl, transferParam.toJSONString());
logger.debug("转链响应: {}", transferResult);
JSONObject transferData = JSON.parseObject(transferResult);
if (transferData == null || transferData.containsKey("error") ||
transferData.getString("shortURL") == null ||
transferData.getString("shortURL").trim().isEmpty()) {
String errorMsg = "转链失败";
if (transferData != null) {
String apiError = transferData.getString("error");
String apiMessage = transferData.getString("message");
if (apiMessage != null && !apiMessage.trim().isEmpty()) {
errorMsg = apiMessage;
} else if (apiError != null && !apiError.trim().isEmpty()) {
errorMsg = apiError;
}
}
logger.warn("URL{}转链失败: {}", urlSegment.urlPart, errorMsg);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("giftCouponKey", giftCouponKey);
info.put("error", errorMsg);
if (product != null && product.getString("skuName") != null) {
info.put("skuName", product.getString("skuName"));
}
replacementInfo.add(info);
continue;
}
String shortURL = transferData.getString("shortURL");
// 4. 替换文本中的URL
replacedContent = replaceUrlInContent(replacedContent, urlSegment, shortURL);
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", shortURL);
info.put("success", true);
info.put("giftCouponKey", giftCouponKey);
info.put("skuName", product.getString("skuName"));
replacementInfo.add(info);
successCount++;
logger.info("URL{}处理成功: {} -> {}", i + 1, urlSegment.urlPart, shortURL);
} catch (Exception e) {
logger.error("处理URL{}失败: {}", urlSegment.urlPart, e.getMessage(), e);
String errorMsg = e.getMessage();
if (errorMsg == null || errorMsg.trim().isEmpty()) {
errorMsg = "处理失败: " + e.getClass().getSimpleName();
}
JSONObject info = new JSONObject();
info.put("index", i + 1);
info.put("originalUrl", urlSegment.urlPart);
info.put("newUrl", urlSegment.urlPart);
info.put("success", false);
info.put("error", errorMsg);
if (product != null && product.getString("skuName") != null) {
info.put("skuName", product.getString("skuName"));
}
replacementInfo.add(info);
}
}
logger.info("文本URL替换完成 - 替换了{}个URL", minSize);
logger.info("文本URL替换完成 - 成功{}/{}", successCount, urlSegments.size());
JSONObject response = new JSONObject();
response.put("replacedContent", replacedContent);
response.put("originalContent", content);
response.put("replacements", replacementInfo);
response.put("totalUrls", urls.size());
response.put("replacedCount", minSize);
response.put("totalUrls", urlSegments.size());
response.put("replacedCount", successCount);
return AjaxResult.success(response);
} catch (Exception e) {
@@ -702,9 +917,7 @@ public class JDOrderController extends BaseController {
// 检查分销标识
String distributionMark = order.getDistributionMark();
if (distributionMark == null || (!distributionMark.equals("F") && !distributionMark.equals("PDD"))) {
return AjaxResult.error("该订单的分销标识不是F或PDD无需处理。当前分销标识: " + distributionMark);
}
// 检查物流链接
String logisticsLink = order.getLogisticsLink();
@@ -810,6 +1023,43 @@ public class JDOrderController extends BaseController {
}
}
/**
* 根据分销标识获取接收人列表
* 从系统配置中读取配置键名格式logistics.push.touser.{分销标识}
* 配置值格式接收人1,接收人2,接收人3逗号分隔
*
* @param distributionMark 分销标识
* @return 接收人列表逗号分隔如果未配置则返回null
*/
private String getTouserByDistributionMark(String distributionMark) {
if (!StringUtils.hasText(distributionMark)) {
logger.warn("分销标识为空,无法获取接收人配置");
return null;
}
try {
// 构建配置键名
String configKey = CONFIG_KEY_PREFIX + distributionMark.trim();
// 从系统配置中获取接收人列表
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;
}
} catch (Exception e) {
logger.error("获取接收人配置失败 - 分销标识: {}, 错误: {}", distributionMark, e.getMessage(), e);
return null;
}
}
/**
* 调用企业应用推送逻辑
* @param order 订单信息
@@ -838,7 +1088,7 @@ public class JDOrderController extends BaseController {
// 收货地址
pushContent.append("收货地址:").append(order.getAddress() != null ? order.getAddress() : "").append("\n");
// 运单号
pushContent.append("运单号:").append(waybillNo).append("\n");
pushContent.append("运单号:").append("\n").append("\n").append("\n").append("\n").append(waybillNo).append("\n");
// 调用企业微信推送接口参考WxtsUtil的实现
String pushUrl = "https://wxts.van333.cn/wx/send/pdd";
@@ -848,8 +1098,23 @@ public class JDOrderController extends BaseController {
String content = pushContent.toString();
pushParam.put("text", content);
// 根据分销标识获取接收人列表
String touser = getTouserByDistributionMark(distributionMark);
if (StringUtils.hasText(touser)) {
pushParam.put("touser", touser);
logger.info("企业微信推送设置接收人 - 订单ID: {}, 分销标识: {}, 接收人: {}",
order.getId(), distributionMark, touser);
} else {
logger.warn("未找到分销标识对应的接收人配置 - 订单ID: {}, 分销标识: {}",
order.getId(), distributionMark);
}
// 记录完整的推送参数(用于调试)
String jsonBody = pushParam.toJSONString();
logger.info("企业微信推送完整参数 - 订单ID: {}, JSON: {}", order.getId(), jsonBody);
// 使用支持自定义header的HTTP请求
String pushResult = sendPostWithHeaders(pushUrl, pushParam.toJSONString(), token);
String pushResult = sendPostWithHeaders(pushUrl, jsonBody, token);
logger.info("企业应用推送调用结果 - 订单ID: {}, waybill_no: {}, 推送结果: {}",
order.getId(), waybillNo, pushResult);
@@ -926,4 +1191,80 @@ public class JDOrderController extends BaseController {
}
return result.toString();
}
private static class UrlSegment {
private final String original;
private final String urlPart;
private final String suffix;
private final String normalizedJdUrl;
private UrlSegment(String original, String urlPart, String suffix, String normalizedJdUrl) {
this.original = original;
this.urlPart = urlPart;
this.suffix = suffix;
this.normalizedJdUrl = normalizedJdUrl;
}
}
private static UrlSegment parseUrlSegment(String segment) {
if (segment == null || segment.trim().isEmpty()) {
return null;
}
String original = segment.trim();
int urlLength = calculateUrlLength(original);
if (urlLength <= 0) {
return null;
}
String urlPart = original.substring(0, urlLength);
String suffix = original.substring(urlLength);
String normalized = normalizeJdUrl(urlPart);
return new UrlSegment(original, urlPart, suffix, normalized);
}
private static int calculateUrlLength(String text) {
int length = 0;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (isAllowedUrlChar(c)) {
length++;
} else {
break;
}
}
return length;
}
private static boolean isAllowedUrlChar(char c) {
if (c <= 0x7F) {
return Character.isLetterOrDigit(c) || "-._~:/?#[]@!$&'()*+,;=%".indexOf(c) >= 0;
}
return false;
}
private static String normalizeJdUrl(String urlPart) {
if (urlPart == null || urlPart.isEmpty()) {
return null;
}
String normalized = urlPart;
if (!normalized.startsWith("http://") && !normalized.startsWith("https://")) {
normalized = "https://" + normalized;
}
if (UJD_LINK_PATTERN.matcher(normalized).matches() || JINGFEN_LINK_PATTERN.matcher(normalized).matches()) {
return normalized;
}
return null;
}
private static String replaceUrlInContent(String content, UrlSegment segment, String newUrl) {
if (content == null || segment == null || newUrl == null || newUrl.trim().isEmpty()) {
return content;
}
String suffix = segment.suffix != null ? segment.suffix : "";
String replacementWithSuffix = newUrl + suffix;
String result = content.replace(segment.original, replacementWithSuffix);
if (result.equals(content) && !segment.original.equals(segment.urlPart)) {
result = result.replace(segment.urlPart, newUrl);
}
return result;
}
}

View File

@@ -0,0 +1,159 @@
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.enums.BusinessType;
import com.ruoyi.jarvis.service.ISocialMediaService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 小红书/抖音内容生成Controller
*
* @author ruoyi
* @date 2025-01-XX
*/
@RestController
@RequestMapping("/jarvis/social-media")
public class SocialMediaController extends BaseController
{
@Autowired
private ISocialMediaService socialMediaService;
/**
* 提取关键词
*/
@PostMapping("/extract-keywords")
public AjaxResult extractKeywords(@RequestBody Map<String, Object> request)
{
try {
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
return AjaxResult.error("商品名称不能为空");
}
Map<String, Object> result = socialMediaService.extractKeywords(productName);
return AjaxResult.success(result);
} catch (Exception e) {
logger.error("提取关键词失败", e);
return AjaxResult.error("提取关键词失败: " + e.getMessage());
}
}
/**
* 生成文案
*/
@PostMapping("/generate-content")
public AjaxResult generateContent(@RequestBody Map<String, Object> request)
{
try {
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
return AjaxResult.error("商品名称不能为空");
}
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String keywords = (String) request.get("keywords");
String style = (String) request.getOrDefault("style", "both");
Map<String, Object> result = socialMediaService.generateContent(
productName, originalPriceObj, finalPriceObj, keywords, style
);
return AjaxResult.success(result);
} catch (Exception e) {
logger.error("生成文案失败", e);
return AjaxResult.error("生成文案失败: " + e.getMessage());
}
}
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*/
@Log(title = "小红书/抖音内容生成", businessType = BusinessType.OTHER)
@PostMapping("/generate-complete")
public AjaxResult generateComplete(@RequestBody Map<String, Object> request)
{
try {
String productImageUrl = (String) request.get("productImageUrl");
String productName = (String) request.get("productName");
if (productName == null || productName.trim().isEmpty()) {
return AjaxResult.error("商品名称不能为空");
}
Object originalPriceObj = request.get("originalPrice");
Object finalPriceObj = request.get("finalPrice");
String style = (String) request.getOrDefault("style", "both");
Map<String, Object> result = socialMediaService.generateCompleteContent(
productImageUrl, productName, originalPriceObj, finalPriceObj, style
);
return AjaxResult.success(result);
} catch (Exception e) {
logger.error("生成完整内容失败", e);
return AjaxResult.error("生成完整内容失败: " + e.getMessage());
}
}
/**
* 获取提示词模板列表
*/
@GetMapping("/prompt/list")
public AjaxResult listPromptTemplates()
{
try {
return socialMediaService.listPromptTemplates();
} catch (Exception e) {
logger.error("获取提示词模板列表失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个提示词模板
*/
@GetMapping("/prompt/{key}")
public AjaxResult getPromptTemplate(@PathVariable String key)
{
try {
return socialMediaService.getPromptTemplate(key);
} catch (Exception e) {
logger.error("获取提示词模板失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 保存提示词模板
*/
@Log(title = "保存提示词模板", businessType = BusinessType.UPDATE)
@PostMapping("/prompt/save")
public AjaxResult savePromptTemplate(@RequestBody Map<String, Object> request)
{
try {
return socialMediaService.savePromptTemplate(request);
} catch (Exception e) {
logger.error("保存提示词模板失败", e);
return AjaxResult.error("保存失败: " + e.getMessage());
}
}
/**
* 删除提示词模板(恢复默认)
*/
@Log(title = "删除提示词模板", businessType = BusinessType.DELETE)
@DeleteMapping("/prompt/{key}")
public AjaxResult deletePromptTemplate(@PathVariable String key)
{
try {
return socialMediaService.deletePromptTemplate(key);
} catch (Exception e) {
logger.error("删除提示词模板失败", e);
return AjaxResult.error("删除失败: " + e.getMessage());
}
}
}

View File

@@ -485,7 +485,7 @@ public class TencentDocController extends BaseController {
// 2. 检查订单是否已推送(防止重复推送)
JDOrder order = jdOrderService.selectJDOrderByThirdPartyOrderNo(thirdPartyOrderNo);
if (order == null) {
logOperation(null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "订单不存在");
return AjaxResult.error("订单不存在:" + thirdPartyOrderNo);
}
@@ -494,7 +494,7 @@ public class TencentDocController extends BaseController {
if (order.getTencentDocPushed() != null && order.getTencentDocPushed() == 1 && !forceRePush) {
String pushTimeStr = order.getTencentDocPushTime() != null ?
new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(order.getTencentDocPushTime()) : "未知";
logOperation(null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"SKIPPED", String.format("订单已推送,推送时间: %s", pushTimeStr));
return AjaxResult.error(String.format("该订单已推送到腾讯文档(推送时间:%s请勿重复操作如需重新推送请使用强制推送功能。", pushTimeStr));
}
@@ -504,7 +504,7 @@ public class TencentDocController extends BaseController {
try {
accessToken = tencentDocTokenService.getValidAccessToken();
} catch (Exception e) {
logOperation(null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, null, null, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "访问令牌无效: " + e.getMessage());
return AjaxResult.error("访问令牌无效,请先完成授权");
}
@@ -530,7 +530,7 @@ public class TencentDocController extends BaseController {
}
if (fileId == null || fileId.isEmpty() || sheetId == null || sheetId.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "文档配置不完整");
return AjaxResult.error("文档配置不完整,请先完成配置");
}
@@ -544,7 +544,7 @@ public class TencentDocController extends BaseController {
if (!lockAcquired) {
log.warn("获取锁失败,该订单正在被其他请求处理:{}", thirdPartyOrderNo);
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "获取分布式锁失败,该订单正在处理中");
return AjaxResult.error("该订单正在处理中,请稍后再试");
}
@@ -558,7 +558,7 @@ public class TencentDocController extends BaseController {
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);
if (headerData == null || !headerData.containsKey("gridData")) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "无法读取表头数据");
return AjaxResult.error("无法读取表头数据");
}
@@ -567,7 +567,7 @@ public class TencentDocController extends BaseController {
JSONObject gridData = headerData.getJSONObject("gridData");
JSONArray rows = gridData.getJSONArray("rows");
if (rows == null || rows.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "表头数据为空");
return AjaxResult.error("表头数据为空");
}
@@ -593,7 +593,7 @@ public class TencentDocController extends BaseController {
}
if (orderNoColumn == -1 || logisticsColumn == -1) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "未找到'单号'或'物流'列");
return AjaxResult.error("未找到'单号'或'物流'列,请检查表头配置");
}
@@ -605,7 +605,7 @@ public class TencentDocController extends BaseController {
JSONObject data = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange);
if (data == null || !data.containsKey("gridData")) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "无法读取数据区域");
return AjaxResult.error("无法读取数据区域");
}
@@ -614,7 +614,7 @@ public class TencentDocController extends BaseController {
JSONArray dataRows = dataGridData.getJSONArray("rows");
if (dataRows == null || dataRows.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "数据区域为空");
return AjaxResult.error("数据区域为空");
}
@@ -638,7 +638,7 @@ public class TencentDocController extends BaseController {
}
if (targetRow == -1) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, logisticsLink,
"FAILED", "未找到单号");
return AjaxResult.error("未找到单号: " + thirdPartyOrderNo);
}
@@ -650,7 +650,7 @@ public class TencentDocController extends BaseController {
JSONObject verifyData = tencentDocService.readSheetData(accessToken, fileId, sheetId, verifyRange);
if (verifyData == null || !verifyData.containsKey("gridData")) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "验证读取失败");
return AjaxResult.error("验证读取失败");
}
@@ -659,7 +659,7 @@ public class TencentDocController extends BaseController {
JSONArray verifyRows = verifyGridData.getJSONArray("rows");
if (verifyRows == null || verifyRows.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "验证行数据为空");
return AjaxResult.error("验证行数据为空");
}
@@ -669,7 +669,7 @@ public class TencentDocController extends BaseController {
// 验证单号是否仍然匹配
if (verifyCells == null || verifyCells.size() <= orderNoColumn) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SKIPPED", "验证时单号列为空,行已变化");
return AjaxResult.error("验证失败:行数据已变化,单号列为空");
}
@@ -681,7 +681,7 @@ public class TencentDocController extends BaseController {
}
if (!thirdPartyOrderNo.equals(verifyOrderNo)) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SKIPPED", String.format("验证失败:单号不匹配,期望 %s实际 %s", thirdPartyOrderNo, verifyOrderNo));
return AjaxResult.error("验证失败:单号不匹配,行数据已变化");
}
@@ -692,7 +692,7 @@ public class TencentDocController extends BaseController {
if (logisticsCell.containsKey("cellValue")) {
String existingLogistics = logisticsCell.getJSONObject("cellValue").getString("text");
if (existingLogistics != null && !existingLogistics.trim().isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SKIPPED", String.format("物流链接列已有值:%s", existingLogistics));
return AjaxResult.error(String.format("该订单物流链接已存在:%s", existingLogistics));
}
@@ -705,13 +705,13 @@ public class TencentDocController extends BaseController {
JSONObject userInfo = com.ruoyi.jarvis.util.TencentDocApiUtil.getUserInfo(accessToken);
JSONObject userData = userInfo.getJSONObject("data");
if (userData == null) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "无法获取用户数据");
return AjaxResult.error("无法获取用户数据");
}
String openId = userData.getString("openID");
if (openId == null || openId.isEmpty()) {
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"FAILED", "无法获取Open-Id");
return AjaxResult.error("无法获取Open-Id");
}
@@ -729,7 +729,7 @@ public class TencentDocController extends BaseController {
);
// 12. 记录成功日志
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, targetRow, logisticsLink,
"SUCCESS", null);
log.info("✓ 物流链接写入成功 - 单号: {}, 行: {}, 链接: {}", thirdPartyOrderNo, targetRow, logisticsLink);
@@ -757,7 +757,7 @@ public class TencentDocController extends BaseController {
} catch (Exception e) {
log.error("填充物流链接失败", e);
logOperation(fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, null,
logOperation(null, fileId, sheetId, "WRITE_SINGLE", thirdPartyOrderNo, null, null,
"FAILED", "异常: " + e.getMessage());
return AjaxResult.error("填充物流链接失败: " + e.getMessage());
} finally {
@@ -776,11 +776,12 @@ public class TencentDocController extends BaseController {
/**
* 记录操作日志到数据库
*/
private void logOperation(String fileId, String sheetId, String operationType,
private void logOperation(String batchId, String fileId, String sheetId, String operationType,
String orderNo, Integer targetRow, String logisticsLink,
String status, String errorMessage) {
try {
com.ruoyi.jarvis.domain.TencentDocOperationLog log = new com.ruoyi.jarvis.domain.TencentDocOperationLog();
log.setBatchId(batchId);
log.setFileId(fileId);
log.setSheetId(sheetId);
log.setOperationType(operationType);
@@ -863,6 +864,7 @@ public class TencentDocController extends BaseController {
* 优化:记录上次处理的最大行数,每次从最大行数-100开始读取避免重复处理历史数据
* 自动获取和管理访问令牌点击同步时自动刷新token
*/
@Anonymous
@PostMapping("/fillLogisticsByOrderNo")
public AjaxResult fillLogisticsByOrderNo(@RequestBody Map<String, Object> params) {
try {
@@ -881,6 +883,9 @@ public class TencentDocController extends BaseController {
}
}
// 从参数获取批次ID如果是批量调用会传入
String batchId = params.get("batchId") != null ? String.valueOf(params.get("batchId")) : null;
// 从参数或配置中获取文档信息
String fileId = (String) params.get("fileId");
String sheetId = (String) params.get("sheetId");
@@ -1193,7 +1198,7 @@ public class TencentDocController extends BaseController {
}
// 记录同步日志
logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink,
logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, excelRow, existingLogisticsLink,
"SKIPPED", "文档中已有物流链接,已同步订单状态");
}
} catch (Exception e) {
@@ -1396,7 +1401,7 @@ public class TencentDocController extends BaseController {
}
// 记录操作日志
logOperation(fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, logisticsLink,
logOperation(batchId, fileId, sheetId, "BATCH_SYNC", expectedOrderNo, row, logisticsLink,
"SUCCESS", null);
} catch (Exception e) {
log.error("写入数据失败 - 行: {}", entry.getKey(), e);
@@ -1405,7 +1410,7 @@ public class TencentDocController extends BaseController {
// 记录失败日志
String orderNo = entry.getValue().getString("orderNo");
String logisticsLink = entry.getValue().getString("logisticsLink");
logOperation(fileId, sheetId, "BATCH_SYNC", orderNo, entry.getKey(), logisticsLink,
logOperation(batchId, fileId, sheetId, "BATCH_SYNC", orderNo, entry.getKey(), logisticsLink,
"FAILED", "写入异常: " + e.getMessage());
}
@@ -1675,5 +1680,352 @@ public class TencentDocController extends BaseController {
return AjaxResult.error("取消待推送任务失败: " + e.getMessage());
}
}
/**
* 反向同步第三方单号
* 从腾讯文档的物流单号列读取链接,通过链接匹配本地订单,将腾讯文档的单号列值写入到订单的第三方单号字段
*
* @param params 包含 fileId, sheetId, startRow起始行默认850
* @return 同步结果
*/
@PostMapping("/reverseSyncThirdPartyOrderNo")
public AjaxResult reverseSyncThirdPartyOrderNo(@RequestBody Map<String, Object> params) {
String batchId = java.util.UUID.randomUUID().toString().replace("-", "");
try {
// 获取访问令牌
String accessToken;
try {
accessToken = tencentDocTokenService.refreshAccessToken();
log.info("成功刷新访问令牌");
} catch (Exception e) {
log.error("刷新访问令牌失败", e);
try {
accessToken = tencentDocTokenService.getValidAccessToken();
} catch (Exception e2) {
return AjaxResult.error("访问令牌无效请先完成授权。获取授权URL: GET /jarvis/tendoc/authUrl");
}
}
// 从参数或配置中获取文档信息
String fileId = (String) params.get("fileId");
String sheetId = (String) params.get("sheetId");
final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
if (fileId == null || fileId.isEmpty()) {
fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
if (fileId == null || fileId.isEmpty()) {
fileId = tencentDocConfig.getFileId();
}
}
if (sheetId == null || sheetId.isEmpty()) {
sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
if (sheetId == null || sheetId.isEmpty()) {
sheetId = tencentDocConfig.getSheetId();
}
}
// 从配置中读取表头行
Integer headerRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "headerRow");
if (headerRow == null) {
headerRow = tencentDocConfig.getHeaderRow();
}
// 起始行默认850
Integer startRow = params.get("startRow") != null ?
Integer.valueOf(params.get("startRow").toString()) : 850;
// 结束行默认到2500行
Integer endRow = params.get("endRow") != null ?
Integer.valueOf(params.get("endRow").toString()) : 2500;
// 记录接收到的参数
log.info("接收到参数 - startRow: {}, endRow: {}, params: {}", startRow, endRow, params);
// 如果 endRow 小于 startRow + 1000可能是前端传错了强制设置为 2500
if (endRow < startRow + 1000) {
log.warn("检测到 endRow ({}) 可能不正确,强制设置为 2500", endRow);
endRow = 2500;
}
if (accessToken == null || fileId == null || sheetId == null) {
return AjaxResult.error("文档配置不完整,请先配置 fileId 和 sheetId");
}
log.info("反向同步第三方单号开始 - fileId: {}, sheetId: {}, 起始行: {}, 结束行: {}",
fileId, sheetId, startRow, endRow);
// 读取表头,识别列位置
String headerRange = String.format("A%d:Z%d", headerRow, headerRow);
JSONObject headerData = tencentDocService.readSheetData(accessToken, fileId, sheetId, headerRange);
if (headerData == null || !headerData.containsKey("values")) {
return AjaxResult.error("读取表头失败");
}
JSONArray headerValues = headerData.getJSONArray("values");
if (headerValues == null || headerValues.isEmpty()) {
return AjaxResult.error("表头数据为空");
}
JSONArray headerRowData = headerValues.getJSONArray(0);
if (headerRowData == null || headerRowData.isEmpty()) {
return AjaxResult.error("无法识别表头");
}
// 识别列位置
Integer orderNoColumn = null; // "单号"列
Integer logisticsLinkColumn = null; // "物流单号"列
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 (orderNoColumn == null || logisticsLinkColumn == null) {
return AjaxResult.error("无法识别表头列,请确保表头包含'单号'和'物流单号'列");
}
// 统计结果
int successCount = 0;
int skippedCount = 0;
int errorCount = 0;
// 分批读取数据每批200行避免单次读取过多数据导致API限制
final int BATCH_SIZE = 200;
int currentStartRow = startRow;
int totalBatches = (int) Math.ceil((double)(endRow - startRow + 1) / BATCH_SIZE);
int currentBatch = 0;
log.info("开始分批处理,共 {} 批,每批 {} 行", totalBatches, BATCH_SIZE);
while (currentStartRow <= endRow) {
currentBatch++;
int currentEndRow = Math.min(currentStartRow + BATCH_SIZE - 1, endRow);
log.info("正在处理第 {}/{} 批:第 {} 行到第 {} 行", currentBatch, totalBatches, currentStartRow, currentEndRow);
// 读取当前批次的数据行
String dataRange = String.format("A%d:Z%d", currentStartRow, currentEndRow);
log.info("读取数据范围: {}", dataRange);
JSONObject dataResponse = null;
try {
dataResponse = tencentDocService.readSheetData(accessToken, fileId, sheetId, dataRange);
} catch (Exception e) {
log.error("读取第 {} 批数据失败({} - {} 行)", currentBatch, currentStartRow, currentEndRow, e);
errorCount += (currentEndRow - currentStartRow + 1);
// 继续处理下一批
currentStartRow = currentEndRow + 1;
continue;
}
if (dataResponse == null || !dataResponse.containsKey("values")) {
log.warn("第 {} 批数据读取返回空({} - {} 行),跳过", currentBatch, currentStartRow, currentEndRow);
currentStartRow = currentEndRow + 1;
continue;
}
JSONArray rows = dataResponse.getJSONArray("values");
if (rows == null || rows.isEmpty()) {
log.info("第 {} 批数据为空({} - {} 行),跳过", currentBatch, currentStartRow, currentEndRow);
currentStartRow = currentEndRow + 1;
continue;
}
log.info("第 {} 批读取到 {} 行数据", currentBatch, rows.size());
// 处理当前批次的每一行
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
JSONArray row = rows.getJSONArray(rowIndex);
if (row == null || row.size() <= Math.max(orderNoColumn, logisticsLinkColumn)) {
skippedCount++;
continue;
}
int actualRow = currentStartRow + rowIndex;
// 确保不超过结束行
if (actualRow > endRow) {
break;
}
String logisticsLink = row.getString(logisticsLinkColumn);
String orderNoFromDoc = row.getString(orderNoColumn);
// 跳过物流链接为空的行
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
log.debug("跳过第 {} 行:物流链接为空", actualRow);
skippedCount++;
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, null,
"SKIPPED", "物流链接为空");
continue;
}
// 跳过单号为空的行
if (orderNoFromDoc == null || orderNoFromDoc.trim().isEmpty()) {
log.debug("跳过第 {} 行:单号为空", actualRow);
skippedCount++;
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink,
"SKIPPED", "单号为空");
continue;
}
// 清理物流链接(去除空格、换行符、中文等)
logisticsLink = cleanLogisticsLink(logisticsLink);
try {
// 通过物流链接查找订单
JDOrder order = jdOrderService.selectJDOrderByLogisticsLink(logisticsLink);
if (order == null) {
log.warn("未找到匹配的订单 - 行: {}, 物流链接: {}", actualRow, logisticsLink);
errorCount++;
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink,
"FAILED", "未找到匹配的订单");
continue;
}
// 检查订单是否已有第三方单号(如果已有且与文档中的不同,跳过)
if (order.getThirdPartyOrderNo() != null && !order.getThirdPartyOrderNo().trim().isEmpty()) {
if (!order.getThirdPartyOrderNo().trim().equals(orderNoFromDoc.trim())) {
log.info("跳过第 {} 行:订单已有第三方单号且不同 - 现有: {}, 文档: {}",
actualRow, order.getThirdPartyOrderNo(), orderNoFromDoc);
skippedCount++;
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink,
"SKIPPED", "订单已有第三方单号且不同");
continue;
}
// 如果相同,继续执行(清除物流链接)
}
// 更新订单的第三方单号
order.setThirdPartyOrderNo(orderNoFromDoc.trim());
int updateResult = jdOrderService.updateJDOrder(order);
if (updateResult <= 0) {
log.error("更新订单失败 - 行: {}, 订单ID: {}, 单号: {}",
actualRow, order.getId(), order.getRemark());
errorCount++;
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink,
"FAILED", "更新订单失败");
continue;
}
log.info("✓ 更新订单成功 - 行: {}, 订单: {}, 第三方单号: {}",
actualRow, order.getRemark(), orderNoFromDoc);
successCount++;
// 记录成功日志
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", order.getRemark(), actualRow, logisticsLink,
"SUCCESS", String.format("已将单号 %s 写入订单的第三方单号字段", orderNoFromDoc));
} catch (Exception e) {
log.error("处理第 {} 行失败", actualRow, e);
errorCount++;
logOperation(batchId, fileId, sheetId, "REVERSE_SYNC", null, actualRow, logisticsLink,
"FAILED", "处理异常: " + e.getMessage());
}
// 添加延迟避免API调用频率过高
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
log.info("第 {}/{} 批处理完成,当前统计 - 成功: {}, 跳过: {}, 错误: {}",
currentBatch, totalBatches, successCount, skippedCount, errorCount);
// 移动到下一批
currentStartRow = currentEndRow + 1;
// 批次之间的延迟避免API调用频率过高
if (currentStartRow <= endRow) {
try {
Thread.sleep(200); // 批次之间延迟200ms
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
JSONObject result = new JSONObject();
result.put("batchId", batchId);
result.put("startRow", startRow);
result.put("endRow", endRow);
result.put("successCount", successCount);
result.put("skippedCount", skippedCount);
result.put("errorCount", errorCount);
String message = String.format(
"✓ 反向同步完成:成功 %d 条,跳过 %d 条,错误 %d 条\n" +
" 处理范围:第 %d-%d 行\n" +
" 批次ID%s",
successCount, skippedCount, errorCount, startRow, endRow, batchId);
result.put("message", message);
log.info("反向同步第三方单号完成 - {}", message);
return AjaxResult.success("反向同步完成", result);
} catch (Exception e) {
log.error("反向同步第三方单号失败", e);
return AjaxResult.error("反向同步失败: " + e.getMessage());
}
}
/**
* 清理物流链接
* 去除空格、换行符、制表符、中文等特殊字符只保留URL有效字符
*
* @param logisticsLink 原始物流链接
* @return 清理后的物流链接
*/
private String cleanLogisticsLink(String logisticsLink) {
if (logisticsLink == null) {
return null;
}
// 去除首尾空格
String cleaned = logisticsLink.trim();
// 去除换行符、制表符等空白字符
cleaned = cleaned.replaceAll("[\\r\\n\\t]", "");
// 去除所有空格
cleaned = cleaned.replaceAll("\\s+", "");
// 提取URL如果包含中文或其他非URL字符尝试提取URL部分
// 匹配 http:// 或 https:// 开头的URL
java.util.regex.Pattern urlPattern = java.util.regex.Pattern.compile("https?://[^\\s\\u4e00-\\u9fa5]+");
java.util.regex.Matcher matcher = urlPattern.matcher(cleaned);
if (matcher.find()) {
cleaned = matcher.group();
} else {
// 如果没有找到完整URL尝试提取 3.cn 相关的链接
java.util.regex.Pattern shortUrlPattern = java.util.regex.Pattern.compile("https?://3\\.cn/[^\\s\\u4e00-\\u9fa5]+");
java.util.regex.Matcher shortMatcher = shortUrlPattern.matcher(cleaned);
if (shortMatcher.find()) {
cleaned = shortMatcher.group();
}
}
return cleaned.trim();
}
}

View File

@@ -111,11 +111,14 @@ public class PublicOrderController extends BaseController {
log.info("日期验证通过: 订单日期[{}]", orderDate);
// 获取forceGenerate参数默认为false
boolean forceGenerate = body != null && body.get("forceGenerate") != null && Boolean.parseBoolean(String.valueOf(body.get("forceGenerate")));
// 执行指令
List<String> result;
try {
log.info("开始执行订单指令...");
result = instructionService.execute(trimmedCmd);
log.info("开始执行订单指令... forceGenerate={}", forceGenerate);
result = instructionService.execute(trimmedCmd, forceGenerate);
log.info("订单指令执行完成");
// 记录执行结果

View File

@@ -10,11 +10,14 @@ import com.ruoyi.jarvis.service.IOrderRowsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.annotation.Anonymous;
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.JDOrder;
import com.ruoyi.jarvis.domain.dto.JDOrderSimpleDTO;
import com.ruoyi.jarvis.service.IJDOrderService;
import com.ruoyi.jarvis.service.IInstructionService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
@@ -30,10 +33,12 @@ public class JDOrderListController extends BaseController
private final IJDOrderService jdOrderService;
private final IOrderRowsService orderRowsService;
private final IInstructionService instructionService;
public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService) {
public JDOrderListController(IJDOrderService jdOrderService, IOrderRowsService orderRowsService, IInstructionService instructionService) {
this.jdOrderService = jdOrderService;
this.orderRowsService = orderRowsService;
this.instructionService = instructionService;
}
/**
@@ -64,6 +69,12 @@ public class JDOrderListController extends BaseController
query.getParams().put("hasFinishTime", true);
}
// 处理混合搜索参数(订单号/第三方单号/分销标识)
String orderSearch = request.getParameter("orderSearch");
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
query.getParams().put("orderSearch", orderSearch.trim());
}
java.util.List<JDOrder> list;
if (orderBy != null && !orderBy.isEmpty()) {
// 设置排序参数
@@ -85,9 +96,11 @@ public class JDOrderListController extends BaseController
if (orderRows != null) {
jdOrder.setProPriceAmount(orderRows.getProPriceAmount());
jdOrder.setFinishTime(orderRows.getFinishTime());
jdOrder.setOrderStatus(orderRows.getValidCode());
} else {
jdOrder.setProPriceAmount(0.0);
jdOrder.setFinishTime(null);
jdOrder.setOrderStatus(null);
}
}
// 过滤掉完成时间为空的订单
@@ -101,8 +114,10 @@ public class JDOrderListController extends BaseController
if (orderRows != null) {
jdOrder.setProPriceAmount(orderRows.getProPriceAmount());
jdOrder.setFinishTime(orderRows.getFinishTime());
jdOrder.setOrderStatus(orderRows.getValidCode());
} else {
jdOrder.setProPriceAmount(0.0);
jdOrder.setOrderStatus(null);
}
}
}
@@ -171,4 +186,251 @@ public class JDOrderListController extends BaseController
{
return toAjax(jdOrderService.deleteJDOrderByIds(ids));
}
/**
* 订单搜索工具接口(返回简易字段)
*/
@Anonymous
@GetMapping("/tools/search")
public TableDataInfo searchOrders(
@RequestParam(required = false) String orderSearch,
@RequestParam(required = false) String address,
HttpServletRequest request)
{
// startPage会从request中读取pageNum和pageSize参数
startPage();
JDOrder query = new JDOrder();
// 处理单号搜索过滤TF、H、F、PDD等关键词
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
String searchKeyword = orderSearch.trim().toUpperCase();
// 过滤掉TF、H、F、PDD等关键词
if (searchKeyword.contains("TF") || searchKeyword.contains("H") ||
searchKeyword.contains("F") || searchKeyword.contains("PDD")) {
// 如果包含过滤关键词,返回空结果
return getDataTable(new java.util.ArrayList<>());
}
// 至少5个字符
if (searchKeyword.length() >= 5) {
query.getParams().put("orderSearch", orderSearch.trim());
}
}
// 处理地址搜索至少3个字符
if (address != null && !address.trim().isEmpty()) {
if (address.trim().length() >= 3) {
query.setAddress(address.trim());
}
}
// 如果没有有效的搜索条件,返回空结果
if ((orderSearch == null || orderSearch.trim().isEmpty() || orderSearch.trim().length() < 5) &&
(address == null || address.trim().isEmpty() || address.trim().length() < 3)) {
return getDataTable(new java.util.ArrayList<>());
}
java.util.List<JDOrder> list = jdOrderService.selectJDOrderList(query);
// 转换为简易DTO只返回前端需要的字段其他字段脱敏
java.util.List<JDOrderSimpleDTO> simpleList = new java.util.ArrayList<>();
for (JDOrder jdOrder : list) {
JDOrderSimpleDTO dto = new JDOrderSimpleDTO();
// 只设置前端需要的字段
dto.setRemark(jdOrder.getRemark());
dto.setOrderId(jdOrder.getOrderId());
dto.setThirdPartyOrderNo(jdOrder.getThirdPartyOrderNo());
dto.setModelNumber(jdOrder.getModelNumber());
dto.setAddress(jdOrder.getAddress());
dto.setIsRefunded(jdOrder.getIsRefunded() != null ? jdOrder.getIsRefunded() : 0);
dto.setIsRebateReceived(jdOrder.getIsRebateReceived() != null ? jdOrder.getIsRebateReceived() : 0);
dto.setStatus(jdOrder.getStatus());
dto.setCreateTime(jdOrder.getCreateTime());
// 关联查询订单状态和赔付金额
OrderRows orderRows = orderRowsService.selectOrderRowsByOrderId(jdOrder.getOrderId());
if (orderRows != null) {
dto.setProPriceAmount(orderRows.getProPriceAmount());
dto.setOrderStatus(orderRows.getValidCode());
} else {
dto.setProPriceAmount(0.0);
dto.setOrderStatus(null);
}
simpleList.add(dto);
}
return getDataTable(simpleList);
}
/**
* 一次性批量更新历史订单将赔付金额大于0的订单标记为后返到账
* 此方法只应执行一次,用于处理历史数据
*/
@Log(title = "批量标记后返到账", businessType = BusinessType.UPDATE)
@RequestMapping(value = "/tools/batch-mark-rebate-received", method = {RequestMethod.POST, RequestMethod.GET})
public AjaxResult batchMarkRebateReceivedForCompensation() {
try {
// 调用批量更新方法
if (instructionService instanceof com.ruoyi.jarvis.service.impl.InstructionServiceImpl) {
((com.ruoyi.jarvis.service.impl.InstructionServiceImpl) instructionService)
.batchMarkRebateReceivedForCompensation();
return AjaxResult.success("批量标记后返到账完成,请查看控制台日志");
} else {
return AjaxResult.error("无法执行批量更新操作");
}
} catch (Exception e) {
return AjaxResult.error("批量标记失败: " + e.getMessage());
}
}
/**
* 生成录单格式文本Excel可粘贴格式
* 根据当前查询条件生成Tab分隔的文本可以直接粘贴到Excel
*/
@GetMapping("/generateExcelText")
public AjaxResult generateExcelText(JDOrder query, HttpServletRequest request) {
try {
// 处理时间筛选参数
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);
}
// 处理混合搜索参数
String orderSearch = request.getParameter("orderSearch");
if (orderSearch != null && !orderSearch.trim().isEmpty()) {
query.getParams().put("orderSearch", orderSearch.trim());
}
// 处理其他查询参数
if (query.getRemark() != null && !query.getRemark().trim().isEmpty()) {
query.setRemark(query.getRemark().trim());
}
if (query.getDistributionMark() != null && !query.getDistributionMark().trim().isEmpty()) {
query.setDistributionMark(query.getDistributionMark().trim());
}
if (query.getModelNumber() != null && !query.getModelNumber().trim().isEmpty()) {
query.setModelNumber(query.getModelNumber().trim());
}
if (query.getBuyer() != null && !query.getBuyer().trim().isEmpty()) {
query.setBuyer(query.getBuyer().trim());
}
if (query.getAddress() != null && !query.getAddress().trim().isEmpty()) {
query.setAddress(query.getAddress().trim());
}
if (query.getStatus() != null && !query.getStatus().trim().isEmpty()) {
query.setStatus(query.getStatus().trim());
}
// 获取订单列表(不分页,获取所有符合条件的订单)
List<JDOrder> list = jdOrderService.selectJDOrderList(query);
if (list == null || list.isEmpty()) {
return AjaxResult.success("暂无订单数据");
}
// 关联查询订单状态和赔付金额
for (JDOrder jdOrder : list) {
OrderRows orderRows = orderRowsService.selectOrderRowsByOrderId(jdOrder.getOrderId());
if (orderRows != null) {
jdOrder.setProPriceAmount(orderRows.getProPriceAmount());
// estimateCosPrice 是京粉实际价格
if (orderRows.getEstimateCosPrice() != null) {
jdOrder.setJingfenActualPrice(orderRows.getEstimateCosPrice());
}
}
}
// 按 remark 排序
list.sort((o1, o2) -> {
String r1 = o1.getRemark() != null ? o1.getRemark() : "";
String r2 = o2.getRemark() != null ? o2.getRemark() : "";
return r1.compareTo(r2);
});
// 生成Excel格式文本Tab分隔
StringBuilder sb = new StringBuilder();
for (JDOrder o : list) {
// 日期格式yyyy/MM/dd
String dateStr = "";
if (o.getOrderTime() != null) {
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy/MM/dd");
dateStr = sdf.format(o.getOrderTime());
}
// 多多单号(第三方单号,如果没有则使用内部单号)
String duoduoOrderNo = o.getThirdPartyOrderNo() != null && !o.getThirdPartyOrderNo().trim().isEmpty()
? o.getThirdPartyOrderNo() : (o.getRemark() != null ? o.getRemark() : "");
// 型号
String modelNumber = o.getModelNumber() != null ? o.getModelNumber() : "";
// 数量固定为1
String quantity = "1";
// 地址
String address = o.getAddress() != null ? o.getAddress() : "";
// 姓名(从地址中提取,地址格式通常是"姓名 电话 详细地址"
String buyer = "";
if (address != null && !address.trim().isEmpty()) {
String[] addressParts = address.trim().split("\\s+");
if (addressParts.length > 0) {
buyer = addressParts[0];
}
}
// 售价固定为0
String sellingPriceStr = "0";
// 成本售价是0成本也设为空
String costStr = "";
// 利润(后返金额)
Double rebate = o.getRebateAmount() != null ? o.getRebateAmount() : 0.0;
String profitStr = rebate > 0
? String.format(java.util.Locale.ROOT, "%.2f", rebate) : "";
// 京东单号
String orderId = o.getOrderId() != null ? o.getOrderId() : "";
// 物流链接
String logisticsLink = o.getLogisticsLink() != null ? o.getLogisticsLink() : "";
// 下单付款
String paymentAmountStr = o.getPaymentAmount() != null
? String.format(java.util.Locale.ROOT, "%.2f", o.getPaymentAmount()) : "";
// 后返
String rebateAmountStr = o.getRebateAmount() != null
? String.format(java.util.Locale.ROOT, "%.2f", o.getRebateAmount()) : "";
// 按顺序拼接:日期、多多单号、型号、数量、姓名、地址、售价、成本、利润、京东单号、物流、下单付款、后返
sb.append(dateStr).append('\t')
.append(duoduoOrderNo).append('\t')
.append(modelNumber).append('\t')
.append(quantity).append('\t')
.append(buyer).append('\t')
.append(address).append('\t')
.append(sellingPriceStr).append('\t')
.append(costStr).append('\t')
.append(profitStr).append('\t')
.append(orderId).append('\t')
.append(logisticsLink).append('\t')
.append(paymentAmountStr).append('\t')
.append(rebateAmountStr).append('\n');
}
return AjaxResult.success(sb.toString());
} catch (Exception e) {
return AjaxResult.error("生成失败: " + e.getMessage());
}
}
}

View File

@@ -8,4 +8,4 @@ tencent:
delayed:
push:
# 延迟时间分钟默认10分钟
minutes: 30
minutes: 10

View File

@@ -217,6 +217,79 @@ public class HttpUtils
return result.toString();
}
/**
* 向指定 URL 发送DELETE方法的请求
*
* @param url 发送请求的 URL
* @return 所代表远程资源的响应结果
*/
public static String sendDelete(String url)
{
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try
{
log.info("sendDelete - {}", url);
URL realUrl = new URL(url);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) realUrl.openConnection();
conn.setRequestMethod("DELETE");
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connection", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64)");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setConnectTimeout(10000);
conn.setReadTimeout(20000);
conn.connect();
int responseCode = conn.getResponseCode();
InputStream inputStream = (responseCode >= 200 && responseCode < 300)
? conn.getInputStream()
: conn.getErrorStream();
if (inputStream != null)
{
in = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = in.readLine()) != null)
{
result.append(line);
}
}
log.info("recv - {}", result);
}
catch (ConnectException e)
{
log.error("调用HttpUtils.sendDelete ConnectException, url=" + url, e);
}
catch (SocketTimeoutException e)
{
log.error("调用HttpUtils.sendDelete SocketTimeoutException, url=" + url, e);
}
catch (IOException e)
{
log.error("调用HttpUtils.sendDelete IOException, url=" + url, e);
}
catch (Exception e)
{
log.error("调用HttpUtils.sendDelete Exception, url=" + url, e);
}
finally
{
try
{
if (in != null)
{
in.close();
}
}
catch (Exception ex)
{
log.error("调用in.close Exception, url=" + url, ex);
}
}
return result.toString();
}
public static String sendSSLPost(String url, String param)
{
return sendSSLPost(url, param, MediaType.APPLICATION_FORM_URLENCODED_VALUE);

View File

@@ -0,0 +1,282 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
/**
* 闲鱼商品对象 erp_product
*
* @author ruoyi
* @date 2024-01-01
*/
public class ErpProduct extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 主键ID */
private Long id;
/** 管家商品ID */
@Excel(name = "管家商品ID")
private Long productId;
/** 商品标题 */
@Excel(name = "商品标题")
private String title;
/** 商品图片(主图) */
@Excel(name = "商品图片")
private String mainImage;
/** 商品价格(分) */
@Excel(name = "商品价格")
private Long price;
/** 商品库存 */
@Excel(name = "商品库存")
private Integer stock;
/** 商品状态 -1:删除 21:待发布 22:销售中 23:已售罄 31:手动下架 33:售出下架 36:自动下架 */
@Excel(name = "商品状态", readConverterExp = "-1=删除,21=待发布,22=销售中,23=已售罄,31=手动下架,33=售出下架,36=自动下架")
private Integer productStatus;
/** 销售状态 */
@Excel(name = "销售状态")
private Integer saleStatus;
/** 闲鱼会员名 */
@Excel(name = "闲鱼会员名")
private String userName;
/** 上架时间 */
@Excel(name = "上架时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long onlineTime;
/** 下架时间 */
@Excel(name = "下架时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long offlineTime;
/** 售出时间 */
@Excel(name = "售出时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long soldTime;
/** 创建时间(闲鱼) */
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long createTimeXy;
/** 更新时间(闲鱼) */
@Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Long updateTimeXy;
/** ERP应用ID */
@Excel(name = "ERP应用ID")
private String appid;
/** 商品链接 */
@Excel(name = "商品链接")
private String productUrl;
/** 备注 */
@Excel(name = "备注")
private String remark;
public void setId(Long id)
{
this.id = id;
}
public Long getId()
{
return id;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public Long getProductId()
{
return productId;
}
public void setTitle(String title)
{
this.title = title;
}
public String getTitle()
{
return title;
}
public void setMainImage(String mainImage)
{
this.mainImage = mainImage;
}
public String getMainImage()
{
return mainImage;
}
public void setPrice(Long price)
{
this.price = price;
}
public Long getPrice()
{
return price;
}
public void setStock(Integer stock)
{
this.stock = stock;
}
public Integer getStock()
{
return stock;
}
public void setProductStatus(Integer productStatus)
{
this.productStatus = productStatus;
}
public Integer getProductStatus()
{
return productStatus;
}
public void setSaleStatus(Integer saleStatus)
{
this.saleStatus = saleStatus;
}
public Integer getSaleStatus()
{
return saleStatus;
}
public void setUserName(String userName)
{
this.userName = userName;
}
public String getUserName()
{
return userName;
}
public void setOnlineTime(Long onlineTime)
{
this.onlineTime = onlineTime;
}
public Long getOnlineTime()
{
return onlineTime;
}
public void setOfflineTime(Long offlineTime)
{
this.offlineTime = offlineTime;
}
public Long getOfflineTime()
{
return offlineTime;
}
public void setSoldTime(Long soldTime)
{
this.soldTime = soldTime;
}
public Long getSoldTime()
{
return soldTime;
}
public void setCreateTimeXy(Long createTimeXy)
{
this.createTimeXy = createTimeXy;
}
public Long getCreateTimeXy()
{
return createTimeXy;
}
public void setUpdateTimeXy(Long updateTimeXy)
{
this.updateTimeXy = updateTimeXy;
}
public Long getUpdateTimeXy()
{
return updateTimeXy;
}
public void setAppid(String appid)
{
this.appid = appid;
}
public String getAppid()
{
return appid;
}
public void setProductUrl(String productUrl)
{
this.productUrl = productUrl;
}
public String getProductUrl()
{
return productUrl;
}
@Override
public void setRemark(String remark)
{
this.remark = remark;
}
@Override
public String getRemark()
{
return remark;
}
@Override
public String toString() {
return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE)
.append("id", getId())
.append("productId", getProductId())
.append("title", getTitle())
.append("mainImage", getMainImage())
.append("price", getPrice())
.append("stock", getStock())
.append("productStatus", getProductStatus())
.append("saleStatus", getSaleStatus())
.append("userName", getUserName())
.append("onlineTime", getOnlineTime())
.append("offlineTime", getOfflineTime())
.append("soldTime", getSoldTime())
.append("createTimeXy", getCreateTimeXy())
.append("updateTimeXy", getUpdateTimeXy())
.append("appid", getAppid())
.append("productUrl", getProductUrl())
.append("remark", getRemark())
.append("createTime", getCreateTime())
.append("updateTime", getUpdateTime())
.toString();
}
}

View File

@@ -80,6 +80,11 @@ public class JDOrder extends BaseEntity {
@Excel(name = "完成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date finishTime;
/** 订单状态从order_rows表查询 */
@Transient
@Excel(name = "订单状态")
private Integer orderStatus;
/** 是否参与统计0否 1是 */
@Excel(name = "参与统计")
private Integer isCountEnabled;
@@ -92,6 +97,30 @@ public class JDOrder extends BaseEntity {
@Excel(name = "京粉实际价格")
private Double jingfenActualPrice;
/** 是否退款0否 1是 */
@Excel(name = "是否退款")
private Integer isRefunded;
/** 退款日期 */
@Excel(name = "退款日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date refundDate;
/** 是否退款到账0否 1是 */
@Excel(name = "是否退款到账")
private Integer isRefundReceived;
/** 退款到账日期 */
@Excel(name = "退款到账日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date refundReceivedDate;
/** 后返到账0否 1是 */
@Excel(name = "后返到账")
private Integer isRebateReceived;
/** 后返到账日期 */
@Excel(name = "后返到账日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date rebateReceivedDate;
}

View File

@@ -44,6 +44,10 @@ public class SuperAdmin extends BaseEntity
@Excel(name = "是否参与订单统计", readConverterExp = "0=否,1=是")
private Integer isCount;
/** 接收人企业微信用户ID多个用逗号分隔 */
@Excel(name = "接收人")
private String touser;
/** 创建时间 */
@Excel(name = "创建时间")
private Date createdAt;
@@ -151,4 +155,14 @@ public class SuperAdmin extends BaseEntity
{
this.isCount = isCount;
}
public String getTouser()
{
return touser;
}
public void setTouser(String touser)
{
this.touser = touser;
}
}

View File

@@ -0,0 +1,132 @@
package com.ruoyi.jarvis.domain.dto;
import java.util.Date;
/**
* 订单搜索工具返回的简易DTO
* 只包含前端展示需要的字段,其他字段脱敏
*/
public class JDOrderSimpleDTO {
/** 内部单号 */
private String remark;
/** 京东单号 */
private String orderId;
/** 第三方单号 */
private String thirdPartyOrderNo;
/** 型号 */
private String modelNumber;
/** 地址 */
private String address;
/** 退款状态0否 1是 */
private Integer isRefunded;
/** 后返到账0否 1是 */
private Integer isRebateReceived;
/** 赔付金额 */
private Double proPriceAmount;
/** 订单状态 */
private Integer orderStatus;
/** 备注/状态 */
private String status;
/** 创建时间 */
private Date createTime;
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getOrderId() {
return orderId;
}
public void setOrderId(String orderId) {
this.orderId = orderId;
}
public String getThirdPartyOrderNo() {
return thirdPartyOrderNo;
}
public void setThirdPartyOrderNo(String thirdPartyOrderNo) {
this.thirdPartyOrderNo = thirdPartyOrderNo;
}
public String getModelNumber() {
return modelNumber;
}
public void setModelNumber(String modelNumber) {
this.modelNumber = modelNumber;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public Integer getIsRefunded() {
return isRefunded;
}
public void setIsRefunded(Integer isRefunded) {
this.isRefunded = isRefunded;
}
public Integer getIsRebateReceived() {
return isRebateReceived;
}
public void setIsRebateReceived(Integer isRebateReceived) {
this.isRebateReceived = isRebateReceived;
}
public Double getProPriceAmount() {
return proPriceAmount;
}
public void setProPriceAmount(Double proPriceAmount) {
this.proPriceAmount = proPriceAmount;
}
public Integer getOrderStatus() {
return orderStatus;
}
public void setOrderStatus(Integer orderStatus) {
this.orderStatus = orderStatus;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}

View File

@@ -0,0 +1,80 @@
package com.ruoyi.jarvis.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import com.ruoyi.jarvis.domain.ErpProduct;
/**
* 闲鱼商品Mapper接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface ErpProductMapper
{
/**
* 查询闲鱼商品
*
* @param id 闲鱼商品主键
* @return 闲鱼商品
*/
public ErpProduct selectErpProductById(Long id);
/**
* 查询闲鱼商品列表
*
* @param erpProduct 闲鱼商品
* @return 闲鱼商品集合
*/
public List<ErpProduct> selectErpProductList(ErpProduct erpProduct);
/**
* 新增闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int insertErpProduct(ErpProduct erpProduct);
/**
* 修改闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int updateErpProduct(ErpProduct erpProduct);
/**
* 删除闲鱼商品
*
* @param id 闲鱼商品主键
* @return 结果
*/
public int deleteErpProductById(Long id);
/**
* 批量删除闲鱼商品
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteErpProductByIds(Long[] ids);
/**
* 根据商品ID和appid查询
*
* @param productId 商品ID
* @param appid ERP应用ID
* @return 闲鱼商品
*/
public ErpProduct selectErpProductByProductIdAndAppid(@Param("productId") Long productId, @Param("appid") String appid);
/**
* 批量插入或更新闲鱼商品
*
* @param erpProducts 闲鱼商品列表
* @return 结果
*/
public int batchInsertOrUpdateErpProduct(List<ErpProduct> erpProducts);
}

View File

@@ -0,0 +1,107 @@
package com.ruoyi.jarvis.service;
import java.util.List;
import com.ruoyi.jarvis.domain.ErpProduct;
/**
* 闲鱼商品Service接口
*
* @author ruoyi
* @date 2024-01-01
*/
public interface IErpProductService
{
/**
* 查询闲鱼商品
*
* @param id 闲鱼商品主键
* @return 闲鱼商品
*/
public ErpProduct selectErpProductById(Long id);
/**
* 查询闲鱼商品列表
*
* @param erpProduct 闲鱼商品
* @return 闲鱼商品集合
*/
public List<ErpProduct> selectErpProductList(ErpProduct erpProduct);
/**
* 新增闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int insertErpProduct(ErpProduct erpProduct);
/**
* 修改闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
public int updateErpProduct(ErpProduct erpProduct);
/**
* 批量删除闲鱼商品
*
* @param ids 需要删除的闲鱼商品主键集合
* @return 结果
*/
public int deleteErpProductByIds(Long[] ids);
/**
* 删除闲鱼商品信息
*
* @param id 闲鱼商品主键
* @return 结果
*/
public int deleteErpProductById(Long id);
/**
* 从闲鱼ERP拉取商品列表并保存
*
* @param appid ERP应用ID
* @param pageNo 页码
* @param pageSize 每页大小
* @param productStatus 商品状态
* @return 拉取结果
*/
public int pullAndSaveProductList(String appid, Integer pageNo, Integer pageSize, Integer productStatus);
/**
* 全量同步商品列表(自动遍历所有页码,同步更新和删除)
*
* @param appid ERP应用ID
* @param productStatus 商品状态null表示全部状态
* @return 同步结果
*/
public SyncResult syncAllProducts(String appid, Integer productStatus);
/**
* 同步结果
*/
public static class SyncResult {
private int totalPulled; // 拉取总数
private int added; // 新增数量
private int updated; // 更新数量
private int deleted; // 删除数量
private int failed; // 失败数量
private String message; // 结果消息
public int getTotalPulled() { return totalPulled; }
public void setTotalPulled(int totalPulled) { this.totalPulled = totalPulled; }
public int getAdded() { return added; }
public void setAdded(int added) { this.added = added; }
public int getUpdated() { return updated; }
public void setUpdated(int updated) { this.updated = updated; }
public int getDeleted() { return deleted; }
public void setDeleted(int deleted) { this.deleted = deleted; }
public int getFailed() { return failed; }
public void setFailed(int failed) { this.failed = failed; }
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
}
}

View File

@@ -11,6 +11,23 @@ public interface IInstructionService {
*/
java.util.List<String> execute(String command);
/**
* 执行文本指令,返回结果文本(支持强制生成参数)
* @param command 指令内容
* @param forceGenerate 是否强制生成表单(跳过地址重复检查)
* @return 执行结果文本列表(可能为单条或多条)
*/
java.util.List<String> execute(String command, boolean forceGenerate);
/**
* 执行文本指令,返回结果文本(支持强制生成参数和控制台入口标识)
* @param command 指令内容
* @param forceGenerate 是否强制生成表单(跳过地址重复检查)
* @param isFromConsole 是否来自控制台入口(控制台入口跳过订单查询校验)
* @return 执行结果文本列表(可能为单条或多条)
*/
java.util.List<String> execute(String command, boolean forceGenerate, boolean isFromConsole);
/**
* 获取历史消息记录
* @param type 消息类型request(请求) 或 response(响应)

View File

@@ -0,0 +1,67 @@
package com.ruoyi.jarvis.service;
import java.util.Map;
/**
* 小红书/抖音内容生成Service接口
*
* @author ruoyi
* @date 2025-01-XX
*/
public interface ISocialMediaService
{
/**
* 提取商品标题关键词
*
* @param productName 商品名称
* @return 关键词结果
*/
Map<String, Object> extractKeywords(String productName);
/**
* 生成文案
*
* @param productName 商品名称
* @param originalPrice 原价
* @param finalPrice 到手价
* @param keywords 关键词
* @param style 文案风格
* @return 生成的文案
*/
Map<String, Object> generateContent(String productName, Object originalPrice,
Object finalPrice, String keywords, String style);
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*
* @param productImageUrl 商品主图URL
* @param productName 商品名称
* @param originalPrice 原价
* @param finalPrice 到手价
* @param style 文案风格
* @return 完整内容
*/
Map<String, Object> generateCompleteContent(String productImageUrl, String productName,
Object originalPrice, Object finalPrice, String style);
/**
* 获取提示词模板列表
*/
com.ruoyi.common.core.domain.AjaxResult listPromptTemplates();
/**
* 获取单个提示词模板
*/
com.ruoyi.common.core.domain.AjaxResult getPromptTemplate(String key);
/**
* 保存提示词模板
*/
com.ruoyi.common.core.domain.AjaxResult savePromptTemplate(Map<String, Object> request);
/**
* 删除提示词模板(恢复默认)
*/
com.ruoyi.common.core.domain.AjaxResult deletePromptTemplate(String key);
}

View File

@@ -98,9 +98,18 @@ private String cleanForbiddenPhrases(String text) {
return text;
}
String cleaned = text;
// 新增:清理【】符号(包括单独出现或成对出现的情况)
cleaned = cleaned.replaceAll("", ""); // 移除左括号【
cleaned = cleaned.replaceAll("", ""); // 移除右括号】
// 新增:清理"咨询客服立减""咨询客服""客服"及变体(含空格)
// 优先处理长组合,避免被拆分后遗漏
cleaned = cleaned.replaceAll("咨询\\s*客服\\s*立减", ""); // 匹配"咨询客服立减""咨询 客服 立减"等
cleaned = cleaned.replaceAll("咨询\\s*客服", ""); // 匹配"咨询客服""咨询 客服"等
cleaned = cleaned.replaceAll("\\s*服", ""); // 匹配"客服""客 服"等
// 一、政策补贴及特殊渠道类(长组合优先)
cleaned = cleaned.replaceAll("咨询客服领\\s*国补", "");
cleaned = cleaned.replaceAll("政府\\s*补贴", ""); // 匹配"政府 补贴"等带空格的情况
cleaned = cleaned.replaceAll("政府\\s*补贴", "");
cleaned = cleaned.replaceAll("购车\\s*补贴", "");
cleaned = cleaned.replaceAll("家电\\s*下乡", "");
cleaned = cleaned.replaceAll("内部\\s*渠道", "");
@@ -128,15 +137,15 @@ private String cleanForbiddenPhrases(String text) {
cleaned = cleaned.replaceAll("原单", "");
cleaned = cleaned.replaceAll("尾单", "");
cleaned = cleaned.replaceAll("工厂\\s*货", "");
cleaned = cleaned.replaceAll("专柜\\s*验货", ""); // 无授权时违规
cleaned = cleaned.replaceAll("专柜\\s*验货", "");
// 四、线下导流及规避监管类(多变体覆盖)
cleaned = cleaned.replaceAll("\\s*信", ""); // 匹配"微信""微 信"
cleaned = cleaned.replaceAll("\\s*信", ""); // 谐音变体
cleaned = cleaned.replaceAll("\\s*信", "");
cleaned = cleaned.replaceAll("\\s*信", "");
cleaned = cleaned.replaceAll("V我", "");
cleaned = cleaned.replaceAll("\\s*卫星", "");
cleaned = cleaned.replaceAll("QQ", "");
cleaned = cleaned.replaceAll("扣扣", ""); // 谐音
cleaned = cleaned.replaceAll("扣扣", "");
cleaned = cleaned.replaceAll("手机\\s*号", "");
cleaned = cleaned.replaceAll("淘宝\\s*链接", "");
cleaned = cleaned.replaceAll("拼多\\s*多", "");
@@ -146,7 +155,7 @@ private String cleanForbiddenPhrases(String text) {
cleaned = cleaned.replaceAll("", "");
cleaned = cleaned.replaceAll("垃圾", "");
cleaned = cleaned.replaceAll("笨蛋", "");
cleaned = cleaned.replaceAll("SB", ""); // 单独出现时清理(避免误判可后续加上下文校验)
cleaned = cleaned.replaceAll("SB", "");
cleaned = cleaned.replaceAll("原味", "");
cleaned = cleaned.replaceAll("情趣", "");
@@ -922,7 +931,8 @@ private String cleanForbiddenPhrases(String text) {
// 调用ERP上架接口
ProductPublishRequest publishRequest = new ProductPublishRequest(account);
publishRequest.setProductId(item.getProductId());
publishRequest.setUserName(commonParams.getUserName());
// 【修复】使用商品对应的子账号,而不是通用参数中的第一个子账号
publishRequest.setUserName(item.getSubAccount() != null ? item.getSubAccount() : commonParams.getUserName());
publishRequest.setSpecifyPublishTime(null); // 立即上架
String resp = publishRequest.getResponseBody();
@@ -1005,32 +1015,57 @@ private String cleanForbiddenPhrases(String text) {
throw new RuntimeException("任务不存在: " + taskId);
}
// 仅重试 待发布(0)/发布失败(3) 的明细
List<BatchPublishItem> allItems = itemMapper.selectBatchPublishItemByTaskId(taskId);
List<BatchPublishItem> itemsToRetry = new ArrayList<>();
// 分类处理:需要重新发布的 和 需要重新上架的
List<BatchPublishItem> itemsToRepublish = new ArrayList<>(); // 待发布(0)/发布失败(3)
List<BatchPublishItem> itemsToRelist = new ArrayList<>(); // 发布成功(2)/上架失败(6)
for (BatchPublishItem it : allItems) {
if (it.getStatus() != null && (it.getStatus() == 0 || it.getStatus() == 3)) {
itemsToRetry.add(it);
if (it.getStatus() != null) {
if (it.getStatus() == 0 || it.getStatus() == 3) {
// 待发布或发布失败,需要重新发布
itemsToRepublish.add(it);
} else if (it.getStatus() == 2 || it.getStatus() == 6) {
// 发布成功但未上架,或上架失败,需要重新上架
itemsToRelist.add(it);
}
}
}
if (itemsToRetry.isEmpty()) {
log.info("任务{} 无需重试,未发现待发布/失败的明细", taskId);
if (itemsToRepublish.isEmpty() && itemsToRelist.isEmpty()) {
log.info("任务{} 无需重试,未发现待处理的明细", taskId);
return;
}
// 构造最小请求对象,仅提供通用参数用于发布
BatchPublishRequest req = new BatchPublishRequest();
try {
BatchPublishRequest.CommonParams commonParams = JSON.parseObject(
task.getCommonParams(), BatchPublishRequest.CommonParams.class);
req.setCommonParams(commonParams);
} catch (Exception e) {
log.warn("解析任务通用参数失败,将使用默认参数: {}", task.getCommonParams(), e);
// 处理需要重新发布的商品
if (!itemsToRepublish.isEmpty()) {
BatchPublishRequest req = new BatchPublishRequest();
try {
BatchPublishRequest.CommonParams commonParams = JSON.parseObject(
task.getCommonParams(), BatchPublishRequest.CommonParams.class);
req.setCommonParams(commonParams);
} catch (Exception e) {
log.warn("解析任务通用参数失败,将使用默认参数: {}", task.getCommonParams(), e);
}
log.info("开始重新发布任务{} 的 {} 条明细", taskId, itemsToRepublish.size());
self.asyncBatchPublish(taskId, itemsToRepublish, req);
}
log.info("开始重试任务{} 的 {} 条明细", taskId, itemsToRetry.size());
self.asyncBatchPublish(taskId, itemsToRetry, req);
// 处理需要重新上架的商品(直接上架,不需要重新发布)
if (!itemsToRelist.isEmpty()) {
log.info("开始重新上架任务{} 的 {} 条明细", taskId, itemsToRelist.size());
for (BatchPublishItem item : itemsToRelist) {
// 重置状态为发布成功,准备上架
item.setStatus(2);
item.setErrorMessage(null);
itemMapper.updateBatchPublishItem(item);
appendItemLogSafe(item.getId(), "【重试】准备重新上架");
// 立即调度上架延迟1秒避免过快
schedulePublish(item.getId(), 1);
}
}
}
}

View File

@@ -0,0 +1,480 @@
package com.ruoyi.jarvis.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
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.ProductListQueryRequest;
import com.ruoyi.erp.request.ProductDeleteRequest;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Set;
import java.util.HashSet;
import java.util.stream.Collectors;
/**
* 闲鱼商品Service业务层处理
*
* @author ruoyi
* @date 2024-01-01
*/
@Service
public class ErpProductServiceImpl implements IErpProductService
{
private static final Logger log = LoggerFactory.getLogger(ErpProductServiceImpl.class);
@Autowired
private ErpProductMapper erpProductMapper;
/**
* 查询闲鱼商品
*
* @param id 闲鱼商品主键
* @return 闲鱼商品
*/
@Override
public ErpProduct selectErpProductById(Long id)
{
return erpProductMapper.selectErpProductById(id);
}
/**
* 查询闲鱼商品列表
*
* @param erpProduct 闲鱼商品
* @return 闲鱼商品
*/
@Override
public List<ErpProduct> selectErpProductList(ErpProduct erpProduct)
{
return erpProductMapper.selectErpProductList(erpProduct);
}
/**
* 新增闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
@Override
public int insertErpProduct(ErpProduct erpProduct)
{
// 检查是否已存在
ErpProduct existing = erpProductMapper.selectErpProductByProductIdAndAppid(
erpProduct.getProductId(), erpProduct.getAppid());
if (existing != null) {
// 更新已存在的商品
erpProduct.setId(existing.getId());
return erpProductMapper.updateErpProduct(erpProduct);
}
return erpProductMapper.insertErpProduct(erpProduct);
}
/**
* 修改闲鱼商品
*
* @param erpProduct 闲鱼商品
* @return 结果
*/
@Override
public int updateErpProduct(ErpProduct erpProduct)
{
return erpProductMapper.updateErpProduct(erpProduct);
}
/**
* 批量删除闲鱼商品
*
* @param ids 需要删除的闲鱼商品主键
* @return 结果
*/
@Override
public int deleteErpProductByIds(Long[] ids)
{
return erpProductMapper.deleteErpProductByIds(ids);
}
/**
* 删除闲鱼商品信息
*
* @param id 闲鱼商品主键
* @return 结果
*/
@Override
public int deleteErpProductById(Long id)
{
return erpProductMapper.deleteErpProductById(id);
}
/**
* 从闲鱼ERP拉取商品列表并保存
*
* @param appid ERP应用ID
* @param pageNo 页码
* @param pageSize 每页大小
* @param productStatus 商品状态
* @return 拉取并保存的商品数量
*/
@Override
public int pullAndSaveProductList(String appid, Integer pageNo, Integer pageSize, Integer productStatus)
{
try {
// 解析ERP账号
ERPAccount account = resolveAccount(appid);
// 创建查询请求
ProductListQueryRequest request = new ProductListQueryRequest(account);
if (pageNo != null) {
request.setPageNo(pageNo);
}
if (pageSize != null) {
request.setPageSize(pageSize);
}
if (productStatus != null) {
// API要求的状态值-1(全部), 10(上架), 21(下架), 22(草稿), 23(审核中), 31(已售), 33(已删除), 36(违规)
// 前端传入的简化状态值1(上架), 2(下架), 3(已售)
Integer apiStatus = convertProductStatus(productStatus);
if (apiStatus != null) {
request.setProductStatus(apiStatus);
}
}
// 调用接口获取商品列表
String responseBody = request.getResponseBody();
JSONObject response = JSONObject.parseObject(responseBody);
if (response == null || response.getInteger("code") == null || response.getInteger("code") != 0) {
String errorMsg = response != null ? response.getString("msg") : "未知错误";
log.error("拉取商品列表失败: code={}, msg={}, response={}",
response != null ? response.getInteger("code") : null, errorMsg, responseBody);
throw new RuntimeException("拉取商品列表失败: " + errorMsg);
}
// 解析商品列表
JSONObject data = response.getJSONObject("data");
if (data == null) {
log.warn("拉取商品列表返回数据为空");
return 0;
}
JSONArray productList = data.getJSONArray("list");
Integer totalCount = data.getInteger("count");
if (productList == null || productList.isEmpty()) {
String statusMsg = productStatus != null ? "(状态:" + productStatus + "" : "";
if (totalCount != null && totalCount > 0) {
log.info("拉取商品列表为空,但总数显示为 {},可能是分页问题", totalCount);
} else {
log.info("拉取商品列表为空{},该账号下没有符合条件的商品", statusMsg);
}
return 0;
}
log.info("拉取到 {} 个商品,开始保存", productList.size());
// 转换为实体对象并保存
List<ErpProduct> erpProducts = new ArrayList<>();
for (int i = 0; i < productList.size(); i++) {
JSONObject productJson = productList.getJSONObject(i);
ErpProduct erpProduct = parseProductJson(productJson, appid);
if (erpProduct != null) {
erpProducts.add(erpProduct);
}
}
// 批量保存或更新
if (!erpProducts.isEmpty()) {
// 逐个保存(兼容更新)
int savedCount = 0;
for (ErpProduct product : erpProducts) {
ErpProduct existing = erpProductMapper.selectErpProductByProductIdAndAppid(
product.getProductId(), product.getAppid());
if (existing != null) {
product.setId(existing.getId());
erpProductMapper.updateErpProduct(product);
} else {
erpProductMapper.insertErpProduct(product);
}
savedCount++;
}
log.info("成功拉取并保存 {} 个商品", savedCount);
return savedCount;
}
return 0;
} catch (Exception e) {
log.error("拉取商品列表异常", e);
throw new RuntimeException("拉取商品列表失败: " + e.getMessage(), e);
}
}
/**
* 解析商品JSON数据
*/
private ErpProduct parseProductJson(JSONObject productJson, String appid) {
try {
ErpProduct product = new ErpProduct();
// 管家商品ID
Long productId = productJson.getLong("product_id");
if (productId == null) {
log.warn("商品ID为空跳过: {}", productJson);
return null;
}
product.setProductId(productId);
// 商品标题
product.setTitle(productJson.getString("title"));
// 商品图片(取第一张)
JSONArray images = productJson.getJSONArray("images");
if (images != null && !images.isEmpty()) {
product.setMainImage(images.getString(0));
}
// 价格(分)
Long price = productJson.getLong("price");
if (price == null) {
// 尝试从price字段解析
Object priceObj = productJson.get("price");
if (priceObj instanceof Number) {
price = ((Number) priceObj).longValue();
}
}
product.setPrice(price);
// 库存
Integer stock = productJson.getInteger("stock");
product.setStock(stock);
// 商品状态
Integer productStatus = productJson.getInteger("product_status");
product.setProductStatus(productStatus);
// 销售状态
Integer saleStatus = productJson.getInteger("sale_status");
product.setSaleStatus(saleStatus);
// 闲鱼会员名
product.setUserName(productJson.getString("user_name"));
// 时间字段(时间戳,秒)
product.setOnlineTime(productJson.getLong("online_time"));
product.setOfflineTime(productJson.getLong("offline_time"));
product.setSoldTime(productJson.getLong("sold_time"));
product.setCreateTimeXy(productJson.getLong("create_time"));
product.setUpdateTimeXy(productJson.getLong("update_time"));
// ERP应用ID
product.setAppid(appid);
// 商品链接
product.setProductUrl(productJson.getString("product_url"));
return product;
} catch (Exception e) {
log.error("解析商品JSON失败: {}", productJson, e);
return null;
}
}
/**
* 转换商品状态值将前端状态值转换为API需要的状态值
* 实际状态值:-1(删除), 21(待发布), 22(销售中), 23(已售罄), 31(手动下架), 33(售出下架), 36(自动下架)
* API支持的状态值-1, 10, 21, 22, 23, 31, 33, 36
* 前端传入的状态值直接使用,不做转换
*/
private Integer convertProductStatus(Integer frontendStatus) {
if (frontendStatus == null) {
return null;
}
// 直接使用前端传入的状态值(-1, 21, 22, 23, 31, 33, 36
// API支持的状态值列表
if (frontendStatus == -1 || frontendStatus == 10 || frontendStatus == 21 ||
frontendStatus == 22 || frontendStatus == 23 || frontendStatus == 31 ||
frontendStatus == 33 || frontendStatus == 36) {
return frontendStatus;
}
log.warn("未知的商品状态值: {}, 将不设置状态筛选", frontendStatus);
return null;
}
/**
* 全量同步商品列表(自动遍历所有页码,同步更新和删除)
*/
@Override
public IErpProductService.SyncResult syncAllProducts(String appid, Integer productStatus) {
IErpProductService.SyncResult result = new IErpProductService.SyncResult();
Set<Long> remoteProductIds = new HashSet<>(); // 远程商品ID集合
int pageNo = 1;
int pageSize = 50; // 每页50条尽量少请求次数
int totalPulled = 0;
int added = 0;
int updated = 0;
try {
ERPAccount account = resolveAccount(appid);
// 第一步:遍历所有页码,拉取并保存所有商品
log.info("开始全量同步商品,账号:{}", appid);
while (true) {
ProductListQueryRequest request = new ProductListQueryRequest(account);
request.setPage(pageNo, pageSize);
if (productStatus != null) {
Integer apiStatus = convertProductStatus(productStatus);
if (apiStatus != null) {
request.setProductStatus(apiStatus);
}
}
String responseBody = request.getResponseBody();
JSONObject response = JSONObject.parseObject(responseBody);
if (response == null || response.getInteger("code") == null || response.getInteger("code") != 0) {
String errorMsg = response != null ? response.getString("msg") : "未知错误";
log.error("拉取商品列表失败(页码:{}: {}", pageNo, errorMsg);
result.setFailed(result.getFailed() + 1);
break;
}
JSONObject data = response.getJSONObject("data");
if (data == null) {
log.warn("拉取商品列表返回数据为空(页码:{}", pageNo);
break;
}
JSONArray productList = data.getJSONArray("list");
if (productList == null || productList.isEmpty()) {
log.info("第 {} 页数据为空,同步完成", pageNo);
break;
}
// 处理当前页商品
for (int i = 0; i < productList.size(); i++) {
JSONObject productJson = productList.getJSONObject(i);
ErpProduct erpProduct = parseProductJson(productJson, appid);
if (erpProduct != null && erpProduct.getProductId() != null) {
remoteProductIds.add(erpProduct.getProductId());
// 保存或更新商品
ErpProduct existing = erpProductMapper.selectErpProductByProductIdAndAppid(
erpProduct.getProductId(), erpProduct.getAppid());
if (existing != null) {
erpProduct.setId(existing.getId());
erpProductMapper.updateErpProduct(erpProduct);
updated++;
} else {
erpProductMapper.insertErpProduct(erpProduct);
added++;
}
totalPulled++;
}
}
log.info("已同步第 {} 页,共 {} 条商品", pageNo, productList.size());
// 判断是否还有下一页
if (productList.size() < pageSize) {
log.info("已拉取完所有页码,共 {} 页", pageNo);
break;
}
pageNo++;
}
result.setTotalPulled(totalPulled);
result.setAdded(added);
result.setUpdated(updated);
// 第二步:对比本地和远程,删除本地有但远程没有的商品
log.info("开始同步删除,远程商品数:{}", remoteProductIds.size());
// 查询本地该账号下的所有商品
ErpProduct queryParam = new ErpProduct();
queryParam.setAppid(appid);
List<ErpProduct> localProducts = erpProductMapper.selectErpProductList(queryParam);
// 找出需要删除的商品(本地有但远程没有的)
List<ErpProduct> toDelete = localProducts.stream()
.filter(p -> !remoteProductIds.contains(p.getProductId()))
.collect(Collectors.toList());
if (!toDelete.isEmpty()) {
log.info("发现 {} 个本地商品在远程已不存在,开始删除", toDelete.size());
for (ErpProduct product : toDelete) {
try {
// 先调用远程删除接口
ProductDeleteRequest deleteRequest = new ProductDeleteRequest(account);
deleteRequest.setProductId(product.getProductId());
String deleteResponse = deleteRequest.getResponseBody();
JSONObject deleteResult = JSONObject.parseObject(deleteResponse);
if (deleteResult != null && deleteResult.getInteger("code") != null &&
deleteResult.getInteger("code") == 0) {
// 远程删除成功,删除本地记录
erpProductMapper.deleteErpProductById(product.getId());
result.setDeleted(result.getDeleted() + 1);
log.debug("成功删除商品:{}", product.getProductId());
} else {
// 远程删除失败,记录日志但不删除本地(可能是远程已经删除了)
String errorMsg = deleteResult != null ? deleteResult.getString("msg") : "未知错误";
log.warn("远程删除商品失败(可能已不存在):{},错误:{}", product.getProductId(), errorMsg);
// 如果远程返回商品不存在的错误,也删除本地记录
if (errorMsg != null && (errorMsg.contains("不存在") || errorMsg.contains("not found"))) {
erpProductMapper.deleteErpProductById(product.getId());
result.setDeleted(result.getDeleted() + 1);
} else {
result.setFailed(result.getFailed() + 1);
}
}
} catch (Exception e) {
log.error("删除商品异常:{}", product.getProductId(), e);
result.setFailed(result.getFailed() + 1);
}
}
}
// 构建结果消息
StringBuilder msg = new StringBuilder();
msg.append(String.format("同步完成!拉取:%d个新增%d个更新%d个删除%d个",
totalPulled, added, updated, result.getDeleted()));
if (result.getFailed() > 0) {
msg.append(String.format(",失败:%d个", result.getFailed()));
}
result.setMessage(msg.toString());
log.info(result.getMessage());
return result;
} catch (Exception e) {
log.error("全量同步商品异常", e);
result.setMessage("同步失败: " + e.getMessage());
result.setFailed(result.getFailed() + 1);
throw new RuntimeException("全量同步商品失败: " + e.getMessage(), e);
}
}
/**
* 解析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

@@ -5,13 +5,17 @@ import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.jarvis.domain.JDOrder;
import com.ruoyi.jarvis.service.ILogisticsService;
import com.ruoyi.system.service.ISysConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.net.URLEncoder;
import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
@@ -22,13 +26,19 @@ public class LogisticsServiceImpl implements ILogisticsService {
private static final Logger logger = LoggerFactory.getLogger(LogisticsServiceImpl.class);
private static final String REDIS_WAYBILL_KEY_PREFIX = "logistics:waybill:order:";
private static final String REDIS_LOCK_KEY_PREFIX = "logistics:lock:order:";
private static final String EXTERNAL_API_URL = "http://192.168.8.88:5001/fetch_logistics?tracking_url=";
private static final String PUSH_URL = "https://wxts.van333.cn/wx/send/pdd";
private static final String PUSH_TOKEN = "super_token_b62190c26";
private static final String CONFIG_KEY_PREFIX = "logistics.push.touser.";
private static final long LOCK_EXPIRE_SECONDS = 300; // 锁过期时间5分钟防止死锁
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private ISysConfigService sysConfigService;
@Override
public boolean isOrderProcessed(Long orderId) {
if (orderId == null) {
@@ -45,27 +55,49 @@ public class LogisticsServiceImpl implements ILogisticsService {
return false;
}
// 检查物流链接
String logisticsLink = order.getLogisticsLink();
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
logger.info("订单暂无物流链接,跳过处理 - 订单ID: {}", order.getId());
return false;
Long orderId = order.getId();
// 双重检查:先检查是否已处理过
if (isOrderProcessed(orderId)) {
logger.info("订单已处理过,跳过 - 订单ID: {}", orderId);
return true; // 返回true表示已处理避免重复处理
}
// 获取分布式锁,防止并发处理同一订单
String lockKey = REDIS_LOCK_KEY_PREFIX + orderId;
Boolean lockAcquired = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "locked", LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(lockAcquired)) {
logger.warn("订单正在被其他线程处理,跳过 - 订单ID: {}", orderId);
return false; // 其他线程正在处理返回false让调用方稍后重试
}
try {
// 获取锁后再次检查是否已处理(双重检查锁定模式)
if (isOrderProcessed(orderId)) {
logger.info("订单在获取锁后检查发现已处理,跳过 - 订单ID: {}", orderId);
return true;
}
// 检查物流链接
String logisticsLink = order.getLogisticsLink();
if (logisticsLink == null || logisticsLink.trim().isEmpty()) {
logger.info("订单暂无物流链接,跳过处理 - 订单ID: {}", orderId);
return false;
}
// 构建外部接口URL
String externalUrl = EXTERNAL_API_URL + URLEncoder.encode(logisticsLink, "UTF-8");
logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", order.getId(), externalUrl);
logger.info("调用外部接口获取物流信息 - 订单ID: {}, URL: {}", orderId, externalUrl);
// 在服务端执行HTTP请求
String result = HttpUtils.sendGet(externalUrl);
if (result == null || result.trim().isEmpty()) {
logger.warn("外部接口返回空结果 - 订单ID: {}", order.getId());
logger.warn("外部接口返回空结果 - 订单ID: {}", orderId);
return false;
}
logger.info("外部接口调用成功 - 订单ID: {}, 返回数据长度: {}", order.getId(), result.length());
logger.info("外部接口调用成功 - 订单ID: {}, 返回数据长度: {}", orderId, result.length());
// 解析返回结果
JSONObject parsedData = null;
@@ -74,42 +106,97 @@ public class LogisticsServiceImpl implements ILogisticsService {
if (parsed instanceof JSONObject) {
parsedData = (JSONObject) parsed;
} else {
logger.warn("返回数据不是JSON对象格式 - 订单ID: {}", order.getId());
logger.warn("返回数据不是JSON对象格式 - 订单ID: {}", orderId);
return false;
}
} catch (Exception e) {
logger.warn("解析返回数据失败 - 订单ID: {}, 错误: {}", order.getId(), e.getMessage());
logger.warn("解析返回数据失败 - 订单ID: {}, 错误: {}", orderId, e.getMessage());
return false;
}
// 检查waybill_no
JSONObject dataObj = parsedData.getJSONObject("data");
if (dataObj == null) {
logger.info("返回数据中没有data字段 - 订单ID: {}", order.getId());
logger.info("返回数据中没有data字段 - 订单ID: {}", orderId);
return false;
}
String waybillNo = dataObj.getString("waybill_no");
if (waybillNo == null || waybillNo.trim().isEmpty()) {
logger.info("waybill_no为空无需处理 - 订单ID: {}", order.getId());
logger.info("waybill_no为空无需处理 - 订单ID: {}", orderId);
return false;
}
logger.info("检测到waybill_no: {} - 订单ID: {}", waybillNo, order.getId());
logger.info("检测到waybill_no: {} - 订单ID: {}", waybillNo, orderId);
// 保存运单号到Redis避免重复处理
String redisKey = REDIS_WAYBILL_KEY_PREFIX + order.getId();
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
// 兼容处理检查Redis中是否已有该订单的运单号记录
// 如果存在且运单号一致,说明之前已经推送过了(可能是之前没有配置接收人但推送成功的情况)
String redisKey = REDIS_WAYBILL_KEY_PREFIX + orderId;
String existingWaybillNo = stringRedisTemplate.opsForValue().get(redisKey);
// 调用企业应用推送
sendEnterprisePushNotification(order, waybillNo);
if (existingWaybillNo != null && existingWaybillNo.trim().equals(waybillNo.trim())) {
// 运单号一致,说明之前已经推送过了,直接标记为已处理,跳过推送
logger.info("订单运单号已存在且一致,说明之前已推送过,跳过重复推送 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
// 更新过期时间,确保记录不会过期
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
return true;
}
logger.info("物流信息获取并推送成功 - 订单ID: {}, waybill_no: {}", order.getId(), waybillNo);
// 兼容处理如果Redis中没有记录但订单创建时间在30天之前且获取到了运单号
// 说明可能是之前推送过但没标记的情况(比如之前没有配置接收人但推送成功,响应解析失败)
// 这种情况下,直接标记为已处理,跳过推送,避免重复推送旧订单
if (existingWaybillNo == null && order.getCreateTime() != null) {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, -30); // 30天前
Date thresholdDate = calendar.getTime();
if (order.getCreateTime().before(thresholdDate)) {
// 订单创建时间在30天之前且Redis中没有记录但获取到了运单号
// 视为之前已推送过但未标记,直接标记为已处理,跳过推送
logger.info("订单创建时间较早({}且Redis中无记录但已获取到运单号视为之前已推送过直接标记为已处理跳过推送 - 订单ID: {}, waybill_no: {}",
order.getCreateTime(), orderId, waybillNo);
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
return true;
}
}
// 如果Redis中有记录但运单号不一致记录警告
if (existingWaybillNo != null && !existingWaybillNo.trim().equals(waybillNo.trim())) {
logger.warn("订单运单号发生变化 - 订单ID: {}, 旧运单号: {}, 新运单号: {}, 将重新推送",
orderId, existingWaybillNo, waybillNo);
}
// 调用企业应用推送,只有推送成功才记录状态
boolean pushSuccess = sendEnterprisePushNotification(order, waybillNo);
if (!pushSuccess) {
logger.warn("企业微信推送未确认成功,稍后将重试 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
return false;
}
// 保存运单号到Redis避免重复处理- 使用原子操作确保只写入一次
Boolean setSuccess = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, waybillNo, 30, TimeUnit.DAYS);
if (Boolean.FALSE.equals(setSuccess)) {
// 如果Redis中已存在说明可能被其他线程处理了记录警告但不算失败
logger.warn("订单运单号已存在(可能被并发处理),但推送已成功 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
// 更新过期时间,确保记录不会过期
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
}
logger.info("物流信息获取并推送成功 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
return true;
} catch (Exception e) {
logger.error("获取物流信息失败 - 订单ID: {}, 错误: {}", order.getId(), e.getMessage(), e);
logger.error("获取物流信息失败 - 订单ID: {}, 错误: {}", orderId, e.getMessage(), e);
return false;
} finally {
// 释放分布式锁
try {
stringRedisTemplate.delete(lockKey);
logger.debug("释放订单处理锁 - 订单ID: {}", orderId);
} catch (Exception e) {
logger.warn("释放订单处理锁失败 - 订单ID: {}, 错误: {}", orderId, e.getMessage());
}
}
}
@@ -118,7 +205,7 @@ public class LogisticsServiceImpl implements ILogisticsService {
* @param order 订单信息
* @param waybillNo 运单号
*/
private void sendEnterprisePushNotification(JDOrder order, String waybillNo) {
private boolean sendEnterprisePushNotification(JDOrder order, String waybillNo) {
try {
// 构建推送消息内容
StringBuilder pushContent = new StringBuilder();
@@ -127,12 +214,9 @@ public class LogisticsServiceImpl implements ILogisticsService {
String distributionMark = order.getDistributionMark() != null ? order.getDistributionMark() : "未知";
pushContent.append(distributionMark).append("\n");
// PDD订单包含第三方单号F订单不包含
if ("PDD".equals(distributionMark)) {
String thirdPartyOrderNo = order.getThirdPartyOrderNo();
if (thirdPartyOrderNo != null && !thirdPartyOrderNo.trim().isEmpty()) {
pushContent.append("第三方单号:").append(thirdPartyOrderNo).append("\n");
}
String thirdPartyOrderNo = order.getThirdPartyOrderNo();
if (thirdPartyOrderNo != null && !thirdPartyOrderNo.trim().isEmpty()) {
pushContent.append("第三方单号:").append(thirdPartyOrderNo).append("\n");
}
// 型号
@@ -140,22 +224,182 @@ public class LogisticsServiceImpl implements ILogisticsService {
// 收货地址
pushContent.append("收货地址:").append(order.getAddress() != null ? order.getAddress() : "").append("\n");
// 运单号
pushContent.append("运单号:").append(waybillNo).append("\n");
pushContent.append("运单号:").append("\n").append("\n").append("\n").append("\n").append(waybillNo).append("\n");
// 调用企业微信推送接口
JSONObject pushParam = new JSONObject();
pushParam.put("title", "JD物流信息推送");
pushParam.put("text", pushContent.toString());
// 根据分销标识获取接收人列表
String touser = getTouserByDistributionMark(distributionMark);
if (StringUtils.hasText(touser)) {
pushParam.put("touser", touser);
logger.info("企业微信推送设置接收人 - 订单ID: {}, 分销标识: {}, 接收人: {}",
order.getId(), distributionMark, touser);
} else {
// 未配置接收人时,使用远程接口的默认接收人,这是正常情况
logger.info("未找到分销标识对应的接收人配置,将使用远程接口默认接收人 - 订单ID: {}, 分销标识: {}",
order.getId(), distributionMark);
}
// 记录完整的推送参数(用于调试)
String jsonBody = pushParam.toJSONString();
logger.info("企业微信推送完整参数 - 订单ID: {}, JSON: {}", order.getId(), jsonBody);
// 使用支持自定义header的HTTP请求
String pushResult = sendPostWithHeaders(PUSH_URL, pushParam.toJSONString(), PUSH_TOKEN);
logger.info("企业应用推送调用结果 - 订单ID: {}, waybill_no: {}, 推送结果: {}",
order.getId(), waybillNo, pushResult);
String pushResult = sendPostWithHeaders(PUSH_URL, jsonBody, PUSH_TOKEN);
if (pushResult == null || pushResult.trim().isEmpty()) {
logger.warn("企业应用推送响应为空 - 订单ID: {}, waybill_no: {}", order.getId(), waybillNo);
return false;
}
boolean success = isPushResponseSuccess(pushResult);
if (success) {
logger.info("企业应用推送成功 - 订单ID: {}, waybill_no: {}, 推送结果: {}",
order.getId(), waybillNo, pushResult);
} else {
logger.warn("企业应用推送响应未确认成功 - 订单ID: {}, waybill_no: {}, 响应: {}",
order.getId(), waybillNo, pushResult);
}
return success;
} catch (Exception e) {
logger.error("调用企业应用推送失败 - 订单ID: {}, waybill_no: {}, 错误: {}",
order.getId(), waybillNo, e.getMessage(), e);
// 不抛出异常,避免影响主流程
// 不抛出异常,主流程根据返回值决定是否重试
return false;
}
}
/**
* 根据分销标识获取接收人列表
* 从系统配置中读取配置键名格式logistics.push.touser.{分销标识}
* 配置值格式接收人1,接收人2,接收人3逗号分隔
*
* @param distributionMark 分销标识
* @return 接收人列表逗号分隔如果未配置则返回null
*/
private String getTouserByDistributionMark(String distributionMark) {
if (!StringUtils.hasText(distributionMark)) {
logger.warn("分销标识为空,无法获取接收人配置");
return null;
}
try {
// 构建配置键名
String configKey = CONFIG_KEY_PREFIX + distributionMark.trim();
// 从系统配置中获取接收人列表
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;
}
} catch (Exception e) {
logger.error("获取接收人配置失败 - 分销标识: {}, 错误: {}", distributionMark, e.getMessage(), e);
return null;
}
}
/**
* 判断推送返回值是否为成功状态
* @param pushResult 推送接口返回结果
* @return 是否成功
*/
private boolean isPushResponseSuccess(String pushResult) {
if (pushResult == null || pushResult.trim().isEmpty()) {
logger.warn("推送响应为空,视为失败");
return false;
}
try {
JSONObject response = JSON.parseObject(pushResult);
if (response == null) {
logger.warn("推送响应解析为null视为失败。原始响应: {}", pushResult);
return false;
}
Integer code = response.getInteger("code");
Boolean successFlag = response.getBoolean("success");
String status = response.getString("status");
String message = response.getString("msg");
String errcode = response.getString("errcode");
// 记录完整的响应信息用于调试
logger.debug("推送响应解析 - code: {}, success: {}, status: {}, msg: {}, errcode: {}",
code, successFlag, status, message, errcode);
// 检查错误码errcode为0表示成功
if (errcode != null && "0".equals(errcode)) {
logger.info("推送成功通过errcode=0判断");
return true;
}
// 检查code字段0或200表示成功
if (code != null && (code == 0 || code == 200)) {
logger.info("推送成功通过code={}判断)", code);
return true;
}
// 检查success字段
if (Boolean.TRUE.equals(successFlag)) {
logger.info("推送成功通过success=true判断");
return true;
}
// 检查status字段
if (status != null && ("success".equalsIgnoreCase(status) || "ok".equalsIgnoreCase(status))) {
logger.info("推送成功通过status={}判断)", status);
return true;
}
// 检查message字段某些接口可能用message表示成功
if (message != null && ("success".equalsIgnoreCase(message) || "ok".equalsIgnoreCase(message))) {
logger.info("推送成功通过message={}判断)", message);
return true;
}
// 检查是否包含明确的错误标识
String responseStr = pushResult.toLowerCase();
boolean hasErrorKeyword = responseStr.contains("\"error\"") || responseStr.contains("\"fail\"") ||
responseStr.contains("\"failed\"") || responseStr.contains("\"errmsg\"");
// 如果包含错误标识,检查是否有明确的错误码
if (hasErrorKeyword) {
if (code != null && code < 0) {
logger.warn("推送失败(检测到错误标识和负错误码) - code: {}, 完整响应: {}", code, pushResult);
return false;
}
if (errcode != null && !"0".equals(errcode) && !errcode.isEmpty()) {
logger.warn("推送失败(检测到错误标识和非零错误码) - errcode: {}, 完整响应: {}", errcode, pushResult);
return false;
}
}
// 如果响应是有效的JSON且没有明确的错误标识视为成功
// 因为远程接口有默认接收人,即使没有配置接收人,推送也应该成功
// 如果响应格式特殊(不是标准格式),只要没有错误,也视为成功
if (!hasErrorKeyword) {
logger.info("推送成功(响应格式有效且无错误标识,使用默认接收人) - 完整响应: {}", pushResult);
return true;
}
// 如果所有判断都失败,记录详细信息
logger.warn("推送响应未确认成功 - code: {}, success: {}, status: {}, msg: {}, errcode: {}, 完整响应: {}",
code, successFlag, status, message, errcode, pushResult);
return false;
} catch (Exception e) {
logger.error("解析企业应用推送响应失败,将视为未成功。原始响应: {}, 错误: {}", pushResult, e.getMessage(), e);
return false;
}
}

View File

@@ -0,0 +1,421 @@
package com.ruoyi.jarvis.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.jarvis.service.ISocialMediaService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 小红书/抖音内容生成Service业务层处理
*
* @author ruoyi
* @date 2025-01-XX
*/
@Service
public class SocialMediaServiceImpl implements ISocialMediaService
{
private static final Logger log = LoggerFactory.getLogger(SocialMediaServiceImpl.class);
@Autowired(required = false)
private StringRedisTemplate redisTemplate;
// jarvis_java 服务地址
private final static String JARVIS_BASE_URL = "http://192.168.8.88:6666";
// Redis Key 前缀
private static final String REDIS_KEY_PREFIX = "social_media:prompt:";
// 模板键名列表
private static final String[] TEMPLATE_KEYS = {
"keywords",
"content:xhs",
"content:douyin",
"content:both"
};
// 模板说明
private static final Map<String, String> TEMPLATE_DESCRIPTIONS = new HashMap<String, String>() {{
put("keywords", "关键词提取提示词模板\n占位符%s - 商品名称");
put("content:xhs", "小红书文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:douyin", "抖音文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
put("content:both", "通用文案生成提示词模板\n占位符%s - 商品名称,%s - 价格信息,%s - 关键词信息");
}};
/**
* 提取商品标题关键词
*/
@Override
public Map<String, Object> extractKeywords(String productName) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 调用 jarvis_java 的接口
String url = JARVIS_BASE_URL + "/jarvis/social-media/extract-keywords";
JSONObject requestBody = new JSONObject();
requestBody.put("productName", productName);
log.info("调用jarvis_java提取关键词接口URL: {}, 参数: {}", url, requestBody.toJSONString());
String response = HttpUtils.sendJsonPost(url, requestBody.toJSONString());
log.info("jarvis_java响应: {}", response);
if (StringUtils.isEmpty(response)) {
throw new Exception("jarvis_java返回空结果");
}
// 解析响应
Object parsed = JSON.parse(response);
if (parsed instanceof JSONObject) {
JSONObject jsonResponse = (JSONObject) parsed;
if (jsonResponse.getInteger("code") == 200) {
Object data = jsonResponse.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
return dataMap;
}
} else {
String msg = jsonResponse.getString("msg");
result.put("success", false);
result.put("error", msg != null ? msg : "提取关键词失败");
return result;
}
}
result.put("success", false);
result.put("error", "响应格式错误");
return result;
} catch (Exception e) {
log.error("提取关键词失败", e);
result.put("success", false);
result.put("error", "提取关键词失败: " + e.getMessage());
return result;
}
}
/**
* 生成文案
*/
@Override
public Map<String, Object> generateContent(String productName, Object originalPrice,
Object finalPrice, String keywords, String style) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 调用 jarvis_java 的接口
String url = JARVIS_BASE_URL + "/jarvis/social-media/generate-content";
JSONObject requestBody = new JSONObject();
requestBody.put("productName", productName);
if (originalPrice != null) {
requestBody.put("originalPrice", parseDouble(originalPrice));
}
if (finalPrice != null) {
requestBody.put("finalPrice", parseDouble(finalPrice));
}
if (StringUtils.isNotEmpty(keywords)) {
requestBody.put("keywords", keywords);
}
if (StringUtils.isNotEmpty(style)) {
requestBody.put("style", style);
}
log.info("调用jarvis_java生成文案接口URL: {}, 参数: {}", url, requestBody.toJSONString());
String response = HttpUtils.sendJsonPost(url, requestBody.toJSONString());
log.info("jarvis_java响应: {}", response);
if (StringUtils.isEmpty(response)) {
throw new Exception("jarvis_java返回空结果");
}
// 解析响应
Object parsed = JSON.parse(response);
if (parsed instanceof JSONObject) {
JSONObject jsonResponse = (JSONObject) parsed;
if (jsonResponse.getInteger("code") == 200) {
Object data = jsonResponse.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
return dataMap;
}
} else {
String msg = jsonResponse.getString("msg");
result.put("success", false);
result.put("error", msg != null ? msg : "生成文案失败");
return result;
}
}
result.put("success", false);
result.put("error", "响应格式错误");
return result;
} catch (Exception e) {
log.error("生成文案失败", e);
result.put("success", false);
result.put("error", "生成文案失败: " + e.getMessage());
return result;
}
}
/**
* 一键生成完整内容(关键词 + 文案 + 图片)
*/
@Override
public Map<String, Object> generateCompleteContent(String productImageUrl, String productName,
Object originalPrice, Object finalPrice, String style) {
Map<String, Object> result = new HashMap<>();
if (StringUtils.isEmpty(productName)) {
result.put("success", false);
result.put("error", "商品名称不能为空");
return result;
}
try {
// 调用 jarvis_java 的接口
String url = JARVIS_BASE_URL + "/jarvis/social-media/generate-complete";
JSONObject requestBody = new JSONObject();
if (StringUtils.isNotEmpty(productImageUrl)) {
requestBody.put("productImageUrl", productImageUrl);
}
requestBody.put("productName", productName);
if (originalPrice != null) {
requestBody.put("originalPrice", parseDouble(originalPrice));
}
if (finalPrice != null) {
requestBody.put("finalPrice", parseDouble(finalPrice));
}
if (StringUtils.isNotEmpty(style)) {
requestBody.put("style", style);
}
log.info("调用jarvis_java生成完整内容接口URL: {}, 参数: {}", url, requestBody.toJSONString());
String response = HttpUtils.sendJsonPost(url, requestBody.toJSONString());
log.info("jarvis_java响应: {}", response);
if (StringUtils.isEmpty(response)) {
throw new Exception("jarvis_java返回空结果");
}
// 解析响应
Object parsed = JSON.parse(response);
if (parsed instanceof JSONObject) {
JSONObject jsonResponse = (JSONObject) parsed;
if (jsonResponse.getInteger("code") == 200) {
Object data = jsonResponse.get("data");
if (data instanceof Map) {
@SuppressWarnings("unchecked")
Map<String, Object> dataMap = (Map<String, Object>) data;
return dataMap;
}
} else {
String msg = jsonResponse.getString("msg");
result.put("success", false);
result.put("error", msg != null ? msg : "生成完整内容失败");
return result;
}
}
result.put("success", false);
result.put("error", "响应格式错误");
return result;
} catch (Exception e) {
log.error("生成完整内容失败", e);
result.put("success", false);
result.put("error", "生成完整内容失败: " + e.getMessage());
return result;
}
}
/**
* 获取提示词模板列表
*/
@Override
public AjaxResult listPromptTemplates() {
try {
Map<String, Object> templates = new HashMap<>();
for (String key : TEMPLATE_KEYS) {
Map<String, Object> templateInfo = new HashMap<>();
templateInfo.put("key", key);
templateInfo.put("description", TEMPLATE_DESCRIPTIONS.get(key));
String template = getTemplateFromRedis(key);
templateInfo.put("template", template);
templateInfo.put("isDefault", template == null);
templates.put(key, templateInfo);
}
return AjaxResult.success(templates);
} catch (Exception e) {
log.error("获取提示词模板列表失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 获取单个提示词模板
*/
@Override
public AjaxResult getPromptTemplate(String key) {
try {
if (!isValidKey(key)) {
return AjaxResult.error("无效的模板键名");
}
String template = getTemplateFromRedis(key);
Map<String, Object> data = new HashMap<>();
data.put("key", key);
data.put("description", TEMPLATE_DESCRIPTIONS.get(key));
data.put("template", template);
data.put("isDefault", template == null);
return AjaxResult.success(data);
} catch (Exception e) {
log.error("获取提示词模板失败", e);
return AjaxResult.error("获取失败: " + e.getMessage());
}
}
/**
* 保存提示词模板
*/
@Override
public AjaxResult savePromptTemplate(Map<String, Object> request) {
try {
String key = (String) request.get("key");
String template = (String) request.get("template");
if (!isValidKey(key)) {
return AjaxResult.error("无效的模板键名");
}
if (StringUtils.isEmpty(template)) {
return AjaxResult.error("模板内容不能为空");
}
if (redisTemplate == null) {
return AjaxResult.error("Redis未配置无法保存模板");
}
String redisKey = REDIS_KEY_PREFIX + key;
String templateValue = template.trim();
if (StringUtils.isEmpty(templateValue)) {
return AjaxResult.error("模板内容不能为空");
}
redisTemplate.opsForValue().set(redisKey, templateValue);
log.info("保存提示词模板成功: {}", key);
return AjaxResult.success("保存成功");
} catch (Exception e) {
log.error("保存提示词模板失败", e);
return AjaxResult.error("保存失败: " + e.getMessage());
}
}
/**
* 删除提示词模板(恢复默认)
*/
@Override
public AjaxResult deletePromptTemplate(String key) {
try {
if (!isValidKey(key)) {
return AjaxResult.error("无效的模板键名");
}
if (redisTemplate == null) {
return AjaxResult.error("Redis未配置无法删除模板");
}
String redisKey = REDIS_KEY_PREFIX + key;
redisTemplate.delete(redisKey);
log.info("删除提示词模板成功: {}", key);
return AjaxResult.success("删除成功,已恢复默认模板");
} catch (Exception e) {
log.error("删除提示词模板失败", e);
return AjaxResult.error("删除失败: " + e.getMessage());
}
}
/**
* 从 Redis 获取模板
*/
private String getTemplateFromRedis(String key) {
if (redisTemplate == null) {
return null;
}
try {
String redisKey = REDIS_KEY_PREFIX + key;
String template = redisTemplate.opsForValue().get(redisKey);
return StringUtils.isNotEmpty(template) ? template : null;
} catch (Exception e) {
log.warn("读取Redis模板失败: {}", key, e);
return null;
}
}
/**
* 验证模板键名是否有效
*/
private boolean isValidKey(String key) {
if (StringUtils.isEmpty(key)) {
return false;
}
for (String validKey : TEMPLATE_KEYS) {
if (validKey.equals(key)) {
return true;
}
}
return false;
}
/**
* 解析Double值
*/
private Double parseDouble(Object value) {
if (value == null) {
return null;
}
if (value instanceof Double) {
return (Double) value;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
return null;
}
}
}

View File

@@ -7,8 +7,11 @@ import com.ruoyi.jarvis.service.ITencentDocDelayedPushService;
import com.ruoyi.jarvis.service.ITencentDocTokenService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -31,7 +34,7 @@ import java.util.concurrent.TimeUnit;
* @author system
*/
@Service
public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushService {
public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushService, ApplicationContextAware {
private static final Logger log = LoggerFactory.getLogger(TencentDocDelayedPushServiceImpl.class);
@@ -46,6 +49,8 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
@Autowired
private ITencentDocTokenService tokenService;
private ApplicationContext applicationContext;
/**
* 延迟时间(分钟),可通过配置文件修改
@@ -73,6 +78,11 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
*/
private ScheduledExecutorService scheduler;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
/**
* 初始化定时任务
*/
@@ -257,13 +267,21 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
try {
log.info("开始执行批量同步...");
// 获取配置信息
String fileId = tencentDocConfig.getFileId();
String sheetId = tencentDocConfig.getSheetId();
Integer startRow = tencentDocConfig.getStartRow();
// 从 Redis 读取配置信息(用户通过前端配置页面设置)
// 注意:使用与 TencentDocConfigController 相同的 key 前缀
final String CONFIG_KEY_PREFIX = "tencent:doc:auto:config:";
String fileId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "fileId");
String sheetId = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "sheetId");
Integer startRow = redisCache.getCacheObject(CONFIG_KEY_PREFIX + "startRow");
if (startRow == null) {
startRow = 3; // 默认值
}
log.info("读取配置 - fileId: {}, sheetId: {}, startRow: {}", fileId, sheetId, startRow);
if (StringUtils.isEmpty(fileId) || StringUtils.isEmpty(sheetId)) {
log.error("腾讯文档配置不完整,无法执行批量同步");
log.error("腾讯文档配置不完整,无法执行批量同步。请先在前端配置页面设置文件ID和工作表ID");
return;
}
@@ -278,39 +296,36 @@ public class TencentDocDelayedPushServiceImpl implements ITencentDocDelayedPushS
);
log.info("✓ 创建批量推送记录批次ID: {}", batchId);
// 调用批量同步接口传递批次ID
java.net.URL url = new java.net.URL("http://localhost:30313/jarvis-api/jarvis/tendoc/fillLogisticsByOrderNo?batchId=" + batchId);
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/json");
conn.setDoOutput(true);
// 发送空JSON对象
try (java.io.OutputStream os = conn.getOutputStream()) {
byte[] input = "{}".getBytes("utf-8");
os.write(input, 0, input.length);
}
int responseCode = conn.getResponseCode();
log.info("批量同步调用完成,响应码: {}", responseCode);
if (responseCode == 200) {
// 读取响应
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(conn.getInputStream(), "utf-8"))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
log.info("批量同步结果: {}", response.toString());
}
} else {
log.error("批量同步调用失败,响应码: {}", responseCode);
// 更新批量推送记录为失败状态
// 直接通过 ApplicationContext 获取 Controller Bean 并调用方法
// 这样避免了 HTTP 调用,是后端内部方法调用
try {
log.info("开始调用批量同步方法(后端内部调用)...");
// 获取 TencentDocController Bean
Object controller = applicationContext.getBean("tencentDocController");
// 通过反射调用 fillLogisticsByOrderNo 方法
java.lang.reflect.Method method = controller.getClass().getMethod(
"fillLogisticsByOrderNo",
java.util.Map.class
);
// 构造参数
java.util.Map<String, Object> params = new java.util.HashMap<>();
params.put("batchId", batchId);
params.put("fileId", fileId);
params.put("sheetId", sheetId);
// 调用方法
Object result = method.invoke(controller, params);
log.info("✓ 批量同步执行完成,结果: {}", result);
} catch (Exception ex) {
log.error("批量同步调用失败", ex);
if (batchId != null) {
batchPushService.updateBatchPushRecord(batchId, "FAILED", 0, 0, 0,
null, "批量同步调用失败,响应码: " + responseCode);
null, "批量同步调用失败: " + ex.getMessage());
}
}

View File

@@ -29,7 +29,7 @@ public class LogisticsScanTask {
* 定时任务每1小时执行一次
* Cron表达式0 0 * * * ? 表示每小时的第0分钟执行
*/
@Scheduled(cron = "0 */30 * * * ?")
@Scheduled(cron = "0 */20 * * * ?")
public void scanAndFetchLogistics() {
logger.info("========== 开始执行物流信息扫描定时任务 ==========");

View File

@@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.jarvis.mapper.ErpProductMapper">
<resultMap type="ErpProduct" id="ErpProductResult">
<result property="id" column="id" />
<result property="productId" column="product_id" />
<result property="title" column="title" />
<result property="mainImage" column="main_image" />
<result property="price" column="price" />
<result property="stock" column="stock" />
<result property="productStatus" column="product_status" />
<result property="saleStatus" column="sale_status" />
<result property="userName" column="user_name" />
<result property="onlineTime" column="online_time" />
<result property="offlineTime" column="offline_time" />
<result property="soldTime" column="sold_time" />
<result property="createTimeXy" column="create_time_xy" />
<result property="updateTimeXy" column="update_time_xy" />
<result property="appid" column="appid" />
<result property="productUrl" column="product_url" />
<result property="remark" column="remark" />
<result property="createTime" column="create_time" />
<result property="updateTime" column="update_time" />
</resultMap>
<sql id="selectErpProductVo">
select id, product_id, title, main_image, price, stock, product_status, sale_status,
user_name, online_time, offline_time, sold_time, create_time_xy, update_time_xy,
appid, product_url, remark, create_time, update_time
from erp_product
</sql>
<select id="selectErpProductList" parameterType="ErpProduct" resultMap="ErpProductResult">
<include refid="selectErpProductVo"/>
<where>
<if test="productId != null "> and product_id = #{productId}</if>
<if test="title != null and title != ''"> and title like concat('%', #{title}, '%')</if>
<if test="productStatus != null "> and product_status = #{productStatus}</if>
<if test="saleStatus != null "> and sale_status = #{saleStatus}</if>
<if test="userName != null and userName != ''"> and user_name = #{userName}</if>
<if test="appid != null and appid != ''"> and appid = #{appid}</if>
</where>
order by update_time_xy desc, id desc
</select>
<select id="selectErpProductById" parameterType="Long" resultMap="ErpProductResult">
<include refid="selectErpProductVo"/>
where id = #{id}
</select>
<select id="selectErpProductByProductIdAndAppid" resultMap="ErpProductResult">
<include refid="selectErpProductVo"/>
where product_id = #{productId} and appid = #{appid}
</select>
<insert id="insertErpProduct" parameterType="ErpProduct" useGeneratedKeys="true" keyProperty="id">
insert into erp_product
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="productId != null">product_id,</if>
<if test="title != null">title,</if>
<if test="mainImage != null">main_image,</if>
<if test="price != null">price,</if>
<if test="stock != null">stock,</if>
<if test="productStatus != null">product_status,</if>
<if test="saleStatus != null">sale_status,</if>
<if test="userName != null">user_name,</if>
<if test="onlineTime != null">online_time,</if>
<if test="offlineTime != null">offline_time,</if>
<if test="soldTime != null">sold_time,</if>
<if test="createTimeXy != null">create_time_xy,</if>
<if test="updateTimeXy != null">update_time_xy,</if>
<if test="appid != null">appid,</if>
<if test="productUrl != null">product_url,</if>
<if test="remark != null">remark,</if>
<if test="createTime != null">create_time,</if>
<if test="updateTime != null">update_time,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="productId != null">#{productId},</if>
<if test="title != null">#{title},</if>
<if test="mainImage != null">#{mainImage},</if>
<if test="price != null">#{price},</if>
<if test="stock != null">#{stock},</if>
<if test="productStatus != null">#{productStatus},</if>
<if test="saleStatus != null">#{saleStatus},</if>
<if test="userName != null">#{userName},</if>
<if test="onlineTime != null">#{onlineTime},</if>
<if test="offlineTime != null">#{offlineTime},</if>
<if test="soldTime != null">#{soldTime},</if>
<if test="createTimeXy != null">#{createTimeXy},</if>
<if test="updateTimeXy != null">#{updateTimeXy},</if>
<if test="appid != null">#{appid},</if>
<if test="productUrl != null">#{productUrl},</if>
<if test="remark != null">#{remark},</if>
<if test="createTime != null">#{createTime},</if>
<if test="updateTime != null">#{updateTime},</if>
</trim>
</insert>
<update id="updateErpProduct" parameterType="ErpProduct">
update erp_product
<trim prefix="SET" suffixOverrides=",">
<if test="productId != null">product_id = #{productId},</if>
<if test="title != null">title = #{title},</if>
<if test="mainImage != null">main_image = #{mainImage},</if>
<if test="price != null">price = #{price},</if>
<if test="stock != null">stock = #{stock},</if>
<if test="productStatus != null">product_status = #{productStatus},</if>
<if test="saleStatus != null">sale_status = #{saleStatus},</if>
<if test="userName != null">user_name = #{userName},</if>
<if test="onlineTime != null">online_time = #{onlineTime},</if>
<if test="offlineTime != null">offline_time = #{offlineTime},</if>
<if test="soldTime != null">sold_time = #{soldTime},</if>
<if test="createTimeXy != null">create_time_xy = #{createTimeXy},</if>
<if test="updateTimeXy != null">update_time_xy = #{updateTimeXy},</if>
<if test="appid != null">appid = #{appid},</if>
<if test="productUrl != null">product_url = #{productUrl},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</trim>
where id = #{id}
</update>
<delete id="deleteErpProductById" parameterType="Long">
delete from erp_product where id = #{id}
</delete>
<delete id="deleteErpProductByIds" parameterType="String">
delete from erp_product where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
<insert id="batchInsertOrUpdateErpProduct" parameterType="java.util.List">
insert into erp_product
(product_id, title, main_image, price, stock, product_status, sale_status,
user_name, online_time, offline_time, sold_time, create_time_xy, update_time_xy,
appid, product_url, remark)
values
<foreach collection="list" item="item" separator=",">
(#{item.productId}, #{item.title}, #{item.mainImage}, #{item.price}, #{item.stock},
#{item.productStatus}, #{item.saleStatus}, #{item.userName}, #{item.onlineTime},
#{item.offlineTime}, #{item.soldTime}, #{item.createTimeXy}, #{item.updateTimeXy},
#{item.appid}, #{item.productUrl}, #{item.remark})
</foreach>
ON DUPLICATE KEY UPDATE
title = VALUES(title),
main_image = VALUES(main_image),
price = VALUES(price),
stock = VALUES(stock),
product_status = VALUES(product_status),
sale_status = VALUES(sale_status),
user_name = VALUES(user_name),
online_time = VALUES(online_time),
offline_time = VALUES(offline_time),
sold_time = VALUES(sold_time),
create_time_xy = VALUES(create_time_xy),
update_time_xy = VALUES(update_time_xy),
product_url = VALUES(product_url),
remark = VALUES(remark),
update_time = NOW()
</insert>
</mapper>

View File

@@ -23,11 +23,18 @@
<result property="isCountEnabled" column="is_count_enabled"/>
<result property="thirdPartyOrderNo" column="third_party_order_no"/>
<result property="jingfenActualPrice" column="jingfen_actual_price"/>
<result property="isRefunded" column="is_refunded"/>
<result property="refundDate" column="refund_date"/>
<result property="isRefundReceived" column="is_refund_received"/>
<result property="refundReceivedDate" column="refund_received_date"/>
<result property="isRebateReceived" column="is_rebate_received"/>
<result property="rebateReceivedDate" column="rebate_received_date"/>
</resultMap>
<sql id="selectJDOrderBase">
select id, remark, distribution_mark, model_number, link, payment_amount, rebate_amount,
address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price
address, logistics_link, order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date
from jd_order
</sql>
@@ -35,17 +42,26 @@
<include refid="selectJDOrderBase"/>
<where>
<if test="remark != null and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark like concat('%', #{distributionMark}, '%')</if>
<if test="params.orderSearch != null and params.orderSearch != ''">
and (
order_id like concat('%', #{params.orderSearch}, '%')
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
or distribution_mark like concat('%', #{params.orderSearch}, '%')
)
</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
<if test="orderId != null and orderId != ''"> and order_id like concat('%', #{orderId}, '%')</if>
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
<if test="orderTime != null"> and order_time = #{orderTime}</if>
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
<if test="isRefunded != null"> and is_refunded = #{isRefunded}</if>
<if test="isRefundReceived != null"> and is_refund_received = #{isRefundReceived}</if>
<if test="isRebateReceived != null"> and is_rebate_received = #{isRebateReceived}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime}
</if>
@@ -60,17 +76,26 @@
<include refid="selectJDOrderBase"/>
<where>
<if test="remark != null and remark != ''"> and remark like concat('%', #{remark}, '%')</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark like concat('%', #{distributionMark}, '%')</if>
<if test="params.orderSearch != null and params.orderSearch != ''">
and (
order_id like concat('%', #{params.orderSearch}, '%')
or third_party_order_no like concat('%', #{params.orderSearch}, '%')
or distribution_mark like concat('%', #{params.orderSearch}, '%')
)
</if>
<if test="distributionMark != null and distributionMark != ''"> and distribution_mark = #{distributionMark}</if>
<if test="modelNumber != null and modelNumber != ''"> and model_number like concat('%', #{modelNumber}, '%')</if>
<if test="link != null and link != ''"> and link like concat('%', #{link}, '%')</if>
<if test="paymentAmount != null"> and payment_amount = #{paymentAmount}</if>
<if test="rebateAmount != null"> and rebate_amount = #{rebateAmount}</if>
<if test="address != null and address != ''"> and address like concat('%', #{address}, '%')</if>
<if test="logisticsLink != null and logisticsLink != ''"> and logistics_link like concat('%', #{logisticsLink}, '%')</if>
<if test="orderId != null and orderId != ''"> and order_id like concat('%', #{orderId}, '%')</if>
<if test="buyer != null and buyer != ''"> and buyer like concat('%', #{buyer}, '%')</if>
<if test="orderTime != null"> and order_time = #{orderTime}</if>
<if test="status != null and status != ''"> and status like concat('%', #{status}, '%')</if>
<if test="isRefunded != null"> and is_refunded = #{isRefunded}</if>
<if test="isRefundReceived != null"> and is_refund_received = #{isRefundReceived}</if>
<if test="isRebateReceived != null"> and is_rebate_received = #{isRebateReceived}</if>
<if test="params.beginTime != null and params.beginTime != ''"><!-- 开始时间检索 -->
and date(order_time) &gt;= #{params.beginTime}
</if>
@@ -100,12 +125,14 @@
remark, distribution_mark, model_number, link,
payment_amount, rebate_amount, address, logistics_link,
tencent_doc_pushed, tencent_doc_push_time,
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price
order_id, buyer, order_time, create_time, update_time, status, is_count_enabled, third_party_order_no, jingfen_actual_price,
is_refunded, refund_date, is_refund_received, refund_received_date, is_rebate_received, rebate_received_date
) values (
#{remark}, #{distributionMark}, #{modelNumber}, #{link},
#{paymentAmount}, #{rebateAmount}, #{address}, #{logisticsLink},
0, null,
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice}
#{orderId}, #{buyer}, #{orderTime}, now(), now(), #{status}, #{isCountEnabled}, #{thirdPartyOrderNo}, #{jingfenActualPrice},
#{isRefunded}, #{refundDate}, #{isRefundReceived}, #{refundReceivedDate}, #{isRebateReceived}, #{rebateReceivedDate}
)
</insert>
@@ -129,6 +156,12 @@
<if test="isCountEnabled != null"> is_count_enabled = #{isCountEnabled},</if>
<if test="thirdPartyOrderNo != null"> third_party_order_no = #{thirdPartyOrderNo},</if>
<if test="jingfenActualPrice != null"> jingfen_actual_price = #{jingfenActualPrice},</if>
<if test="isRefunded != null"> is_refunded = #{isRefunded},</if>
<if test="refundDate != null"> refund_date = #{refundDate},</if>
<if test="isRefundReceived != null"> is_refund_received = #{isRefundReceived},</if>
<if test="refundReceivedDate != null"> refund_received_date = #{refundReceivedDate},</if>
<if test="isRebateReceived != null"> is_rebate_received = #{isRebateReceived},</if>
<if test="rebateReceivedDate != null"> rebate_received_date = #{rebateReceivedDate},</if>
update_time = now()
</set>
where id = #{id}
@@ -159,7 +192,7 @@
order by order_time desc
limit 1
</select>
<select id="selectJDOrderByThirdPartyOrderNo" parameterType="string" resultMap="JDOrderResult">
<include refid="selectJDOrderBase"/>
where third_party_order_no = #{thirdPartyOrderNo}
@@ -176,7 +209,7 @@
<select id="selectJDOrderListByDistributionMarkFOrPDD" resultMap="JDOrderResult">
<include refid="selectJDOrderBase"/>
<where>
(distribution_mark = 'F' OR distribution_mark = 'PDD')
(distribution_mark = 'F' OR distribution_mark = 'PDD' OR distribution_mark = 'H' OR distribution_mark = 'W' OR distribution_mark = 'PDD-W')
AND logistics_link IS NOT NULL
AND logistics_link != ''
</where>

View File

@@ -11,12 +11,13 @@
<result property="secretKey" column="secret_key"/>
<result property="isActive" column="is_active"/>
<result property="isCount" column="is_count"/>
<result property="touser" column="touser"/>
<result property="createdAt" column="created_at"/>
<result property="updatedAt" column="updated_at"/>
</resultMap>
<sql id="selectSuperAdminVo">
select id, wxid, name, union_id, app_key, secret_key, is_active, is_count, created_at, updated_at from super_admin
select id, wxid, name, union_id, app_key, secret_key, is_active, is_count, touser, created_at, updated_at from super_admin
</sql>
<select id="selectSuperAdminList" parameterType="SuperAdmin" resultMap="SuperAdminResult">
@@ -51,6 +52,8 @@
<if test="appKey != null and appKey != ''">app_key,</if>
<if test="secretKey != null and secretKey != ''">secret_key,</if>
<if test="isActive != null">is_active,</if>
<if test="isCount != null">is_count,</if>
<if test="touser != null and touser != ''">touser,</if>
created_at,
updated_at,
</trim>
@@ -61,6 +64,8 @@
<if test="appKey != null and appKey != ''">#{appKey},</if>
<if test="secretKey != null and secretKey != ''">#{secretKey},</if>
<if test="isActive != null">#{isActive},</if>
<if test="isCount != null">#{isCount},</if>
<if test="touser != null and touser != ''">#{touser},</if>
now(),
now(),
</trim>
@@ -76,6 +81,7 @@
<if test="secretKey != null and secretKey != ''">secret_key = #{secretKey},</if>
<if test="isActive != null">is_active = #{isActive},</if>
<if test="isCount != null">is_count = #{isCount},</if>
<if test="touser != null">touser = #{touser},</if>
updated_at = now(),
</trim>
where id = #{id}

View File

@@ -0,0 +1,43 @@
.operation-logs {
margin-top: 15px;
}
+.operation-logs >>> .el-table {
+ border-radius: 8px;
+ overflow: hidden;
+}
+.operation-logs >>> .log-row-success td {
+ background: #f0f9eb;
+}
+.operation-logs >>> .log-row-failed td {
+ background: #fef0f0;
+}
+.operation-logs >>> .log-row-skipped td {
+ background: #fdf6ec;
+}
+.status-icon {
+ font-size: 12px;
+ margin-right: 4px;
+}
+.text-placeholder {
+ color: #c0c4cc;
+}
+.text-muted {
+ color: #909399;
+}
.logs-header {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
font-weight: 500;
margin-bottom: 10px;
color: #606266;
}

View File

@@ -0,0 +1,4 @@
-- 为超级管理员表添加接收人字段
-- 字段说明touser 存储企业微信用户ID多个用逗号分隔
ALTER TABLE super_admin ADD COLUMN touser VARCHAR(500) DEFAULT NULL COMMENT '接收人企业微信用户ID多个用逗号分隔';

View File

@@ -0,0 +1,16 @@
-- 为jd_order表添加退款相关字段
-- 执行日期2025-01-XX
ALTER TABLE jd_order
ADD COLUMN is_refunded INT DEFAULT 0 COMMENT '是否退款0否 1是',
ADD COLUMN refund_date DATETIME NULL COMMENT '退款日期',
ADD COLUMN is_refund_received INT DEFAULT 0 COMMENT '是否退款到账0否 1是',
ADD COLUMN refund_received_date DATETIME NULL COMMENT '退款到账日期',
ADD COLUMN is_rebate_received INT DEFAULT 0 COMMENT '后返到账0否 1是',
ADD COLUMN rebate_received_date DATETIME NULL COMMENT '后返到账日期';
-- 添加索引(可选,根据查询需求)
-- CREATE INDEX idx_is_refunded ON jd_order(is_refunded);
-- CREATE INDEX idx_is_refund_received ON jd_order(is_refund_received);
-- CREATE INDEX idx_is_rebate_received ON jd_order(is_rebate_received);

View File

@@ -0,0 +1,37 @@
-- 闲鱼商品菜单配置
-- 菜单类型M=目录 C=菜单 F=按钮
-- 1. 主菜单如果是放在系统管理下parent_id为1如果是独立菜单需要先创建一个jarvis目录
-- 假设放在系统管理parent_id=1可以根据实际情况调整
-- 闲鱼商品管理菜单(主菜单)
insert into sys_menu values(2000, '闲鱼商品管理', 1, 10, 'erpProduct', 'system/erpProduct/index', '', '', 1, 0, 'C', '0', '0', 'jarvis:erpProduct:list', 'shopping', 'admin', sysdate(), '', null, '闲鱼商品管理菜单');
-- 闲鱼商品管理按钮权限
insert into sys_menu values(2001, '商品查询', 2000, 1, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:query', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2002, '商品新增', 2000, 2, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:add', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2003, '商品修改', 2000, 3, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:edit', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2004, '商品删除', 2000, 4, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:remove', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2005, '商品导出', 2000, 5, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:export', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2006, '拉取商品', 2000, 6, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:pull', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2007, '批量上架', 2000, 7, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:publish', '#', 'admin', sysdate(), '', null, '');
insert into sys_menu values(2008, '批量下架', 2000, 8, '', '', '', '', 1, 0, 'F', '0', '0', 'jarvis:erpProduct:downShelf', '#', 'admin', sysdate(), '', null, '');
-- 给管理员角色role_id=1添加闲鱼商品菜单权限
insert into sys_role_menu(role_id, menu_id) values(1, 2000);
insert into sys_role_menu(role_id, menu_id) values(1, 2001);
insert into sys_role_menu(role_id, menu_id) values(1, 2002);
insert into sys_role_menu(role_id, menu_id) values(1, 2003);
insert into sys_role_menu(role_id, menu_id) values(1, 2004);
insert into sys_role_menu(role_id, menu_id) values(1, 2005);
insert into sys_role_menu(role_id, menu_id) values(1, 2006);
insert into sys_role_menu(role_id, menu_id) values(1, 2007);
insert into sys_role_menu(role_id, menu_id) values(1, 2008);
-- 注意:
-- 1. 如果菜单需要放在其他目录下比如jarvis目录请修改parent_id
-- 2. order_num 是显示顺序,可以根据需要调整(值越大越靠后)
-- 3. 如果管理员角色ID不是1请修改上面的role_id值
-- 4. 如果需要给其他角色添加权限可以复制上面的insert语句并修改role_id
-- 5. 执行完此SQL后需要清除Redis缓存或重启系统才能看到菜单

31
sql/闲鱼商品表.sql Normal file
View File

@@ -0,0 +1,31 @@
-- 闲鱼商品表
CREATE TABLE `erp_product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`product_id` bigint(20) NOT NULL COMMENT '管家商品ID',
`title` varchar(500) DEFAULT NULL COMMENT '商品标题',
`main_image` varchar(1000) DEFAULT NULL COMMENT '商品图片(主图)',
`price` bigint(20) DEFAULT NULL COMMENT '商品价格(分)',
`stock` int(11) DEFAULT NULL COMMENT '商品库存',
`product_status` int(11) DEFAULT NULL COMMENT '商品状态 -1:删除 21:待发布 22:销售中 23:已售罄 31:手动下架 33:售出下架 36:自动下架',
`sale_status` int(11) DEFAULT NULL COMMENT '销售状态',
`user_name` varchar(100) DEFAULT NULL COMMENT '闲鱼会员名',
`online_time` bigint(20) DEFAULT NULL COMMENT '上架时间(时间戳)',
`offline_time` bigint(20) DEFAULT NULL COMMENT '下架时间(时间戳)',
`sold_time` bigint(20) DEFAULT NULL COMMENT '售出时间(时间戳)',
`create_time_xy` bigint(20) DEFAULT NULL COMMENT '创建时间(闲鱼,时间戳)',
`update_time_xy` bigint(20) DEFAULT NULL COMMENT '更新时间(闲鱼,时间戳)',
`appid` varchar(100) DEFAULT NULL COMMENT 'ERP应用ID',
`product_url` varchar(1000) DEFAULT NULL COMMENT '商品链接',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id_appid` (`product_id`, `appid`),
KEY `idx_product_id` (`product_id`),
KEY `idx_appid` (`appid`),
KEY `idx_product_status` (`product_status`),
KEY `idx_user_name` (`user_name`),
KEY `idx_online_time` (`online_time`),
KEY `idx_update_time_xy` (`update_time_xy`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='闲鱼商品表';