package cn.van.business.util; import cn.van.business.model.jd.OrderRow; import cn.van.business.repository.OrderRowRepository; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import com.fasterxml.jackson.databind.ObjectMapper; import com.jd.open.api.sdk.DefaultJdClient; import com.jd.open.api.sdk.JdClient; import com.jd.open.api.sdk.domain.kplunion.GoodsService.request.query.GoodsReq; import com.jd.open.api.sdk.domain.kplunion.GoodsService.response.query.GoodsQueryResult; import com.jd.open.api.sdk.domain.kplunion.GoodsService.response.query.UrlInfo; import com.jd.open.api.sdk.domain.kplunion.promotionbysubunioni.PromotionService.request.get.PromotionCodeReq; import com.jd.open.api.sdk.request.kplunion.*; import com.jd.open.api.sdk.response.kplunion.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; 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.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import static cn.van.business.util.JDUtil.UserInteractionState.GiftMoneyStep.*; import static cn.van.business.util.JDUtil.UserInteractionState.ProcessState.INIT; import static cn.van.business.util.WXUtil.chatRoom_BY; import static cn.van.business.util.WXUtil.super_admins; /** * @author Leo * @version 1.0 * @create 2024/11/5 17:40 * @description: */ @Component public class JDUtil { static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); /** * 密钥配置 */ // van论坛 private static final String LPF_APP_KEY_WZ = "98e21c89ae5610240ec3f5f575f86a59"; private static final String LPF_SECRET_KEY_WZ = "3dcb6b23a1104639ac433fd07adb6dfb"; private static final String LPF_WZ_ID = "4101253066"; // 导购的 private static final String LPF_APP_KEY_DG = "faf410cb9587dc80dc7b31e321d7d322"; private static final String LPF_SECRET_KEY_DG = "a4fb15d7bedd4316b97b4e96e4effc1c"; private static final String LL_APP_KEY_DG = "9c2011409f0fc906b73432dd3687599d"; private static final String LL_SECRET_KEY_DG = "3ceddff403e544a8a2eacc727cf05dab"; private static final String SERVER_URL = "https://api.jd.com/routerjson"; //accessToken private static final String ACCESS_TOKEN = ""; private static final Logger logger = LoggerFactory.getLogger(JDUtil.class); private static final String INTERACTION_STATE_PREFIX = "interaction_state:"; private static final long TIMEOUT_MINUTES = 1; private static final String WENAN_FANAN = "提供方法自己下单\n" + "全程都是自己的账号下单\n" + "标价就是下单的到手价\n" + "(不包含教程费)\n" + "本人有耐心,会一步一步提供教程\n" + "以后有什么质量问题都是用自己的账号走京东售后\n" + "\n" + "更新\n" + "\n" + "用你自己的账号下单\n" + "官方店铺 提供方法自己下单\n" + "不含教程费,价格不对不收费\n" + "教程费用私聊我问价\n" + "——————————————————————————————————————————\n" + "同行可长久合作,可提供神级家电线报\n" + "\n" + "配合家电线报可以自己下单,不用找代购和代下,订单和利润都掌握在自己手中。\n" + "\n" + "一次入会永久使用,包含家电帮,雷神价,韭菜帮,河南&湖南帮,各种暗号帮后返等内部独家家电线报\n" + "\n" + "JD采销采购不定时发放独家优惠券\n" + "\n" + "基本上你能看到的京东家电低价都是从这些渠道里面出来。\n" + "\n" + "2025年家电项目新方向,配合家电线报下单,秒省1K+。"; private static final String WENAN_ZCXS = "\n" + "购买后,两小时内出库,物流会电话联系您,同时生成京东官方安装单。送装一体,无需担心。\n" + "\n" + "\n" + "1:全新正品,原包装未拆封(京东商城代购,就近直发)\n" + "2:可提供下单运单号与电子发票(发票在收到货后找我要)。\n" + "3:收货时查看是否有质量或运损问题。可拍照让京东免费申请换新。\n" + "4:下单后非质量问题不支持退款退货,强制退扣100元。\n" + "5:价格有浮动,不支持补差价,谢谢理解。\n" + "6:全国联保,全国统一安装标准。支持官方 400,服务号查询,假一赔十。\n"; private static final String FANAN_COMMON = "\n复制到薇,依次领券,到J东APP结算\n" + "换新选生活电器-除螨仪,可免回收\n"; final WXUtil wxUtil; private final StringRedisTemplate redisTemplate; private final OrderRowRepository orderRowRepository; private final OrderUtil orderUtil; // 添加ObjectMapper来序列化和反序列化UserInteractionState private final ObjectMapper objectMapper = new ObjectMapper(); private final ConcurrentHashMap userInteractionStates = new ConcurrentHashMap<>(); private HashMap cacheMap = new HashMap<>(); // 构造函数中注入StringRedisTemplate @Autowired public JDUtil(StringRedisTemplate redisTemplate, OrderRowRepository orderRowRepository, WXUtil wxUtil, OrderUtil orderUtil) { this.redisTemplate = redisTemplate; this.orderRowRepository = orderRowRepository; this.wxUtil = wxUtil; this.orderUtil = orderUtil; } private static List filterOrdersByDate(List orderRows, int daysBack) { LocalDate now = LocalDate.now(); return orderRows.stream().filter(order -> { // 将 Date 转换为 LocalDate LocalDate orderDate = order.getOrderTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); // 计算是否在给定的天数内 return !orderDate.isBefore(now.minusDays(daysBack)) && !orderDate.isAfter(now); }).collect(Collectors.toList()); } private static Stream getStreamForWeiGui(List todayOrders) { return todayOrders.stream().filter(orderRow -> orderRow.getValidCode() == 13 || orderRow.getValidCode() == 25 || orderRow.getValidCode() == 26 || orderRow.getValidCode() == 27 || orderRow.getValidCode() == 28 || orderRow.getValidCode() == 29); } /** * 拼多多地址 */ public String convertAddress(String input) { try { // 正则表达式分解输入内容 Pattern pattern = Pattern.compile("^(.+?)\\[(\\d+)\\]\\s+(\\d+)\\s+(.+)\\[\\d+\\]$"); Matcher matcher = pattern.matcher(input); if (matcher.matches()) { // 提取各部分内容 String name = matcher.group(1); String code = matcher.group(2); String phone = matcher.group(3); String address = matcher.group(4).trim(); // 构造新地址格式 return String.format("%s 13068923963 %s联系客户%s转%s", name, address, phone, code); } } catch (Exception ignored) { return "地址格式不匹配"; } return "地址格式不匹配"; } public void sendOrderToWxByOrderDefault(String order, String fromWxid) { logger.info("执行 sendOrderToWxByOrderDefault 方法,order: {}, fromWxid: {}", order, fromWxid); if ("礼".equals(order)) { // 设置状态为等待文案输入 String key = INTERACTION_STATE_PREFIX + fromWxid; UserInteractionState state = loadOrCreateState(key); state.setCurrentState(UserInteractionState.ProcessState.GIFT_MONEY_FLOW); state.setCurrentStep(UserInteractionState.GiftMoneyStep.STEP_WAIT_FOR_CONTENT); saveState(key, state); // 提示用户输入文案 wxUtil.sendTextMessage(fromWxid, "请输入包含商品链接的文案:", 1, fromWxid, false); } else { handleUserInteraction(fromWxid, order); } } /** * 礼金创建流程 - 循环处理商品 */ private void processGiftMoneyFlow(String wxid, String message, UserInteractionState state) { try { // 提取文案中的 u.jd.com 链接 List urls = extractUJDUrls(message); if (urls.isEmpty()) { wxUtil.sendTextMessage(wxid, "未找到有效的商品链接,请重新输入包含商品链接的文案。", 1, wxid, false); return; } // 查询商品信息,筛选有效商品 List validProducts = new ArrayList<>(); for (String url : urls) { try { GoodsQueryResult productInfo = queryProductInfoByUJDUrl(url); if (productInfo != null && productInfo.getCode() == 200 && productInfo.getTotalCount() > 0) { validProducts.add(productInfo); } } catch (Exception e) { logger.error("查询商品信息失败:{}", url, e); } } if (validProducts.isEmpty()) { wxUtil.sendTextMessage(wxid, "未找到有效的商品信息,请检查链接是否正确。", 1, wxid, false); return; } // 缓存商品信息 cacheMap.put("validProducts" + wxid, JSON.toJSONString(validProducts)); // 更新状态 state.setCurrentStep(UserInteractionState.GiftMoneyStep.STEP_AMOUNT); state.getCollectedFields().clear(); state.getCollectedFields().put("productIndex", "0"); // 当前处理的商品索引 state.getCollectedFields().put("totalProducts", String.valueOf(validProducts.size())); // 总商品数 saveState(INTERACTION_STATE_PREFIX + wxid, state); // 提示用户输入金额 wxUtil.sendTextMessage(wxid, "请输入开通金额(1-50元,支持小数点后两位):", 1, wxid, false); } catch (Exception e) { logger.error("处理礼金文案异常 - 用户: {}", wxid, e); wxUtil.sendTextMessage(wxid, "处理请求时发生异常,请重试", 1, wxid, false); resetState(wxid, state); } } public void cjwlm(String message) { logger.info("执行 cjwlm 方法,order: {}", message); if (message.contains("https://u.jd")) { wxUtil.sendTextMessage(chatRoom_BY, "[爱心]---采集开始---[爱心] \n " + message, 1, chatRoom_BY, true); // 2. 生成推广内容 HashMap> contentResult = generatePromotionContent(message); // 3. 发送文本内容 for (String text : contentResult.get("text")) { wxUtil.sendTextMessage(chatRoom_BY, text, 1, chatRoom_BY, true); } // 4. 发送图片(如果有) List images = contentResult.get("images"); if (images != null) { try { Thread.sleep(6000); } catch (InterruptedException ignored) { } for (String imageUrl : images) { if (imageUrl != null) { wxUtil.sendImageMessage(chatRoom_BY, imageUrl); } } } wxUtil.sendTextMessage(chatRoom_BY, "[爱心]---采集完成---[爱心]", 1, chatRoom_BY, true); } // 具体逻辑 } private OrderStats calculateStats(List orders) { long paid = orders.stream().filter(o -> o.getValidCode() == 16).count(); long pending = orders.stream().filter(o -> o.getValidCode() == 15).count(); long canceled = orders.stream().filter(o -> o.getValidCode() != 16 && o.getValidCode() != 17).count(); long completed = orders.stream().filter(o -> o.getValidCode() == 17).count(); return new OrderStats(orders.size(), orders.size() - canceled, paid, orders.stream().filter(o -> o.getValidCode() == 16).mapToDouble(OrderRow::getEstimateFee).sum(), pending, orders.stream().filter(o -> o.getValidCode() == 15).mapToDouble(OrderRow::getEstimateFee).sum(), canceled, completed, orders.stream().filter(o -> o.getValidCode() == 17).mapToDouble(OrderRow::getEstimateFee).sum(), getStreamForWeiGui(orders).count(), getStreamForWeiGui(orders).mapToDouble(o -> o.getEstimateCosPrice() * o.getCommissionRate() * 0.01).sum()); } private StringBuilder buildStatsContent(String title, OrderStats stats) { StringBuilder content = new StringBuilder(); content//[爱心][Wow][Packet][Party][Broken][心碎][亲亲][色] .append("* ").append(title).append(" *\n").append("━━━━━━━━━━━━\n").append("[爱心] 订单总数:").append(stats.getTotalOrders()).append("\n") // [文件] .append("[Party] 有效订单:").append(stats.getValidOrders()).append("\n") // [OK] .append("[心碎]已取消:").append(stats.getCanceledOrders()).append("\n") // [禁止] .append("────────────\n").append("[爱心]已付款:").append(stats.getPaidOrders()).append("\n") // [钱袋] .append("[Packet] 已付款佣金:").append(String.format("%.2f", stats.getPaidCommission())).append("\n") // [钞票] .append("────────────\n").append("[Wow] 待付款:").append(stats.getPendingOrders()).append("\n") // [时钟] .append("[Packet] 待付款佣金:").append(String.format("%.2f", stats.getPendingCommission())).append("\n") // [钱] .append("────────────\n").append("[亲亲] 已完成:").append(stats.getCompletedOrders()).append("\n") // [旗帜] .append("[Packet] 已完成佣金:").append(String.format("%.2f", stats.getCompletedCommission())).append("\n") // [信用卡] .append("────────────\n").append("[Emm] 违规订单:").append(stats.getViolations()).append("\n") // [警告] .append("[Broken] 违规佣金:").append(String.format("%.2f", stats.getViolationCommission())).append("\n") // [炸弹] .append("━━━━━━━━━━━━"); return content; } public void sendOrderToWxByOrderPDD(String order, String fromWxid) { String convertAddress = convertAddress(order); wxUtil.sendTextMessage(fromWxid, convertAddress, 0, fromWxid, false); } /** * 接收京粉指令指令 */ public void sendOrderToWxByOrderJD(String order, String fromWxid) { int[] param = {-1}; WXUtil.SuperAdmin superAdmin = super_admins.get(fromWxid); String unionId = superAdmin.getUnionId(); List orderRows = orderRowRepository.findByValidCodeNotInOrderByOrderTimeDescAndUnionId(param, Long.valueOf(unionId)); /** * 菜单: * 今日统计 * 昨日统计 * 最近七天统计 * 最近一个月统计 * 今天订单 * 昨天订单 * */ List contents = new ArrayList<>(); StringBuilder content = new StringBuilder(); switch (order) { case "菜单": content.append("菜单:京+命令 \n 如: 京今日统计\r"); content.append("今日统计\r"); content.append("昨天统计\r"); content.append("七日统计\r"); content.append("一个月统计\r"); content.append("两个月统计\r"); content.append("三个月统计\r"); content.append("总统计\r\n"); content.append("这个月统计\r"); content.append("上个月统计\r\n"); content.append("今日订单\r"); content.append("昨日订单\r"); content.append("七日订单\r"); content.append("刷新7天\r"); contents.add(content); content = new StringBuilder(); content.append("高级菜单:京+高级+命令 \n 如: 京高级违规30\r"); content.append("京高级违规+整数(不传数字为365天)\r"); content.append("京高级SKU+sku\r"); content.append("京高级搜索+搜索标题(精准查询订单号+精准查询sku+模糊查询收件人+模糊查询地址),只返回最近100条\r"); contents.add(content); break; case "测试指令": { //test01(); break; } case "今日统计": { // 订单总数,已付款,已取消,佣金总计 List todayOrders = filterOrdersByDate(orderRows, 0); OrderStats stats = calculateStats(todayOrders); contents.add(buildStatsContent("今日统计", stats)); break; } case "昨日统计": { List yesterdayOrders = filterOrdersByDate(orderRows, 1); OrderStats stats = calculateStats(yesterdayOrders); contents.add(buildStatsContent("昨日统计", stats)); break; } case "三日统计": { List last3DaysOrders = filterOrdersByDate(orderRows, 3); OrderStats stats = calculateStats(last3DaysOrders); contents.add(buildStatsContent("三日统计", stats)); break; } case "七日统计": { List last7DaysOrders = filterOrdersByDate(orderRows, 7); OrderStats stats = calculateStats(last7DaysOrders); contents.add(buildStatsContent("七日统计", stats)); break; } case "一个月统计": { List last30DaysOrders = filterOrdersByDate(orderRows, 30); OrderStats stats = calculateStats(last30DaysOrders); contents.add(buildStatsContent("一个月统计", stats)); break; } case "两个月统计": { List last60DaysOrders = filterOrdersByDate(orderRows, 60); OrderStats stats = calculateStats(last60DaysOrders); contents.add(buildStatsContent("两个月统计", stats)); break; } case "三个月统计": { List last90DaysOrders = filterOrdersByDate(orderRows, 90); OrderStats stats = calculateStats(last90DaysOrders); contents.add(buildStatsContent("三个月统计", stats)); break; } case "这个月统计": { // 计算出距离1号有几天 int days = LocalDate.now().getDayOfMonth(); List thisMonthOrders = filterOrdersByDate(orderRows, days); OrderStats stats = calculateStats(thisMonthOrders); contents.add(buildStatsContent("这个月统计", stats)); break; } case "上个月统计": { LocalDate lastMonth = LocalDate.now().minusMonths(1); int days = LocalDate.now().getDayOfMonth(); List lastMonthOrders = filterOrdersByDate(orderRows, lastMonth.lengthOfMonth() + days); List thisMonthOrders = filterOrdersByDate(orderRows, days); lastMonthOrders = lastMonthOrders.stream().filter(orderRow -> !thisMonthOrders.contains(orderRow)).collect(Collectors.toList()); OrderStats stats = calculateStats(lastMonthOrders); contents.add(buildStatsContent("上个月统计", stats)); break; } //总统计 case "总统计": { OrderStats stats = calculateStats(orderRows); contents.add(buildStatsContent("总统计", stats)); break; } case "今日订单": { List todayOrders = filterOrdersByDate(orderRows, 0); // 订单总数,已付款,已取消,佣金总计 OrderStats stats = calculateStats(todayOrders); contents.add(buildStatsContent("今日统计", stats)); if (!todayOrders.isEmpty()) { orderUtil.orderToWxBatch(todayOrders); } break; } case "昨日订单": { content = new StringBuilder(); List yesterdayOrders = filterOrdersByDate(orderRows, 1); List todayOrders = filterOrdersByDate(orderRows, 0); logger.info("原始订单数量:{}", orderRows.size()); logger.info("昨日过滤后数量:{}", yesterdayOrders.size()); yesterdayOrders.removeAll(todayOrders); logger.info("今日过滤后数量:{}", todayOrders.size()); logger.info("最终昨日订单数量:{}", yesterdayOrders.size()); OrderStats stats = calculateStats(yesterdayOrders); contents.add(buildStatsContent("昨日统计", stats)); if (!yesterdayOrders.isEmpty()) { orderUtil.orderToWxBatch(yesterdayOrders); } break; } case "七日订单": { List last7DaysOrders = filterOrdersByDate(orderRows, 1); List todayOrders = filterOrdersByDate(orderRows, 0); last7DaysOrders.removeAll(todayOrders); OrderStats stats = calculateStats(last7DaysOrders); contents.add(buildStatsContent("七日统计", stats)); if (!last7DaysOrders.isEmpty()) { orderUtil.orderToWxBatch(last7DaysOrders); } break; } default: sendOrderToWxByOrderJDAdvanced(order, fromWxid); } if (!contents.isEmpty()) { for (StringBuilder stringBuilder : contents) { wxUtil.sendTextMessage(fromWxid, stringBuilder.toString(), 1, fromWxid, false); } } } /** * 接收京粉指令指令 * 高级菜单 */ public void sendOrderToWxByOrderJDAdvanced(String order, String fromWxid) { int[] param = {-1}; WXUtil.SuperAdmin superAdmin = super_admins.get(fromWxid); String unionId = superAdmin.getUnionId(); List orderRows = orderRowRepository.findByValidCodeNotInOrderByOrderTimeDescAndUnionId(param, Long.valueOf(unionId)); List contents = new ArrayList<>(); StringBuilder content = new StringBuilder(); if (order.startsWith("高级")) { content = new StringBuilder(); order = order.replace("高级", ""); if (order.startsWith("违规")) { String days = order.replace("违规", ""); Integer daysInt = 365; if (Util.isNotEmpty(days)) { daysInt = Integer.parseInt(days); } List filterOrdersByDays = filterOrdersByDate(orderRows, daysInt); content.append("违规排行:"); content.append(daysInt).append("天").append("\r\n"); Map skuIdViolationCountMap = filterOrdersByDays.stream().filter(orderRow -> orderRow.getValidCode() == 27 || orderRow.getValidCode() == 28).filter(orderRow -> orderRow.getSkuName() != null).collect(Collectors.groupingBy(OrderRow::getSkuName, // ✅ 拼接SKU Collectors.counting())); Map> orderInfoMap = filterOrdersByDays.stream().filter(orderRow -> orderRow.getValidCode() == 27 || orderRow.getValidCode() == 28).filter(orderRow -> orderRow.getSkuName() != null).map(orderRow -> { OrderInfo info = new OrderInfo(); info.setSkuName(orderRow.getSkuName()); info.setOrderId(orderRow.getOrderId()); info.setOrderDate(orderRow.getOrderTime()); return info; }).collect(Collectors.groupingBy(OrderInfo::getSkuName)); List> sortedViolationCounts = skuIdViolationCountMap.entrySet().stream().sorted(Map.Entry.comparingByValue(Comparator.reverseOrder())).collect(Collectors.toList()); Integer num = 0; for (Map.Entry entry : sortedViolationCounts) { num++; String skuName = entry.getKey(); Long count = entry.getValue(); // 修改后直接使用已包含SKU信息的key content.append("\n").append(num).append(",商品:").append(entry.getKey()) // 这里已包含SKU信息 .append("\r\r").append(" 违规次数:").append(count).append("\r"); List infos = orderInfoMap.get(skuName); if (infos != null) { for (OrderInfo info : infos) { content.append("\n 订单:").append(info.getOrderId()).append("\r 下单:").append(info.getOrderDate()).append("\r"); } } } contents.add(content); } // 订单查询 if (order.startsWith("搜索")) { order = order.replace("搜索", ""); content = new StringBuilder(); // 精准查询订单号+精准查询sku+模糊查询收件人+模糊查询地址 content.append("精准查询订单号:\r"); List orderRowList = orderRowRepository.findByOrderId(Long.parseLong(order)); if (!orderRowList.isEmpty()) { OrderRow orderRow = orderRowList.get(0); if (orderRow.getUnionId().equals(Long.parseLong(unionId))) { content.append(orderUtil.getFormattedOrderInfo(orderRow, orderRow.getValidCode())); } else { content.append("订单不属于你,无法查询\r"); } } else { content.append("订单不存在\r"); } contents.add(content); content = new StringBuilder(); // 不统计已取消的订单 content.append("精准查询sku,不统计已取消的订单:\r"); int[] validCodes = {-1, 3}; List bySkuIdAndUnionId = orderRowRepository.findBySkuIdAndUnionId(validCodes, Long.parseLong(order), Long.parseLong(unionId)); int size = bySkuIdAndUnionId.size(); content.append("查询到").append(size).append("条订单\r"); // 切割成20条20条返回前100条 for (int i = 0; i < size; i += 20) { List subList = bySkuIdAndUnionId.subList(i, Math.min(i + 20, size)); content.append("第").append(i / 20 + 1).append("页:\r"); for (OrderRow orderRow : subList) { content.append(orderUtil.getFormattedOrderInfo(orderRow, orderRow.getValidCode())); contents.add(content); content = new StringBuilder(); } } content = new StringBuilder(); content.append("模糊查询收件人+模糊查询地址:\r"); //List orderRowList = orderRowRepository content.append("暂不支持"); contents.add(content); } if (order.startsWith("SKU")) { content = new StringBuilder(); order = order.replace("SKU", ""); String[] split = order.split("\r\n"); content.append("电脑端").append("\r\n"); for (String s : split) { content.append("https://item.jd.com/").append(s.trim()).append(".html").append("\r\n"); } wxUtil.sendTextMessage(fromWxid, content.toString(), 1, fromWxid, false); content = new StringBuilder(); content.append("手机端").append("\r\n"); for (String s : split) { content.append("https://item.m.jd.com/product/").append(s.trim()).append(".html").append("\r\n"); } wxUtil.sendTextMessage(fromWxid, content.toString(), 1, fromWxid, false); content = new StringBuilder(); contents.add(content); } // 转链 if (order.startsWith("转链")) { content = new StringBuilder(); order = order.replace("转链", ""); String jsonString; try { GoodsQueryResult goodsQueryResult = queryProductInfoByUJDUrl(order); jsonString = JSON.toJSONString(goodsQueryResult); content.append(jsonString); } catch (Exception e) { content.append(e); throw new RuntimeException(e); } contents.add(content); } } else { try { sendOrderToWxByOrderJD("菜单", fromWxid); } catch (Exception e) { throw new RuntimeException(e); } } if (!contents.isEmpty()) { for (StringBuilder stringBuilder : contents) { wxUtil.sendTextMessage(fromWxid, stringBuilder.toString(), 1, fromWxid, false); } } } /** * 接口描述:通过商品链接、领券链接、活动链接获取普通推广链接或优惠券二合一推广链接 * jd.union.open.promotion.bysubunionid.get */ String transfer(String url, String giftCouponKey) { JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, LPF_APP_KEY_WZ, LPF_SECRET_KEY_WZ); UnionOpenPromotionBysubunionidGetRequest request = new UnionOpenPromotionBysubunionidGetRequest(); PromotionCodeReq promotionCodeReq = new PromotionCodeReq(); promotionCodeReq.setSceneId(1); promotionCodeReq.setMaterialId(url); if (giftCouponKey != null) { promotionCodeReq.setGiftCouponKey(giftCouponKey); } request.setPromotionCodeReq(promotionCodeReq); request.setVersion("1.0"); //logger.info("transfer - request {}", JSON.toJSONString(request)); UnionOpenPromotionBysubunionidGetResponse response; try { response = client.execute(request); //logger.info("transfer - response {}", JSON.toJSONString(response)); } catch (Exception e) { throw new RuntimeException(e); } /** * { * "jd_union_open_promotion_bysubunionid_get_responce": { * "getResult": { * "code": "200", * "data": { * "clickURL": "https://union-click.jd.com/jdc?e=XXXXXX p=XXXXXXXXXXX", * "weChatShortLink": "#小程序://京小街/****", * "jShortCommand": "短口令", * "shortURL": "https://u.jd.com/XXXXX", * "jCommand": "6.0复制整段话 http://JhT7V5wlKygHDK京口令内容#J6UFE5iMn***" * }, * "message": "success" * } * } * } * */ String result = ""; if (Util.isNotEmpty(response)) { if (response.getCode().equals("0") && response.getGetResult().getCode() == 200) { result = response.getGetResult().getData().getShortURL(); } } else { result = null; } return result; } //public UnionOpenGoodsBigfieldQueryResponse getUnionOpenGoodsBigfieldQueryResponse(){ // JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, APP_KEY, SECRET_KEY); // // UnionOpenGoodsBigfieldQueryRequest request=new UnionOpenGoodsBigfieldQueryRequest(); // BigFieldGoodsReq goodsReq=new BigFieldGoodsReq(); // goodsReq.setSkuIds(); // request.setGoodsReq(goodsReq); // request.setVersion("1.0"); // UnionOpenGoodsBigfieldQueryResponse response= null; // try { // response = client.execute(request); // } catch (Exception e) { // throw new RuntimeException(e); // } // return response; //} /** * 消毒柜部分的业务逻辑 */ @Scheduled(fixedRate = 60000) // 每分钟执行一次 public void cleanUpTimeoutStates() { LocalDateTime now = LocalDateTime.now(); redisTemplate.keys(INTERACTION_STATE_PREFIX + "*").forEach(key -> { String stateJson = redisTemplate.opsForValue().get(key); try { UserInteractionState state = objectMapper.readValue(stateJson, UserInteractionState.class); LocalDateTime lastInteractionTime = LocalDateTime.parse(state.getLastInteractionTime(), DATE_TIME_FORMATTER); if (ChronoUnit.MINUTES.between(lastInteractionTime, now) > TIMEOUT_MINUTES) { redisTemplate.delete(key); logger.debug("Deleted timeout state for key: {}", key); } } catch (Exception e) { logger.error("Error parsing interaction state: {}", e.getMessage()); } }); } private void handleUserInteraction(String fromWxid, String message) { String key = INTERACTION_STATE_PREFIX + fromWxid; UserInteractionState state = loadOrCreateState(key); try { switch (state.getCurrentState()) { case INIT: handleInitState(fromWxid, message, state); break; case PRODUCT_PROMOTION: handlePromotionState(fromWxid, message, state); break; case GIFT_MONEY_FLOW: processGiftMoneyFlow(fromWxid, message, state); break; default: resetState(fromWxid, state); wxUtil.sendTextMessage(fromWxid, "流程异常,请重新开始", 1, fromWxid, false); } } catch (Exception e) { logger.error("流程处理异常 - 用户: {}, 状态: {}", fromWxid, state, e); resetState(fromWxid, state); wxUtil.sendTextMessage(fromWxid, "处理异常,请重试", 1, fromWxid, false); } finally { saveState(key, state); } userInteractionStates.put(key, state); logger.debug("Updated interaction state for user {}: {}", fromWxid, JSON.toJSONString(state)); } private UserInteractionState loadOrCreateState(String key) { UserInteractionState state = loadState(key); if (state == null) { state = new UserInteractionState(); logger.debug("创建新交互状态: {}", key); } else if (isStateExpired(state)) { redisTemplate.delete(key); state = new UserInteractionState(); logger.debug("状态已过期,重置: {}", key); } return state; } private boolean isStateExpired(UserInteractionState state) { LocalDateTime lastTime = LocalDateTime.parse(state.getLastInteractionTime(), DATE_TIME_FORMATTER); return ChronoUnit.MINUTES.between(lastTime, LocalDateTime.now()) > TIMEOUT_MINUTES; } private void handleInitState(String wxid, String message, UserInteractionState state) { if ("转".equals(message)) { state.setCurrentState(UserInteractionState.ProcessState.PRODUCT_PROMOTION); state.setCurrentField("content"); wxUtil.sendTextMessage(wxid, "请输入推广方案(包含商品链接):", 1, wxid, false); logger.info("进入转链流程 - 用户: {}", wxid); } } private void handlePromotionState(String wxid, String message, UserInteractionState state) { if ("content".equals(state.getCurrentField())) { processContentInput(wxid, message, state); } else if ("confirm".equals(state.getCurrentField())) { handleGiftMoneyConfirmation(wxid, message, state); } } /** * 处理用户输入的推广方案内容 * * @param wxid 用户微信ID * @param message 用户输入的方案内容 * @param state 当前交互状态 */ private void processContentInput(String wxid, String message, UserInteractionState state) { try { // 1. 清除旧缓存 cacheMap.remove("productData" + wxid); cacheMap.remove("finalWenAn" + wxid); // 2. 生成推广内容 HashMap> contentResult = generatePromotionContent(message); // 3. 发送文本内容 for (String text : contentResult.get("text")) { wxUtil.sendTextMessage(wxid, text, 1, wxid, true); } // 4. 发送图片(如果有) List images = contentResult.get("images"); if (images != null) { try { Thread.sleep(1000); } catch (InterruptedException ignored) { } for (String imageUrl : images) { if (imageUrl != null) { wxUtil.sendImageMessage(wxid, imageUrl); } } } // 5. 缓存商品数据 if (!contentResult.get("data").isEmpty()) { String productData = contentResult.get("data").get(0); cacheMap.put("productData" + wxid, productData); cacheMap.put("finalWenAn" + wxid, contentResult.get("finalWenAn").get(0)); // 6. 进入确认礼金步骤 state.setCurrentField("confirm"); try { Thread.sleep(5000); } catch (InterruptedException ignored) { } wxUtil.sendTextMessage(wxid, "是否需要开通礼金?\n回复 1 - 是\n回复 2 - 否", 1, wxid, false); } else { wxUtil.sendTextMessage(wxid, "未获取到商品数据,请检查链接格式", 1, wxid, false); state.reset(); } } catch (Exception e) { logger.error("处理推广内容异常 - 用户: {}", wxid, e); wxUtil.sendTextMessage(wxid, "处理内容时发生异常,请重试", 1, wxid, false); state.reset(); } } /** * 处理用户对礼金开通的确认 * * @param wxid 用户微信ID * @param message 用户回复(1或2) * @param state 当前交互状态 */ private void handleGiftMoneyConfirmation(String wxid, String message, UserInteractionState state) { try { if ("1".equals(message)) { // 用户选择开通礼金 state.setCurrentState(UserInteractionState.ProcessState.GIFT_MONEY_FLOW); state.setCurrentStep(UserInteractionState.GiftMoneyStep.STEP_AMOUNT); state.getCollectedFields().clear(); wxUtil.sendTextMessage(wxid, "请输入开通金额(1-50元,支持小数点后两位):\n" + "示例:20.50", 1, wxid, false); } else if ("2".equals(message)) { // 用户选择不开通礼金 String cachedData = cacheMap.get("productData" + wxid); if (cachedData != null) { JSONObject productInfo = JSON.parseObject(cachedData); String skuUrl = productInfo.getString("materialUrl"); String transferUrl = transfer(skuUrl, null); String finalWenAn = cacheMap.get("finalWenAn" + wxid); if (finalWenAn != null) { finalWenAn = finalWenAn.replace(productInfo.getString("url"), transferUrl); wxUtil.sendTextMessage(wxid, "不开礼金,只转链的方案:\n", 1, wxid, false); wxUtil.sendTextMessage(wxid, finalWenAn, 1, wxid, true); } else { wxUtil.sendTextMessage(wxid, "未找到商品信息,请重新开始流程", 1, wxid, false); } } else { wxUtil.sendTextMessage(wxid, "未找到商品信息,请重新开始流程", 1, wxid, false); } // 清理状态和缓存 state.reset(); cacheMap.remove("productData" + wxid); cacheMap.remove("finalWenAn" + wxid); } else { // 无效输入 wxUtil.sendTextMessage(wxid, "请输入有效选项:\n" + "回复 1 - 开通礼金\n" + "回复 2 - 直接转链", 1, wxid, false); } } catch (Exception e) { logger.error("处理礼金确认异常 - 用户: {}", wxid, e); wxUtil.sendTextMessage(wxid, "处理请求时发生异常,请重试", 1, wxid, false); state.reset(); } } private void resetState(String wxid, UserInteractionState state) { state.reset(); saveState(INTERACTION_STATE_PREFIX + wxid, state); } /** * 生成转链和方案的方法 * * @param message 方案内容,包含商品链接 * @return 处理后的方案,附带商品信息 */ public HashMap> generatePromotionContent(String message) { HashMap> finalMessage = new HashMap<>(); List textList = new ArrayList<>(); List imagesList = new ArrayList<>(); List dataList = new ArrayList<>(); List finalWenAn = new ArrayList<>(); // 提取方案中的所有 u.jd.com 链接 List urls = extractUJDUrls(message); if (urls.isEmpty()) { textList.add("方案中未找到有效的商品链接,请检查格式是否正确。"); finalMessage.put("text", textList); return finalMessage; } for (String url : urls) { try { // 查询商品信息 GoodsQueryResult productInfo = queryProductInfoByUJDUrl(url); if (productInfo == null || productInfo.getCode() != 200) { textList.add("链接查询失败:" + url); continue; } long totalCount = productInfo.getTotalCount(); if (totalCount == 0) { // 优惠券链接 textList.add("链接类型:优惠券\n" + url); } else { // 商品链接 JSONObject jsonObject = JSONObject.parseObject(JSONObject.toJSONString(productInfo.getData()[0])); jsonObject.put("url", url); dataList.add(jsonObject.toJSONString()); HashMap itemMap = new HashMap<>(); itemMap.put("url", url); itemMap.put("materialUrl", productInfo.getData()[0].getMaterialUrl()); itemMap.put("oriItemId", productInfo.getData()[0].getOriItemId()); itemMap.put("owner", productInfo.getData()[0].getOwner()); itemMap.put("shopId", String.valueOf(productInfo.getData()[0].getShopInfo().getShopId())); itemMap.put("shopName", productInfo.getData()[0].getShopInfo().getShopName()); String skuName = productInfo.getData()[0].getSkuName(); String cleanedSkuName = skuName.replaceAll("国家", "").replaceAll("补贴", "").replaceAll("15%", "").replaceAll("20%", ""); itemMap.put("skuName", cleanedSkuName); itemMap.put("spuid", String.valueOf(productInfo.getData()[0].getSpuid())); itemMap.put("commission", String.valueOf(productInfo.getData()[0].getCommissionInfo().getCommission())); itemMap.put("commissionShare", String.valueOf(productInfo.getData()[0].getCommissionInfo().getCommissionShare())); StringBuilder couponInfo = new StringBuilder(); couponInfo.append("店铺:").append(itemMap.get("shopName")).append("\n") .append("标题:").append(cleanedSkuName).append("\n") .append("自营 POP:").append(itemMap.get("owner").equals("g") ? " 自营 " : " POP ").append("\n") .append("佣金:").append(itemMap.get("commission")).append("\n") .append("佣金比例:").append(itemMap.get("commissionShare")); textList.add(couponInfo.toString()); if (productInfo.getData()[0].getImageInfo() != null) { for (UrlInfo image : productInfo.getData()[0].getImageInfo().getImageList()) { imagesList.add(image.getUrl()); } } // 生成闲鱼的商品文案 DateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日HH时mm分ss秒"); textList.add("(教你买) " + cleanedSkuName + "\n" + WENAN_FANAN.replaceAll("更新", dateFormat.format(new Date()) + "更新")); textList.add(cleanedSkuName + "\n" + WENAN_ZCXS); } } catch (Exception e) { logger.error("处理商品链接时发生异常:{}", url, e); textList.add("处理商品链接时发生异常:" + url); } } // 生成最终文案 StringBuilder wenan = new StringBuilder(FANAN_COMMON); for (String url : urls) { wenan.append(message.replace(url, "")); } finalWenAn.add(wenan.toString()); finalMessage.put("text", textList); finalMessage.put("images", imagesList); finalMessage.put("data", dataList); finalMessage.put("finalWenAn", finalWenAn); return finalMessage; } /** * 提取方案中的所有 u.jd.com 链接 * * @param message 方案内容 * @return 包含所有 u.jd.com 链接的列表 */ private List extractUJDUrls(String message) { List urls = new ArrayList<>(); Pattern pattern = Pattern.compile("https://u\\.jd\\.com/\\S+"); Matcher matcher = pattern.matcher(message); while (matcher.find()) { urls.add(matcher.group()); } return urls; } /** * 通过这个可以将u.jd.*** 查询到对应的商品信息 和 商品图片,甚至可以查询到是不是自营的商品 */ public UnionOpenGoodsQueryResponse getUnionOpenGoodsQueryRequest(String uJDUrl) throws Exception { JdClient client = new DefaultJdClient(SERVER_URL, ACCESS_TOKEN, LPF_APP_KEY_WZ, LPF_SECRET_KEY_WZ); UnionOpenGoodsQueryRequest request = new UnionOpenGoodsQueryRequest(); GoodsReq goodsReq = new GoodsReq(); //goodsReq.setSkuIds(List.of(Long.parseLong(skuId)).toArray(new Long[0])); // 传入skuId集合 /** * 查询域集合,不填写则查询全部,目目前支持:categoryInfo(类目信息),imageInfo(图片信息), * baseBigFieldInfo(基础大字段信息),bookBigFieldInfo(图书大字段信息), * videoBigFieldInfo(影音大字段信息),detailImages(商详图) * */ //goodsReq.setFields(new String[]{"categoryInfo", "imageInfo", "baseBigFieldInfo"}); // 设置需要查询的字段 goodsReq.setKeyword(uJDUrl); goodsReq.setSceneId(1); request.setGoodsReqDTO(goodsReq); request.setVersion("1.0"); request.setSignmethod("md5"); // 时间戳,格式为yyyy-MM-dd HH:mm:ss,时区为GMT+8。API服务端允许客户端请求最大时间误差为10分钟 Date date = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); request.setTimestamp(simpleDateFormat.format(date)); //logger.debug("getUnionOpenGoodsQueryRequest 查询商品信息请求:{}", JSON.toJSONString(request)); UnionOpenGoodsQueryResponse execute = client.execute(request); //logger.debug("getUnionOpenGoodsQueryRequest 查询商品信息响应:{}", JSON.toJSONString(execute)); return execute; } public GoodsQueryResult queryProductInfoByUJDUrl(String uJDUrl) throws Exception { UnionOpenGoodsQueryResponse response = getUnionOpenGoodsQueryRequest(uJDUrl); if (response == null || response.getQueryResult() == null) { return null; } GoodsQueryResult queryResult = response.getQueryResult(); if (queryResult.getCode() != 200) { return null; } return queryResult; } // 将状态存储迁移到Redis public void saveState(String wxid, UserInteractionState state) { String key = INTERACTION_STATE_PREFIX + wxid; redisTemplate.opsForValue().set(key, JSON.toJSONString(state), 10, TimeUnit.MINUTES); } public UserInteractionState loadState(String wxid) { String json = redisTemplate.opsForValue().get(INTERACTION_STATE_PREFIX + wxid); return JSON.parseObject(json, UserInteractionState.class); } // 定义一个内部类来存储用户交互状态 @Getter @Setter static class UserInteractionState { private GiftMoneyStep currentStep; // 新增当前步骤字段 private String lastInteractionTime; private ProcessState currentState; private Map collectedFields; // 用于存储收集到的字段值 private String currentField; // 当前正在询问的字段 public UserInteractionState() { this.currentStep = GiftMoneyStep.STEP_CONFIRM_GIFT; // 初始 this.lastInteractionTime = LocalDateTime.now().format(DATE_TIME_FORMATTER); this.currentState = INIT; this.collectedFields = new HashMap<>(); this.currentField = null; } public void updateLastInteractionTime() { this.lastInteractionTime = LocalDateTime.now().format(DATE_TIME_FORMATTER); } public void reset() { this.currentState = INIT; this.collectedFields.clear(); this.currentStep = STEP_CONFIRM_GIFT; // 明确重置步骤 updateLastInteractionTime(); } // 推荐使用枚举管理状态 public enum ProcessState { INIT, GIFT_MONEY_FLOW, PRODUCT_PROMOTION } @Getter public enum GiftMoneyStep { STEP_WAIT_FOR_CONTENT(3), STEP_CONFIRM_GIFT(0), // 显式指定序号 STEP_AMOUNT(1), STEP_QUANTITY(2); private final int code; GiftMoneyStep(int code) { this.code = code; } } } // 限流异常类(需自定义) public static class RateLimitExceededException extends RuntimeException { public RateLimitExceededException(String message) { super(message); } } @Setter @Getter public static class OrderInfo { private String skuName; private Long count; private Long orderId; private Date orderDate; } // 统计指标DTO @Getter @AllArgsConstructor class OrderStats { private long totalOrders; // 总订单数 private long validOrders; // 有效订单数(不含取消) private long paidOrders; // 已付款订单 private double paidCommission; // 已付款佣金 private long pendingOrders; // 待付款订单 private double pendingCommission; // 待付款佣金 private long canceledOrders; // 已取消订单 private long completedOrders; // 已完成订单 private double completedCommission;// 已完成佣金 private long violations; // 违规订单数 private double violationCommission;// 违规佣金 } }