Compare commits

...

41 Commits

Author SHA1 Message Date
Leo
9a8c7b1039 1 2025-12-08 14:42:44 +08:00
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
39 changed files with 3523 additions and 127 deletions

View File

@@ -0,0 +1,137 @@
package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.jarvis.domain.Comment;
import com.ruoyi.jarvis.domain.dto.CommentApiStatistics;
import com.ruoyi.jarvis.domain.dto.CommentStatistics;
import com.ruoyi.jarvis.service.ICommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
/**
* 评论管理 Controller
*/
@RestController
@RequestMapping("/jarvis/comment")
public class CommentController extends BaseController {
@Autowired
private ICommentService commentService;
/**
* 查询京东评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/jd/list")
public TableDataInfo list(Comment comment) {
startPage();
List<Comment> list = commentService.selectCommentList(comment);
return getDataTable(list);
}
/**
* 导出京东评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:export')")
@Log(title = "京东评论", businessType = BusinessType.EXPORT)
@PostMapping("/jd/export")
public void export(HttpServletResponse response, Comment comment) {
List<Comment> list = commentService.selectCommentList(comment);
ExcelUtil<Comment> util = new ExcelUtil<Comment>(Comment.class);
util.exportExcel(response, list, "京东评论数据");
}
/**
* 获取京东评论详细信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:query')")
@GetMapping("/jd/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) {
return success(commentService.selectCommentById(id));
}
/**
* 修改评论使用状态
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping("/jd")
public AjaxResult edit(@RequestBody Comment comment) {
return toAjax(commentService.updateCommentIsUse(comment));
}
/**
* 删除评论
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:remove')")
@Log(title = "评论管理", businessType = BusinessType.DELETE)
@DeleteMapping("/jd/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) {
return toAjax(commentService.deleteCommentByIds(ids));
}
/**
* 重置评论使用状态按商品ID
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping("/jd/reset/{productId}")
public AjaxResult resetByProductId(@PathVariable String productId) {
return toAjax(commentService.resetCommentIsUseByProductId(productId));
}
/**
* 获取评论统计信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/statistics")
public AjaxResult getStatistics(@RequestParam(required = false) String source) {
List<CommentStatistics> statistics = commentService.getCommentStatistics(source);
return success(statistics);
}
/**
* 获取接口调用统计
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/api/statistics")
public AjaxResult getApiStatistics(
@RequestParam(required = false) String apiType,
@RequestParam(required = false) String productType,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
List<CommentApiStatistics> statistics = commentService.getApiStatistics(apiType, productType, startDate, endDate);
return success(statistics);
}
/**
* 获取Redis产品类型映射京东
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/redis/jd/map")
public AjaxResult getJdProductTypeMap() {
Map<String, String> map = commentService.getJdProductTypeMap();
return success(map);
}
/**
* 获取Redis产品类型映射淘宝
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/redis/tb/map")
public AjaxResult getTbProductTypeMap() {
Map<String, String> map = commentService.getTbProductTypeMap();
return success(map);
}
}

View File

@@ -103,7 +103,7 @@ public class ErpProductController extends BaseController
}
/**
* 从闲鱼ERP拉取商品列表并保存
* 从闲鱼ERP拉取商品列表并保存(单页,保留用于兼容)
*/
@PreAuthorize("@ss.hasPermi('jarvis:erpProduct:pull')")
@Log(title = "拉取闲鱼商品", businessType = BusinessType.INSERT)
@@ -116,12 +116,67 @@ public class ErpProductController extends BaseController
{
try {
int count = erpProductService.pullAndSaveProductList(appid, pageNo, pageSize, productStatus);
return success("成功拉取并保存 " + count + " 个商品");
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);
}
}
/**
* 批量上架商品
*/

View File

@@ -21,14 +21,15 @@ public class InstructionController extends BaseController {
}
/**
* 执行文本指令
* 执行文本指令(控制台入口,需要权限)
* body: { command: "京今日统计", forceGenerate: false }
*/
@PostMapping("/execute")
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")));
java.util.List<String> result = instructionService.execute(cmd, 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,6 +39,8 @@ 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);
@@ -47,10 +51,12 @@ public class JDOrderController extends BaseController {
"^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";
@@ -1017,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 订单信息
@@ -1045,7 +1088,7 @@ public class JDOrderController extends BaseController {
// 收货地址
pushContent.append("收货地址:").append(order.getAddress() != null ? order.getAddress() : "").append("\n");
// 运单号
pushContent.append("运单号:").append("\n").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";
@@ -1055,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);

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

@@ -0,0 +1,90 @@
package com.ruoyi.web.controller.jarvis;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.jarvis.domain.TaobaoComment;
import com.ruoyi.jarvis.service.ITaobaoCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 淘宝评论管理 Controller
*/
@RestController
@RequestMapping("/jarvis/taobaoComment")
public class TaobaoCommentController extends BaseController {
@Autowired
private ITaobaoCommentService taobaoCommentService;
/**
* 查询淘宝评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:list')")
@GetMapping("/list")
public TableDataInfo list(TaobaoComment taobaoComment) {
startPage();
List<TaobaoComment> list = taobaoCommentService.selectTaobaoCommentList(taobaoComment);
return getDataTable(list);
}
/**
* 导出淘宝评论列表
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:export')")
@Log(title = "淘宝评论", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, TaobaoComment taobaoComment) {
List<TaobaoComment> list = taobaoCommentService.selectTaobaoCommentList(taobaoComment);
ExcelUtil<TaobaoComment> util = new ExcelUtil<TaobaoComment>(TaobaoComment.class);
util.exportExcel(response, list, "淘宝评论数据");
}
/**
* 获取淘宝评论详细信息
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable("id") Integer id) {
return success(taobaoCommentService.selectTaobaoCommentById(id));
}
/**
* 修改评论使用状态
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody TaobaoComment taobaoComment) {
return toAjax(taobaoCommentService.updateTaobaoCommentIsUse(taobaoComment));
}
/**
* 删除评论
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:remove')")
@Log(title = "评论管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Integer[] ids) {
return toAjax(taobaoCommentService.deleteTaobaoCommentByIds(ids));
}
/**
* 重置评论使用状态按商品ID
*/
@PreAuthorize("@ss.hasPermi('jarvis:comment:edit')")
@Log(title = "评论管理", businessType = BusinessType.UPDATE)
@PutMapping("/reset/{productId}")
public AjaxResult resetByProductId(@PathVariable String productId) {
return toAjax(taobaoCommentService.resetTaobaoCommentIsUseByProductId(productId));
}
}

View File

@@ -1680,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

@@ -6,6 +6,8 @@ import com.ruoyi.common.core.domain.AjaxResult;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.jarvis.service.ICommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@@ -22,18 +24,28 @@ public class CommentPublicController extends BaseController {
private static final String JD_BASE = "http://192.168.8.88:6666/jd";
private static final String SKEY = "2192057370ef8140c201079969c956a3";
@Autowired(required = false)
private ICommentService commentService;
/**
* 获取可选型号/类型(示例)
*/
@GetMapping("/types")
public AjaxResult types() {
boolean success = false;
try {
String url = JD_BASE + "/comment/types?skey=" + SKEY;
String result = HttpUtils.sendGet(url);
Object parsed = JSON.parse(result);
success = true;
return AjaxResult.success(parsed);
} catch (Exception e) {
return AjaxResult.error("types failed: " + e.getMessage());
} finally {
// 记录接口调用统计
if (commentService != null) {
commentService.recordApiCall("jd", "types", success);
}
}
}
@@ -43,18 +55,27 @@ public class CommentPublicController extends BaseController {
*/
@PostMapping("/generate")
public AjaxResult generate(@RequestBody Map<String, String> body) {
boolean success = false;
String productType = null;
try {
String url = JD_BASE + "/comment/generate";
JSONObject param = new JSONObject();
param.put("skey", SKEY);
if (body != null && body.get("productType") != null) {
param.put("productType", body.get("productType"));
productType = body.get("productType");
param.put("productType", productType);
}
String result = HttpUtils.sendJsonPost(url, param.toJSONString());
Object parsed = JSON.parse(result);
success = true;
return AjaxResult.success(parsed);
} catch (Exception e) {
return AjaxResult.error("generate failed: " + e.getMessage());
} finally {
// 记录接口调用统计
if (commentService != null && productType != null) {
commentService.recordApiCall("jd", productType, success);
}
}
}

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,60 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 京东评论对象 comments
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class Comment extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键ID */
@Excel(name = "ID")
private Long id;
/** 商品ID */
@Excel(name = "商品ID")
private String productId;
/** 用户名 */
@Excel(name = "用户名")
private String userName;
/** 评论内容 */
@Excel(name = "评论内容")
private String commentText;
/** 评论ID */
@Excel(name = "评论ID")
private String commentId;
/** 图片URLs */
@Excel(name = "图片URLs")
private String pictureUrls;
/** 创建时间 */
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
/** 评论日期 */
@Excel(name = "评论日期", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date commentDate;
/** 是否已使用 0-未使用 1-已使用 */
@Excel(name = "使用状态", readConverterExp = "0=未使用,1=已使用")
private Integer isUse;
/** 产品类型从Redis映射获取 */
private String productType;
/** Redis映射的产品ID */
private String mappedProductId;
}

View File

@@ -38,8 +38,8 @@ public class ErpProduct extends BaseEntity
@Excel(name = "商品库存")
private Integer stock;
/** 商品状态 1:上架 2:下架 3:售 */
@Excel(name = "商品状态", readConverterExp = "1=上架,2=下架,3=")
/** 商品状态 -1:删除 21:待发布 22:销售中 23:已售罄 31:手动下架 33:售出下架 36:自动下架 */
@Excel(name = "商品状态", readConverterExp = "-1=删除,21=待发布,22=销售中,23=已售罄,31=手动下架,33=售出下架,36=自动下架")
private Integer productStatus;
/** 销售状态 */

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,58 @@
package com.ruoyi.jarvis.domain;
import com.ruoyi.common.core.domain.BaseEntity;
import com.ruoyi.common.annotation.Excel;
import lombok.Data;
import java.util.Date;
/**
* 淘宝评论对象 taobao_comments
*/
@Data
public class TaobaoComment extends BaseEntity {
private static final long serialVersionUID = 1L;
/** 主键ID */
@Excel(name = "ID")
private Integer id;
/** 商品ID */
@Excel(name = "商品ID")
private String productId;
/** 用户名 */
@Excel(name = "用户名")
private String userName;
/** 评论内容 */
@Excel(name = "评论内容")
private String commentText;
/** 评论ID */
@Excel(name = "评论ID")
private String commentId;
/** 图片URLs */
@Excel(name = "图片URLs")
private String pictureUrls;
/** 创建时间 */
@Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date createdAt;
/** 评论日期 */
@Excel(name = "评论日期")
private String commentDate;
/** 是否已使用 0-未使用 1-已使用 */
@Excel(name = "使用状态", readConverterExp = "0=未使用,1=已使用")
private Integer isUse;
/** 产品类型从Redis映射获取 */
private String productType;
/** Redis映射的产品ID */
private String mappedProductId;
}

View File

@@ -0,0 +1,30 @@
package com.ruoyi.jarvis.domain.dto;
import lombok.Data;
import java.util.Date;
/**
* 评论接口调用统计
*/
@Data
public class CommentApiStatistics {
/** 统计日期 */
private Date statDate;
/** 接口类型jd-京东tb-淘宝 */
private String apiType;
/** 产品类型 */
private String productType;
/** 调用次数 */
private Long callCount;
/** 成功次数 */
private Long successCount;
/** 失败次数 */
private Long failCount;
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.jarvis.domain.dto;
import lombok.Data;
/**
* 评论统计信息
*/
@Data
public class CommentStatistics {
/** 评论来源jd-京东tb-淘宝 */
private String source;
/** 产品类型 */
private String productType;
/** 产品ID */
private String productId;
/** 总评论数 */
private Long totalCount;
/** 可用评论数(未使用) */
private Long availableCount;
/** 已使用评论数 */
private Long usedCount;
/** 接口调用次数 */
private Long apiCallCount;
/** 今日调用次数 */
private Long todayCallCount;
}

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,42 @@
package com.ruoyi.jarvis.mapper;
import com.ruoyi.jarvis.domain.Comment;
import java.util.List;
import java.util.Map;
/**
* 京东评论 Mapper 接口
*/
public interface CommentMapper {
/**
* 查询京东评论列表
*/
List<Comment> selectCommentList(Comment comment);
/**
* 根据ID查询京东评论
*/
Comment selectCommentById(Long id);
/**
* 根据商品ID查询评论统计
*/
Map<String, Object> selectCommentStatisticsByProductId(String productId);
/**
* 更新评论使用状态
*/
int updateCommentIsUse(Comment comment);
/**
* 批量删除评论
*/
int deleteCommentByIds(Long[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetCommentIsUseByProductId(String productId);
}

View File

@@ -0,0 +1,42 @@
package com.ruoyi.jarvis.mapper;
import com.ruoyi.jarvis.domain.TaobaoComment;
import java.util.List;
import java.util.Map;
/**
* 淘宝评论 Mapper 接口
*/
public interface TaobaoCommentMapper {
/**
* 查询淘宝评论列表
*/
List<TaobaoComment> selectTaobaoCommentList(TaobaoComment taobaoComment);
/**
* 根据ID查询淘宝评论
*/
TaobaoComment selectTaobaoCommentById(Integer id);
/**
* 根据商品ID查询评论统计
*/
Map<String, Object> selectTaobaoCommentStatisticsByProductId(String productId);
/**
* 更新评论使用状态
*/
int updateTaobaoCommentIsUse(TaobaoComment taobaoComment);
/**
* 批量删除评论
*/
int deleteTaobaoCommentByIds(Integer[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetTaobaoCommentIsUseByProductId(String productId);
}

View File

@@ -0,0 +1,65 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.Comment;
import com.ruoyi.jarvis.domain.dto.CommentStatistics;
import com.ruoyi.jarvis.domain.dto.CommentApiStatistics;
import java.util.List;
import java.util.Map;
/**
* 评论管理 Service 接口
*/
public interface ICommentService {
/**
* 查询京东评论列表
*/
List<Comment> selectCommentList(Comment comment);
/**
* 根据ID查询京东评论
*/
Comment selectCommentById(Long id);
/**
* 更新评论使用状态
*/
int updateCommentIsUse(Comment comment);
/**
* 批量删除评论
*/
int deleteCommentByIds(Long[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetCommentIsUseByProductId(String productId);
/**
* 获取评论统计信息包含Redis映射
*/
List<CommentStatistics> getCommentStatistics(String source);
/**
* 记录接口调用统计
*/
void recordApiCall(String apiType, String productType, boolean success);
/**
* 获取接口调用统计
*/
List<CommentApiStatistics> getApiStatistics(String apiType, String productType, String startDate, String endDate);
/**
* 获取Redis产品类型映射京东
*/
Map<String, String> getJdProductTypeMap();
/**
* 获取Redis产品类型映射淘宝
*/
Map<String, String> getTbProductTypeMap();
}

View File

@@ -69,5 +69,39 @@ public interface IErpProductService
* @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

@@ -19,6 +19,15 @@ public interface IInstructionService {
*/
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

@@ -0,0 +1,37 @@
package com.ruoyi.jarvis.service;
import com.ruoyi.jarvis.domain.TaobaoComment;
import java.util.List;
/**
* 淘宝评论管理 Service 接口
*/
public interface ITaobaoCommentService {
/**
* 查询淘宝评论列表
*/
List<TaobaoComment> selectTaobaoCommentList(TaobaoComment taobaoComment);
/**
* 根据ID查询淘宝评论
*/
TaobaoComment selectTaobaoCommentById(Integer id);
/**
* 更新评论使用状态
*/
int updateTaobaoCommentIsUse(TaobaoComment taobaoComment);
/**
* 批量删除评论
*/
int deleteTaobaoCommentByIds(Integer[] ids);
/**
* 重置评论使用状态(批量)
*/
int resetTaobaoCommentIsUseByProductId(String productId);
}

View File

@@ -0,0 +1,282 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.Comment;
import com.ruoyi.jarvis.domain.dto.CommentApiStatistics;
import com.ruoyi.jarvis.domain.dto.CommentStatistics;
import com.ruoyi.jarvis.mapper.CommentMapper;
import com.ruoyi.jarvis.service.ICommentService;
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.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* 评论管理 Service 实现
*/
@Service
public class CommentServiceImpl implements ICommentService {
private static final Logger log = LoggerFactory.getLogger(CommentServiceImpl.class);
private static final String PRODUCT_TYPE_MAP_PREFIX = "product_type_map";
private static final String PRODUCT_TYPE_MAP_PREFIX_TB = "product_type_map_tb";
private static final String API_CALL_STAT_PREFIX = "comment:api:stat:";
private static final String API_CALL_TODAY_PREFIX = "comment:api:today:";
@Autowired
private CommentMapper commentMapper;
@Autowired(required = false)
private com.ruoyi.jarvis.mapper.TaobaoCommentMapper taobaoCommentMapper;
@Autowired(required = false)
private StringRedisTemplate stringRedisTemplate;
@Override
public List<Comment> selectCommentList(Comment comment) {
List<Comment> list = commentMapper.selectCommentList(comment);
// 填充Redis映射的产品类型信息
if (stringRedisTemplate != null) {
Map<String, String> jdMap = getJdProductTypeMap();
for (Comment c : list) {
// 查找对应的产品类型
String productId = c.getProductId();
if (jdMap != null) {
for (Map.Entry<String, String> entry : jdMap.entrySet()) {
if (entry.getValue().equals(productId)) {
c.setProductType(entry.getKey());
c.setMappedProductId(productId);
break;
}
}
}
}
}
return list;
}
@Override
public Comment selectCommentById(Long id) {
Comment comment = commentMapper.selectCommentById(id);
if (comment != null && stringRedisTemplate != null) {
Map<String, String> jdMap = getJdProductTypeMap();
if (jdMap != null) {
String productId = comment.getProductId();
for (Map.Entry<String, String> entry : jdMap.entrySet()) {
if (entry.getValue().equals(productId)) {
comment.setProductType(entry.getKey());
comment.setMappedProductId(productId);
break;
}
}
}
}
return comment;
}
@Override
public int updateCommentIsUse(Comment comment) {
return commentMapper.updateCommentIsUse(comment);
}
@Override
public int deleteCommentByIds(Long[] ids) {
return commentMapper.deleteCommentByIds(ids);
}
@Override
public int resetCommentIsUseByProductId(String productId) {
return commentMapper.resetCommentIsUseByProductId(productId);
}
@Override
public List<CommentStatistics> getCommentStatistics(String source) {
List<CommentStatistics> statisticsList = new ArrayList<>();
Map<String, String> productTypeMap = null;
if ("jd".equals(source) || source == null) {
productTypeMap = getJdProductTypeMap();
} else if ("tb".equals(source)) {
productTypeMap = getTbProductTypeMap();
}
if (productTypeMap == null || productTypeMap.isEmpty()) {
return statisticsList;
}
for (Map.Entry<String, String> entry : productTypeMap.entrySet()) {
String productType = entry.getKey();
String productId = entry.getValue();
CommentStatistics stats = new CommentStatistics();
stats.setSource("jd".equals(source) ? "京东评论" : "淘宝评论");
stats.setProductType(productType);
stats.setProductId(productId);
// 查询评论统计
Map<String, Object> statMap = null;
if ("jd".equals(source) || source == null) {
statMap = commentMapper.selectCommentStatisticsByProductId(productId);
} else if ("tb".equals(source) && taobaoCommentMapper != null) {
statMap = taobaoCommentMapper.selectTaobaoCommentStatisticsByProductId(productId);
}
if (statMap != null) {
stats.setTotalCount(((Number) statMap.get("totalCount")).longValue());
stats.setAvailableCount(((Number) statMap.get("availableCount")).longValue());
stats.setUsedCount(((Number) statMap.get("usedCount")).longValue());
}
// 获取接口调用统计
if (stringRedisTemplate != null) {
String todayKey = API_CALL_TODAY_PREFIX + source + ":" + productType + ":" + getTodayDate();
String todayCount = stringRedisTemplate.opsForValue().get(todayKey);
stats.setTodayCallCount(todayCount != null ? Long.parseLong(todayCount) : 0L);
// 获取总调用次数从Redis中统计
String statKey = API_CALL_STAT_PREFIX + source + ":" + productType;
String totalCount = stringRedisTemplate.opsForValue().get(statKey);
stats.setApiCallCount(totalCount != null ? Long.parseLong(totalCount) : 0L);
}
statisticsList.add(stats);
}
return statisticsList;
}
@Override
public void recordApiCall(String apiType, String productType, boolean success) {
if (stringRedisTemplate == null) {
return;
}
try {
String today = getTodayDate();
String todayKey = API_CALL_TODAY_PREFIX + apiType + ":" + productType + ":" + today;
stringRedisTemplate.opsForValue().increment(todayKey);
stringRedisTemplate.expire(todayKey, 7, TimeUnit.DAYS); // 保留7天
String statKey = API_CALL_STAT_PREFIX + apiType + ":" + productType;
stringRedisTemplate.opsForValue().increment(statKey);
// 记录成功/失败统计
String successKey = API_CALL_STAT_PREFIX + apiType + ":" + productType + ":success";
String failKey = API_CALL_STAT_PREFIX + apiType + ":" + productType + ":fail";
if (success) {
stringRedisTemplate.opsForValue().increment(successKey);
} else {
stringRedisTemplate.opsForValue().increment(failKey);
}
} catch (Exception e) {
log.error("记录接口调用统计失败", e);
}
}
@Override
public List<CommentApiStatistics> getApiStatistics(String apiType, String productType, String startDate, String endDate) {
List<CommentApiStatistics> statisticsList = new ArrayList<>();
if (stringRedisTemplate == null) {
return statisticsList;
}
try {
// 如果指定了产品类型,只查询该类型的统计
if (productType != null && !productType.isEmpty()) {
CommentApiStatistics stats = new CommentApiStatistics();
stats.setApiType(apiType);
stats.setProductType(productType);
String statKey = API_CALL_STAT_PREFIX + apiType + ":" + productType;
String totalCount = stringRedisTemplate.opsForValue().get(statKey);
stats.setCallCount(totalCount != null ? Long.parseLong(totalCount) : 0L);
String successKey = statKey + ":success";
String successCount = stringRedisTemplate.opsForValue().get(successKey);
stats.setSuccessCount(successCount != null ? Long.parseLong(successCount) : 0L);
String failKey = statKey + ":fail";
String failCount = stringRedisTemplate.opsForValue().get(failKey);
stats.setFailCount(failCount != null ? Long.parseLong(failCount) : 0L);
statisticsList.add(stats);
} else {
// 查询所有产品类型的统计
Map<String, String> productTypeMap = "jd".equals(apiType) ? getJdProductTypeMap() : getTbProductTypeMap();
if (productTypeMap != null) {
for (String pt : productTypeMap.keySet()) {
CommentApiStatistics stats = new CommentApiStatistics();
stats.setApiType(apiType);
stats.setProductType(pt);
String statKey = API_CALL_STAT_PREFIX + apiType + ":" + pt;
String totalCount = stringRedisTemplate.opsForValue().get(statKey);
stats.setCallCount(totalCount != null ? Long.parseLong(totalCount) : 0L);
String successKey = statKey + ":success";
String successCount = stringRedisTemplate.opsForValue().get(successKey);
stats.setSuccessCount(successCount != null ? Long.parseLong(successCount) : 0L);
String failKey = statKey + ":fail";
String failCount = stringRedisTemplate.opsForValue().get(failKey);
stats.setFailCount(failCount != null ? Long.parseLong(failCount) : 0L);
statisticsList.add(stats);
}
}
}
} catch (Exception e) {
log.error("获取接口调用统计失败", e);
}
return statisticsList;
}
@Override
public Map<String, String> getJdProductTypeMap() {
if (stringRedisTemplate == null) {
return new HashMap<>();
}
try {
Map<Object, Object> rawMap = stringRedisTemplate.opsForHash().entries(PRODUCT_TYPE_MAP_PREFIX);
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue().toString());
}
return result;
} catch (Exception e) {
log.error("获取京东产品类型映射失败", e);
return new HashMap<>();
}
}
@Override
public Map<String, String> getTbProductTypeMap() {
if (stringRedisTemplate == null) {
return new HashMap<>();
}
try {
Map<Object, Object> rawMap = stringRedisTemplate.opsForHash().entries(PRODUCT_TYPE_MAP_PREFIX_TB);
Map<String, String> result = new LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue().toString());
}
return result;
} catch (Exception e) {
log.error("获取淘宝产品类型映射失败", e);
return new HashMap<>();
}
}
private String getTodayDate() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(new Date());
}
}

View File

@@ -9,10 +9,14 @@ 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业务层处理
@@ -146,8 +150,10 @@ public class ErpProductServiceImpl implements IErpProductService
JSONObject response = JSONObject.parseObject(responseBody);
if (response == null || response.getInteger("code") == null || response.getInteger("code") != 0) {
log.error("拉取商品列表失败: {}", responseBody);
return 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);
}
// 解析商品列表
@@ -158,11 +164,20 @@ public class ErpProductServiceImpl implements IErpProductService
}
JSONArray productList = data.getJSONArray("list");
Integer totalCount = data.getInteger("count");
if (productList == null || productList.isEmpty()) {
log.info("拉取商品列表为空");
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++) {
@@ -270,35 +285,181 @@ public class ErpProductServiceImpl implements IErpProductService
}
/**
* 转换商品状态值:将前端的简化状态值转换为API需要的状态值
* 前端1(上架), 2(下架), 3(已售), 22(草稿), 23(审核中)
* API-1(全部), 10(上架), 21(下架), 22(草稿), 23(审核中), 31(已售), 33(已删除), 36(违规)
* 如果前端传入的是API已有的状态值如22、23直接返回否则进行映射转换
* 转换商品状态值将前端状态值转换为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;
}
// 如果是API标准状态值22、23直接返回
if (frontendStatus == 22 || frontendStatus == 23) {
// 直接使用前端传入的状态值(-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;
}
// 前端简化状态值到API状态值的映射
switch (frontendStatus) {
case 1: // 上架
return 10;
case 2: // 下架
return 21;
case 3: // 已售
return 31;
default:
// 如果是其他API标准状态值-1, 10, 21, 31, 33, 36直接返回
if (frontendStatus == -1 || frontendStatus == 10 || frontendStatus == 21 ||
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);
}
}
log.warn("未知的商品状态值: {}, 将不设置状态筛选", frontendStatus);
return null;
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);
}
}

View File

@@ -63,9 +63,19 @@ public class InstructionServiceImpl implements IInstructionService {
@Override
public List<String> execute(String command, boolean forceGenerate) {
return execute(command, forceGenerate, false);
}
@Override
public List<String> execute(String command, boolean forceGenerate, boolean isFromConsole) {
// 存储接收的消息到Redis队列
storeMessageToRedis("instruction:request", command);
// 检测价保/赔付消息,自动标记后返到账
if (command != null && !command.trim().isEmpty()) {
handleCompensationMessage(command);
}
List<String> result;
if (command == null || command.trim().isEmpty()) {
@@ -79,19 +89,26 @@ public class InstructionServiceImpl implements IInstructionService {
}
// TF/H/生/拼多多 生成类指令
else if (input.startsWith("TF")) {
result = Collections.singletonList(handleTF(input));
String tfResult = handleTF(input, forceGenerate);
// 如果包含错误码,按 \n\n 分割成多个结果
if (tfResult != null && (tfResult.contains("ERROR_CODE:ADDRESS_DUPLICATE") || tfResult.contains("ERROR_CODE:ORDER_NUMBER_DUPLICATE"))) {
String[] parts = tfResult.split("\n\n");
result = new ArrayList<>(Arrays.asList(parts));
} else {
result = Collections.singletonList(tfResult);
}
} else if (input.startsWith("H")) {
result = Collections.singletonList(handleH(input));
result = Collections.singletonList(handleH(input, forceGenerate));
} else if (input.startsWith("W")) {
result = Collections.singletonList(handleW(input));
result = Collections.singletonList(handleW(input, forceGenerate));
} else if (input.startsWith("")) {
result = Collections.singletonList(handleSheng(input));
result = Collections.singletonList(handleSheng(input, forceGenerate));
} else if (isPDDWCommand(input)) {
result = Collections.singletonList(handlePDDW(input));
result = Collections.singletonList(handlePDDW(input, forceGenerate));
} else if (input.equals("拼多多") || input.startsWith("拼多多\n") || input.startsWith("拼多多\r\n") || isPDDFormat(input)) {
result = Collections.singletonList(handlePDD(input));
result = Collections.singletonList(handlePDD(input, forceGenerate));
} else if (input.startsWith("录单") || input.startsWith("慢单") || input.startsWith("慢搜") || input.startsWith("慢查") || input.startsWith("")) {
result = handleRecordLikeMulti(input, forceGenerate);
result = handleRecordLikeMulti(input, forceGenerate, isFromConsole);
} else if (input.startsWith("转链") || input.startsWith("文案") || input.startsWith("")) {
result = Collections.singletonList(handleTransferLike(input));
} else {
@@ -209,10 +226,14 @@ public class InstructionServiceImpl implements IInstructionService {
}
private List<String> handleRecordLikeMulti(String input) {
return handleRecordLikeMulti(input, false);
return handleRecordLikeMulti(input, false, false);
}
private List<String> handleRecordLikeMulti(String input, boolean forceGenerate) {
return handleRecordLikeMulti(input, forceGenerate, false);
}
private List<String> handleRecordLikeMulti(String input, boolean forceGenerate, boolean isFromConsole) {
// 仅实现“慢搜/慢查 关键词”与“录单/慢单 日期范围”的只读输出,避免侵入写库
if (input.startsWith("慢搜") || input.startsWith("慢查")) {
String kw = input.replaceFirst("^慢搜|^慢查", "").trim();
@@ -610,7 +631,7 @@ public class InstructionServiceImpl implements IInstructionService {
return outputs.isEmpty() ? Collections.singletonList("无数据") : outputs;
}
if (input.startsWith("")) {
return Collections.singletonList(handleDanWriteDb(input, forceGenerate));
return Collections.singletonList(handleDanWriteDb(input, forceGenerate, isFromConsole));
}
return Collections.singletonList(helpText());
}
@@ -629,8 +650,6 @@ public class InstructionServiceImpl implements IInstructionService {
15639125541
13243039070
*/
phoneWithTF.add("13723151190");
phoneWithTF.add("18839187854");
phoneWithTF.add("15639125541");
phoneWithTF.add("13243039070");
@@ -646,6 +665,10 @@ public class InstructionServiceImpl implements IInstructionService {
}
private String handleTF(String input) {
return handleTF(input, false);
}
private String handleTF(String input, boolean forceGenerate) {
String body = input.replaceFirst("^TF\\s*", "");
body = body.replaceAll("[啊阿]", "");
String[] lines = body.split("\\r?\\n+");
@@ -729,7 +752,7 @@ public class InstructionServiceImpl implements IInstructionService {
StringBuilder order = new StringBuilder();
order.append(""+phone).append("\n").append(fenxiaoInfo).append("\n").append(modelNumber).append("\n").append(jf).append("\n").append(quantityStr).append("\n").append(address);
// 传递第三方单号给 generateOrderText以便在生成订单文本时正确提取
outputs.add(generateOrderText(order.toString(), thirdPartyOrderNo));
outputs.add(generateOrderText(order.toString(), thirdPartyOrderNo, forceGenerate));
} else {
outputs.add("TF 指令格式TF\t分销信息\t分销信息\t分销信息\t型号\t数量\t姓名\t电话\t地址 ;也支持多行,每行一条数据");
}
@@ -740,14 +763,26 @@ public class InstructionServiceImpl implements IInstructionService {
private String handleH(String input) {
return handleHLike(input, "H", "H");
return handleH(input, false);
}
private String handleH(String input, boolean forceGenerate) {
return handleHLike(input, "H", "H", forceGenerate);
}
private String handleW(String input) {
return handleHLike(input, "W", "W");
return handleW(input, false);
}
private String handleW(String input, boolean forceGenerate) {
return handleHLike(input, "W", "W", forceGenerate);
}
private String handleHLike(String input, String commandKeyword, String distributionMark) {
return handleHLike(input, commandKeyword, distributionMark, false);
}
private String handleHLike(String input, String commandKeyword, String distributionMark, boolean forceGenerate) {
String cleaned = input.replaceFirst("^" + Pattern.quote(commandKeyword), "");
if (cleaned.startsWith("\n")) {
cleaned = cleaned.substring(1);
@@ -774,10 +809,14 @@ public class InstructionServiceImpl implements IInstructionService {
StringBuilder sheng = new StringBuilder();
sheng.append("\n").append(distributionMark).append("\n").append(modelNumber).append("\n").append(jfLink != null ? jfLink : "").append("\n")
.append("1\n").append(name).append(fullAddress);
return generateOrderText(sheng.toString());
return generateOrderText(sheng.toString(), null, forceGenerate);
}
private String handleSheng(String input) {
return handleSheng(input, false);
}
private String handleSheng(String input, boolean forceGenerate) {
// 输入格式:
// 生\n分销标记\n型号\n转链链接\n数量\n地址
String[] split = input.split("\n");
@@ -797,7 +836,7 @@ public class InstructionServiceImpl implements IInstructionService {
}
split[3] = link;
split[5] = sanitizeAddress(split[5]);
return generateOrderText(String.join("\n", split));
return generateOrderText(String.join("\n", split), null, forceGenerate);
}
/**
@@ -835,14 +874,26 @@ public class InstructionServiceImpl implements IInstructionService {
}
private String handlePDD(String input) {
return handlePDDWithMark(input, "拼多多", "PDD");
return handlePDD(input, false);
}
private String handlePDD(String input, boolean forceGenerate) {
return handlePDDWithMark(input, "拼多多", "PDD", forceGenerate);
}
private String handlePDDW(String input) {
return handlePDDWithMark(input, "拼多多 W", "PDD-W");
return handlePDDW(input, false);
}
private String handlePDDW(String input, boolean forceGenerate) {
return handlePDDWithMark(input, "拼多多 W", "PDD-W", forceGenerate);
}
private String handlePDDWithMark(String input, String commandKeyword, String distributionMark) {
return handlePDDWithMark(input, commandKeyword, distributionMark, false);
}
private String handlePDDWithMark(String input, String commandKeyword, String distributionMark, boolean forceGenerate) {
// 拼多多新格式:
// <指令>251102-457567158704072
// 赵政委[6947]
@@ -986,7 +1037,7 @@ public class InstructionServiceImpl implements IInstructionService {
.append(fullAddress.toString());
// 传递第三方单号给 generateOrderText
return generateOrderText(sheng.toString(), thirdPartyOrderNo);
return generateOrderText(sheng.toString(), thirdPartyOrderNo, forceGenerate);
}
private boolean shouldAutoFillLink(String distributionMark) {
@@ -1030,10 +1081,14 @@ public class InstructionServiceImpl implements IInstructionService {
}
private String generateOrderText(String shengInput) {
return generateOrderText(shengInput, null);
return generateOrderText(shengInput, null, false);
}
private String generateOrderText(String shengInput, String providedThirdPartyOrderNo) {
return generateOrderText(shengInput, providedThirdPartyOrderNo, false);
}
private String generateOrderText(String shengInput, String providedThirdPartyOrderNo, boolean forceGenerate) {
String[] split = shengInput.split("\n");
// 第一行可能是 生 或 生{备注}
String head = split[0].trim();
@@ -1058,12 +1113,14 @@ public class InstructionServiceImpl implements IInstructionService {
String orderNumberKey = "order_number:" + orderNumberForDedup;
String existedOrderNumber = stringRedisTemplate.opsForValue().get(orderNumberKey);
if (existedOrderNumber != null) {
String warn = "[炸弹] [炸弹] [炸弹] 此订单编号(" + orderNumberForDedup + ")已经存在,请勿重复生成订单 [炸弹] [炸弹] [炸弹] ";
StringBuilder warnOut = new StringBuilder();
warnOut.append(warn).append("\n");
return warnOut.toString().trim();
// 如果强制生成,跳过订单编号重复检查
if (!forceGenerate) {
// 返回特殊错误码,前端会识别并弹出验证码
return "ERROR_CODE:ORDER_NUMBER_DUPLICATE\n此订单编号(" + orderNumberForDedup + ")已经存在,请勿重复生成订单";
}
// forceGenerate为true时跳过订单编号重复检查继续执行
}
// 记录订单编号24小时过期
// 只有在不强制生成或订单编号不存在时才设置Redis强制生成时也更新Redis记录
stringRedisTemplate.opsForValue().set(orderNumberKey, orderNumberForDedup, 1, TimeUnit.DAYS);
}
@@ -1073,12 +1130,15 @@ public class InstructionServiceImpl implements IInstructionService {
String existed = stringRedisTemplate.opsForValue().get(addressKey);
if (existed != null) {
if (!(existed.contains("李波") || existed.contains("吴胜硕") || existed.contains("小硕硕"))) {
String warn = "[炸弹] [炸弹] [炸弹] 此地址已经存在,请勿重复生成订单 [炸弹] [炸弹] [炸弹] ";
StringBuilder warnOut = new StringBuilder();
warnOut.append(warn).append("\n");
return warnOut.toString().trim();
// 如果强制生成,跳过地址重复检查
if (!forceGenerate) {
// 返回特殊错误码,前端会识别并弹出验证码
return "ERROR_CODE:ADDRESS_DUPLICATE\n此地址已经存在请勿重复生成订单";
}
// forceGenerate为true时跳过地址重复检查继续执行
}
}
// 只有在不强制生成或地址不存在时才设置Redis强制生成时也更新Redis记录
stringRedisTemplate.opsForValue().set(addressKey, address, 1, TimeUnit.DAYS);
}
@@ -1217,10 +1277,14 @@ public class InstructionServiceImpl implements IInstructionService {
// ===== "单 …" 写库 =====
private String handleDanWriteDb(String input) {
return handleDanWriteDb(input, false);
return handleDanWriteDb(input, false, false);
}
private String handleDanWriteDb(String input, boolean forceGenerate) {
return handleDanWriteDb(input, forceGenerate, false);
}
private String handleDanWriteDb(String input, boolean forceGenerate, boolean isFromConsole) {
// 保存原始输入,用于返回前端时保留完整物流链接
String originalInput = input.trim().replace("", "");
// 与 JDUtil.parseOrderFromText 一致的模板字段
@@ -1240,8 +1304,8 @@ public class InstructionServiceImpl implements IInstructionService {
return warn;
}
// 订单号重复校验
if (!isEmpty(order.getOrderId())) {
// 订单号重复校验(控制台入口跳过此校验,允许管理员更新单号)
if (!isFromConsole && !isEmpty(order.getOrderId())) {
JDOrder existingByOrderId = jdOrderService.selectJDOrderByOrderId(order.getOrderId());
if (existingByOrderId != null) {
// 如果是更新同一条记录remark相同则不提示重复
@@ -1255,8 +1319,8 @@ public class InstructionServiceImpl implements IInstructionService {
}
}
// 物流链接重复校验
if (!isEmpty(order.getLogisticsLink())) {
// 物流链接重复校验(控制台入口跳过此校验,允许管理员更新物流链接)
if (!isFromConsole && !isEmpty(order.getLogisticsLink())) {
JDOrder existingByLogistics = jdOrderService.selectJDOrderByLogisticsLink(order.getLogisticsLink());
if (existingByLogistics != null) {
// 如果是更新同一条记录remark相同则不提示重复
@@ -1270,23 +1334,12 @@ public class InstructionServiceImpl implements IInstructionService {
}
}
// 地址重复检查
// 地址重复提示(不阻断写入,与 JDUtil 提示一致)
List<JDOrder> byAddress = jdOrderService.selectJDOrderListByAddress(order.getAddress());
if (byAddress != null && !byAddress.isEmpty()) {
// 如果强制生成,跳过地址重复检查
if (!forceGenerate) {
// 返回特殊错误码,前端会识别并弹出验证码
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
int count = byAddress.size();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < Math.min(count, 3); i++) {
if (i > 0) sb.append("\n");
sb.append(sdf.format(byAddress.get(i).getOrderTime()));
}
// 使用特殊错误码标识地址重复
return "ERROR_CODE:ADDRESS_DUPLICATE\n收货地址重复此地址共" + count + "个订单,最近的订单时间:" + sb.toString();
}
// forceGenerate为true时跳过地址重复检查继续执行
java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String latest = sdf.format(byAddress.get(0).getOrderTime());
// 仅提示,不阻断写入
}
// 根据订单号查询order_rows获取京粉实际价格
@@ -1302,8 +1355,8 @@ public class InstructionServiceImpl implements IInstructionService {
}
}
// 验证1如果查询不到对应订单提示用户
if (isEmpty(order.getOrderId()) || orderRow == null) {
// 验证1如果查询不到对应订单提示用户(控制台入口跳过此校验)
if (!isFromConsole && (isEmpty(order.getOrderId()) || orderRow == null)) {
String warn = "[炸弹] [炸弹] [炸弹] 录单警告!!! \n查询不到对应订单使用红包 或者 转链),请五分钟后再试,或重新下单";
return warn;
}
@@ -1321,13 +1374,28 @@ public class InstructionServiceImpl implements IInstructionService {
}
}
// 按 remark 判断新增/更新
JDOrder exists = jdOrderService.selectJDOrderByRemark(order.getRemark());
if (exists != null) {
order.setId(exists.getId());
jdOrderService.updateJDOrder(order);
} else {
jdOrderService.insertJDOrder(order);
// 判断新增/更新:所有入口都优先根据订单号更新(用于刷新数据)
JDOrder exists = null;
// 如果订单号不为空,优先根据订单号查找已存在的订单(用于刷新数据)
if (!isEmpty(order.getOrderId())) {
exists = jdOrderService.selectJDOrderByOrderId(order.getOrderId());
if (exists != null) {
// 根据订单号找到已存在的订单,更新该订单(刷新数据)
order.setId(exists.getId());
jdOrderService.updateJDOrder(order);
}
}
// 如果没有通过订单号找到,则按 remark 判断新增/更新
if (exists == null) {
exists = jdOrderService.selectJDOrderByRemark(order.getRemark());
if (exists != null) {
order.setId(exists.getId());
jdOrderService.updateJDOrder(order);
} else {
jdOrderService.insertJDOrder(order);
}
}
// H-TF订单触发延迟推送机制
@@ -1822,6 +1890,110 @@ public class InstructionServiceImpl implements IInstructionService {
}
}, "TencentDoc-Writer-" + order.getRemark()).start();
}
/**
* 处理价保/赔付消息,自动标记后返到账
* 消息格式示例:
* [爱心] 价保/赔付 100 [爱心]
* [裂开] 违规订单-违反京东平台规则
* 京粉:凡
* 订单343216308806 (非plus)
* ...
*/
private void handleCompensationMessage(String message) {
try {
// 检测是否包含价保/赔付和违规订单-违反京东平台规则
if (message.contains("价保/赔付") || message.contains("价保") || message.contains("赔付")) {
if (message.contains("违规订单-违反京东平台规则")) {
// 提取订单号订单343216308806
Pattern orderPattern = Pattern.compile("订单[:]\\s*(\\d+)");
Matcher matcher = orderPattern.matcher(message);
if (matcher.find()) {
String orderId = matcher.group(1);
// 查找订单
JDOrder order = jdOrderService.selectJDOrderByOrderId(orderId);
if (order != null) {
// 设置后返到账状态为1并设置到账日期为当前时间
order.setIsRebateReceived(1);
order.setRebateReceivedDate(new Date());
// 更新订单
int updateResult = jdOrderService.updateJDOrder(order);
if (updateResult > 0) {
System.out.println("✓ 自动标记后返到账成功 - 订单号: " + orderId);
} else {
System.err.println("✗ 自动标记后返到账失败 - 订单号: " + orderId + " (更新失败)");
}
} else {
System.err.println("✗ 未找到订单 - 订单号: " + orderId);
}
} else {
System.err.println("✗ 无法从消息中提取订单号");
}
}
}
} catch (Exception e) {
System.err.println("✗ 处理价保/赔付消息时发生异常: " + e.getMessage());
e.printStackTrace();
}
}
/**
* 一次性批量更新历史订单将赔付金额大于0的订单标记为后返到账
* 此方法只应执行一次,用于处理历史数据
*/
public void batchMarkRebateReceivedForCompensation() {
try {
System.out.println("开始批量标记后返到账(赔付金额>0的订单...");
// 查询所有订单
List<JDOrder> allOrders = jdOrderService.selectJDOrderList(new JDOrder());
// 查询所有订单的赔付金额从OrderRows表
int updatedCount = 0;
int skippedCount = 0;
for (JDOrder order : allOrders) {
// 查询订单的赔付金额
OrderRows orderRows = orderRowsService.selectOrderRowsByOrderId(order.getOrderId());
if (orderRows != null && orderRows.getProPriceAmount() != null && orderRows.getProPriceAmount() > 0) {
// 如果赔付金额大于0且后返到账状态不是1则更新
if (order.getIsRebateReceived() == null || order.getIsRebateReceived() != 1) {
order.setIsRebateReceived(1);
// 如果到账日期为空,设置为当前时间
if (order.getRebateReceivedDate() == null) {
order.setRebateReceivedDate(new Date());
}
int updateResult = jdOrderService.updateJDOrder(order);
if (updateResult > 0) {
updatedCount++;
System.out.println("✓ 标记后返到账 - 订单号: " + order.getOrderId() +
", 赔付金额: " + orderRows.getProPriceAmount());
} else {
System.err.println("✗ 更新失败 - 订单号: " + order.getOrderId());
}
} else {
skippedCount++;
}
} else {
skippedCount++;
}
}
System.out.println("批量标记完成!");
System.out.println("更新数量: " + updatedCount);
System.out.println("跳过数量: " + skippedCount);
System.out.println("总计: " + allOrders.size());
} catch (Exception e) {
System.err.println("✗ 批量标记后返到账时发生异常: " + e.getMessage());
e.printStackTrace();
}
}
}

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,46 +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 + orderId;
String existingWaybillNo = stringRedisTemplate.opsForValue().get(redisKey);
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;
}
// 兼容处理如果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: {}", order.getId(), waybillNo);
logger.warn("企业微信推送未确认成功,稍后将重试 - 订单ID: {}, waybill_no: {}", orderId, waybillNo);
return false;
}
// 保存运单号到Redis避免重复处理
String redisKey = REDIS_WAYBILL_KEY_PREFIX + order.getId();
stringRedisTemplate.opsForValue().set(redisKey, waybillNo, 30, TimeUnit.DAYS);
// 保存运单号到Redis避免重复处理- 使用原子操作确保只写入一次
Boolean setSuccess = stringRedisTemplate.opsForValue().setIfAbsent(redisKey, waybillNo, 30, TimeUnit.DAYS);
logger.info("物流信息获取并推送成功 - 订单ID: {}, waybill_no: {}", order.getId(), waybillNo);
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());
}
}
}
@@ -141,15 +224,32 @@ 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);
String pushResult = sendPostWithHeaders(PUSH_URL, jsonBody, PUSH_TOKEN);
if (pushResult == null || pushResult.trim().isEmpty()) {
logger.warn("企业应用推送响应为空 - 订单ID: {}, waybill_no: {}", order.getId(), waybillNo);
return false;
@@ -173,37 +273,132 @@ public class LogisticsServiceImpl implements ILogisticsService {
}
}
/**
* 根据分销标识获取接收人列表
* 从系统配置中读取配置键名格式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.warn("解析企业应用推送响应失败,将视为未成功: {}", e.getMessage());
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

@@ -0,0 +1,100 @@
package com.ruoyi.jarvis.service.impl;
import com.ruoyi.jarvis.domain.TaobaoComment;
import com.ruoyi.jarvis.mapper.TaobaoCommentMapper;
import com.ruoyi.jarvis.service.ITaobaoCommentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 淘宝评论管理 Service 实现
*/
@Service
public class TaobaoCommentServiceImpl implements ITaobaoCommentService {
private static final String PRODUCT_TYPE_MAP_PREFIX_TB = "product_type_map_tb";
@Autowired
private TaobaoCommentMapper taobaoCommentMapper;
@Autowired(required = false)
private StringRedisTemplate stringRedisTemplate;
@Override
public List<TaobaoComment> selectTaobaoCommentList(TaobaoComment taobaoComment) {
List<TaobaoComment> list = taobaoCommentMapper.selectTaobaoCommentList(taobaoComment);
// 填充Redis映射的产品类型信息
if (stringRedisTemplate != null) {
Map<String, String> tbMap = getTbProductTypeMap();
for (TaobaoComment c : list) {
// 查找对应的产品类型
String productId = c.getProductId();
if (tbMap != null) {
for (Map.Entry<String, String> entry : tbMap.entrySet()) {
if (entry.getValue().equals(productId)) {
c.setProductType(entry.getKey());
c.setMappedProductId(productId);
break;
}
}
}
}
}
return list;
}
@Override
public TaobaoComment selectTaobaoCommentById(Integer id) {
TaobaoComment comment = taobaoCommentMapper.selectTaobaoCommentById(id);
if (comment != null && stringRedisTemplate != null) {
Map<String, String> tbMap = getTbProductTypeMap();
if (tbMap != null) {
String productId = comment.getProductId();
for (Map.Entry<String, String> entry : tbMap.entrySet()) {
if (entry.getValue().equals(productId)) {
comment.setProductType(entry.getKey());
comment.setMappedProductId(productId);
break;
}
}
}
}
return comment;
}
@Override
public int updateTaobaoCommentIsUse(TaobaoComment taobaoComment) {
return taobaoCommentMapper.updateTaobaoCommentIsUse(taobaoComment);
}
@Override
public int deleteTaobaoCommentByIds(Integer[] ids) {
return taobaoCommentMapper.deleteTaobaoCommentByIds(ids);
}
@Override
public int resetTaobaoCommentIsUseByProductId(String productId) {
return taobaoCommentMapper.resetTaobaoCommentIsUseByProductId(productId);
}
private Map<String, String> getTbProductTypeMap() {
if (stringRedisTemplate == null) {
return null;
}
try {
Map<Object, Object> rawMap = stringRedisTemplate.opsForHash().entries(PRODUCT_TYPE_MAP_PREFIX_TB);
Map<String, String> result = new java.util.LinkedHashMap<>();
for (Map.Entry<Object, Object> entry : rawMap.entrySet()) {
result.put(entry.getKey().toString(), entry.getValue().toString());
}
return result;
} catch (Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,76 @@
<?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.CommentMapper">
<resultMap id="CommentResult" type="Comment">
<result property="id" column="id"/>
<result property="productId" column="product_id"/>
<result property="userName" column="user_name"/>
<result property="commentText" column="comment_text"/>
<result property="commentId" column="comment_id"/>
<result property="pictureUrls" column="picture_urls"/>
<result property="createdAt" column="created_at"/>
<result property="commentDate" column="comment_date"/>
<result property="isUse" column="is_use"/>
</resultMap>
<sql id="selectCommentBase">
select id, product_id, user_name, comment_text, comment_id, picture_urls,
created_at, comment_date, is_use
from comments
</sql>
<select id="selectCommentList" parameterType="Comment" resultMap="CommentResult">
<include refid="selectCommentBase"/>
<where>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="userName != null and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
<if test="commentText != null and commentText != ''"> and comment_text like concat('%', #{commentText}, '%')</if>
<if test="isUse != null"> and is_use = #{isUse}</if>
<if test="params.beginTime != null and params.beginTime != ''">
and created_at &gt;= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''">
and created_at &lt;= #{params.endTime}
</if>
</where>
order by created_at desc
</select>
<select id="selectCommentById" parameterType="Long" resultMap="CommentResult">
<include refid="selectCommentBase"/>
where id = #{id}
</select>
<select id="selectCommentStatisticsByProductId" parameterType="String" resultType="java.util.Map">
select
count(*) as totalCount,
sum(case when is_use = 0 then 1 else 0 end) as availableCount,
sum(case when is_use = 1 then 1 else 0 end) as usedCount
from comments
where product_id = #{productId}
</select>
<update id="updateCommentIsUse" parameterType="Comment">
update comments
<set>
<if test="isUse != null">is_use = #{isUse},</if>
</set>
where id = #{id}
</update>
<update id="resetCommentIsUseByProductId" parameterType="String">
update comments
set is_use = 0
where product_id = #{productId}
</update>
<delete id="deleteCommentByIds" parameterType="String">
delete from comments where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</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,18 +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="thirdPartyOrderNo != null and thirdPartyOrderNo != ''"> and third_party_order_no like concat('%', #{thirdPartyOrderNo}, '%')</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>
@@ -61,18 +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="thirdPartyOrderNo != null and thirdPartyOrderNo != ''"> and third_party_order_no like concat('%', #{thirdPartyOrderNo}, '%')</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>
@@ -102,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>
@@ -131,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}

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,76 @@
<?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.TaobaoCommentMapper">
<resultMap id="TaobaoCommentResult" type="TaobaoComment">
<result property="id" column="id"/>
<result property="productId" column="product_id"/>
<result property="userName" column="user_name"/>
<result property="commentText" column="comment_text"/>
<result property="commentId" column="comment_id"/>
<result property="pictureUrls" column="picture_urls"/>
<result property="createdAt" column="created_at"/>
<result property="commentDate" column="comment_date"/>
<result property="isUse" column="is_use"/>
</resultMap>
<sql id="selectTaobaoCommentBase">
select id, product_id, user_name, comment_text, comment_id, picture_urls,
created_at, comment_date, is_use
from taobao_comments
</sql>
<select id="selectTaobaoCommentList" parameterType="TaobaoComment" resultMap="TaobaoCommentResult">
<include refid="selectTaobaoCommentBase"/>
<where>
<if test="productId != null and productId != ''"> and product_id = #{productId}</if>
<if test="userName != null and userName != ''"> and user_name like concat('%', #{userName}, '%')</if>
<if test="commentText != null and commentText != ''"> and comment_text like concat('%', #{commentText}, '%')</if>
<if test="isUse != null"> and is_use = #{isUse}</if>
<if test="params.beginTime != null and params.beginTime != ''">
and created_at &gt;= #{params.beginTime}
</if>
<if test="params.endTime != null and params.endTime != ''">
and created_at &lt;= #{params.endTime}
</if>
</where>
order by created_at desc
</select>
<select id="selectTaobaoCommentById" parameterType="Integer" resultMap="TaobaoCommentResult">
<include refid="selectTaobaoCommentBase"/>
where id = #{id}
</select>
<select id="selectTaobaoCommentStatisticsByProductId" parameterType="String" resultType="java.util.Map">
select
count(*) as totalCount,
sum(case when is_use = 0 then 1 else 0 end) as availableCount,
sum(case when is_use = 1 then 1 else 0 end) as usedCount
from taobao_comments
where product_id = #{productId}
</select>
<update id="updateTaobaoCommentIsUse" parameterType="TaobaoComment">
update taobao_comments
<set>
<if test="isUse != null">is_use = #{isUse},</if>
</set>
where id = #{id}
</update>
<update id="resetTaobaoCommentIsUseByProductId" parameterType="String">
update taobao_comments
set is_use = 0
where product_id = #{productId}
</update>
<delete id="deleteTaobaoCommentByIds" parameterType="String">
delete from taobao_comments where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

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

@@ -6,7 +6,7 @@ CREATE TABLE `erp_product` (
`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:上架 2:下架 3:',
`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 '上架时间(时间戳)',