From f8172b49d6a30995ce7959d692ef7fd352be033d Mon Sep 17 00:00:00 2001 From: Jack <46790855@qq.com> Date: Sat, 6 Sep 2025 00:24:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=BE=E5=BC=83=20=E6=AD=BB=E4=BF=A1?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E9=98=9F=E5=88=97=EF=BC=8C=E9=87=87=E7=94=A8?= =?UTF-8?q?=20redis=20=E8=BF=87=E6=9C=9F=E4=BA=8B=E4=BB=B6=E7=9B=91?= =?UTF-8?q?=E5=90=AC=EF=BC=8C=E8=B6=85=E6=97=B6=E8=AE=A2=E5=8D=95=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mall/common/constant/RedisConstant.java | 4 + .../suisung/mall/shop/components/IpUtil.java | 52 +++++- .../suisung/mall/shop/config/RedisConfig.java | 34 ++++ .../service/impl/LakalaApiServiceImpl.java | 7 +- .../order/listener/DelayMessageReceiver.java | 3 +- .../order/listener/OrderPayedListener.java | 131 ++++++++----- .../listener/RedisKeyExpiredListener.java | 126 +++++++++++++ .../store/service/ShopMchEntryService.java | 2 +- .../service/ShopStoreAnalyticsService.java | 8 + .../service/ShopStoreCompanyService.java | 8 + .../service/ShopStoreEmployeeService.java | 11 ++ .../service/impl/ShopMchEntryServiceImpl.java | 151 +++++++-------- .../impl/ShopStoreAnalyticsServiceImpl.java | 16 ++ .../impl/ShopStoreBaseServiceImpl.java | 173 +++++++++++------- .../impl/ShopStoreCompanyServiceImpl.java | 14 ++ .../impl/ShopStoreEmployeeServiceImpl.java | 27 +++ 16 files changed, 569 insertions(+), 198 deletions(-) create mode 100644 mall-shop/src/main/java/com/suisung/mall/shop/order/listener/RedisKeyExpiredListener.java diff --git a/mall-common/src/main/java/com/suisung/mall/common/constant/RedisConstant.java b/mall-common/src/main/java/com/suisung/mall/common/constant/RedisConstant.java index 780791b6..c3f0336c 100644 --- a/mall-common/src/main/java/com/suisung/mall/common/constant/RedisConstant.java +++ b/mall-common/src/main/java/com/suisung/mall/common/constant/RedisConstant.java @@ -31,4 +31,8 @@ public class RedisConstant { public static final String Product_Cate_Key = ConstantRedis.Cache_NameSpace + "product_cate_Key"; public static final String Store_Brand_Key = ConstantRedis.Cache_NameSpace + "store_brand_key"; + + public static final String SF_Order_Proc_Expire_Key = ConstantRedis.Cache_NameSpace + "sf_order_proc_expire_key__"; + + public static final String Order_Pay_Retry_Count_Key = ConstantRedis.Cache_NameSpace + "order_pay_retry_count:"; } diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/components/IpUtil.java b/mall-shop/src/main/java/com/suisung/mall/shop/components/IpUtil.java index 985273c3..98537fec 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/components/IpUtil.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/components/IpUtil.java @@ -30,6 +30,12 @@ public class IpUtil implements ApplicationRunner { * ip2region搜索器实例 */ private static Searcher searcher = null; + + /** + * 数据文件加载状态 + */ + private static boolean dataFileLoaded = false; + private static String dataFileInfo = "未加载"; /** * 根据IP地址获取地区信息 @@ -39,18 +45,23 @@ public class IpUtil implements ApplicationRunner { */ public static String getRegion(String ip) { if (Objects.isNull(searcher)) { - log.error("IP2RegionUtils 没有成功加载数据文件"); + log.error("IP2RegionUtils 没有成功加载数据文件. 数据文件状态: {}, 文件信息: {}", + dataFileLoaded ? "已加载但searcher为null" : "未加载", dataFileInfo); return null; } if (CheckUtil.isEmpty(ip)) { + log.debug("传入的IP地址为空"); return null; } try { - return searcher.search(ip); + String result = searcher.search(ip); + log.debug("IP地址 {} 解析成功,结果: {}", ip, result); + return result; } catch (ArrayIndexOutOfBoundsException e) { - log.error("IP:{} 解析时发生数组越界异常,可能是数据文件损坏或不兼容", ip, e); + log.error("IP:{} 解析时发生数组越界异常,可能是数据文件损坏或不兼容. 数据文件状态: {}, 文件信息: {}", + ip, dataFileLoaded ? "已加载" : "未加载", dataFileInfo, e); return null; } catch (Exception e) { String errorMsg = e.getMessage(); @@ -60,7 +71,8 @@ public class IpUtil implements ApplicationRunner { errorMsg += " -> " + e.getCause().getMessage(); } } - log.error("IP:{} 格式错误:{}", ip, errorMsg, e); + log.error("IP:{} 格式错误:{}. 数据文件状态: {}, 文件信息: {}", + ip, errorMsg, dataFileLoaded ? "已加载" : "未加载", dataFileInfo, e); return null; } } @@ -97,17 +109,24 @@ public class IpUtil implements ApplicationRunner { districtVo.setCountry(parts[0] != null ? parts[0] : ""); // 设置国家 districtVo.setProvince(parts[2] != null ? parts[2] : ""); // 设置省份 districtVo.setCity(parts[3] != null ? parts[3] : ""); // 设置城市 + + log.debug("IP地址 {} 解析为地区信息成功. 国家: {}, 省份: {}, 城市: {}", + ip, districtVo.getCountry(), districtVo.getProvince(), districtVo.getCity()); } else { log.debug("IP:{} 解析结果格式不符合要求,原始数据: {}", ip, ss); return null; } } catch (ArrayIndexOutOfBoundsException e) { - log.error("解析IP地址 {} 的地区信息时发生数组越界异常,原始数据: {}", ip, ss, e); + log.error("解析IP地址 {} 的地区信息时发生数组越界异常,原始数据: {}. 数据文件状态: {}, 文件信息: {}", + ip, ss, dataFileLoaded ? "已加载" : "未加载", dataFileInfo, e); return null; } catch (Exception e) { - log.error("解析IP地址 {} 的地区信息时发生异常: {}", ip, e.getMessage(), e); + log.error("解析IP地址 {} 的地区信息时发生异常: {}. 数据文件状态: {}, 文件信息: {}", + ip, e.getMessage(), dataFileLoaded ? "已加载" : "未加载", dataFileInfo, e); return null; } + } else { + log.debug("IP地址 {} 未解析到地区信息", ip); } return districtVo; @@ -137,16 +156,29 @@ public class IpUtil implements ApplicationRunner { buffer.flush(); byte[] bytes = buffer.toByteArray(); + // 记录数据文件信息 + dataFileInfo = String.format("文件大小: %d 字节", bytes.length); + log.info("读取到 ip2region 数据文件,{}", dataFileInfo); + inputStream.close(); searcher = Searcher.newWithBuffer(bytes); - log.info("成功加载 ip2region 数据文件。"); + dataFileLoaded = true; + + // 测试搜索器 + if (searcher != null) { + String testResult = searcher.search("8.8.8.8"); + log.info("成功加载 ip2region 数据文件。测试搜索 8.8.8.8 结果: {}", testResult); + } else { + log.error("加载 ip2region 数据文件失败,searcher 实例为 null"); + } } catch (ArrayIndexOutOfBoundsException e) { - log.error("解析IP地址的地区信息时发生数组越界异常,原始数据: {}", e); + dataFileLoaded = true; + log.error("解析IP地址的地区信息时发生数组越界异常。数据文件状态: 已加载, 文件信息: {}", dataFileInfo, e); } catch (IOException e) { - log.error("加载 ip2region 失败。", e); + log.error("加载 ip2region 失败。数据文件状态: 未加载, 文件信息: {}", dataFileInfo, e); } catch (Exception e) { - log.error("初始化 ip2region 搜索器失败。", e); + log.error("初始化 ip2region 搜索器失败。数据文件状态: 未加载, 文件信息: {}", dataFileInfo, e); } } } \ No newline at end of file diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/config/RedisConfig.java b/mall-shop/src/main/java/com/suisung/mall/shop/config/RedisConfig.java index fb2ef130..6bbeeeed 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/config/RedisConfig.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/config/RedisConfig.java @@ -1,12 +1,46 @@ package com.suisung.mall.shop.config; import com.suisung.mall.core.config.BaseRedisConfig; +import com.suisung.mall.shop.order.listener.RedisKeyExpiredListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; /** * Redis相关配置 */ +@Slf4j @Configuration public class RedisConfig extends BaseRedisConfig { + @Bean + RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory, + MessageListenerAdapter keyExpiredListener) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.addMessageListener(keyExpiredListener, new PatternTopic("__keyevent@*__:expired")); + return container; + } + @Bean + MessageListenerAdapter keyExpiredListener() { + return new MessageListenerAdapter(new RedisKeyExpiredListener()); + } + +// /** +// * 监听Redis键过期事件 +// */ +// public static class KeyExpiredListener implements MessageListener { +// @Override +// public void onMessage(org.springframework.data.redis.connection.Message message, +// byte[] pattern) { +// String expiredKey = new String(message.getBody()); +// // 在这里处理过期键的逻辑 +// log.info("Redis 过期事件监听 Key expired: {}", expiredKey); +// // 您可以在这里添加自己的业务逻辑,比如清理相关资源、发送通知等 +// } +// } } diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/lakala/service/impl/LakalaApiServiceImpl.java b/mall-shop/src/main/java/com/suisung/mall/shop/lakala/service/impl/LakalaApiServiceImpl.java index 8ee47b43..7c3bf751 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/lakala/service/impl/LakalaApiServiceImpl.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/lakala/service/impl/LakalaApiServiceImpl.java @@ -1582,7 +1582,7 @@ public class LakalaApiServiceImpl implements LakalaApiService { /** - * 拉卡拉订单分账,用户下单成功之后(大约15秒后),进行分账 + * 拉卡拉订单分账,用户确认收货成功之后(大约15秒后),进行分账 * 说明:分账指令是异步处理模式,响应报文成功时,指令状态是”status”: “PROCESSING”,需要等待分账结果通知,或者主动发起查询,建议主动发起查询与分账指令动作之间间隔15秒以上。 * 参考:https://o.lakala.com/#/home/document/detail?id=389 * @@ -1649,10 +1649,10 @@ public class LakalaApiServiceImpl implements LakalaApiService { // continue; // } - // 分账总金额,单位(分) + // 分账总金额(支付金额-运费)运费不参与分账,单位(分) Integer splitAmount = totalAmt - shoppingFee; if (splitAmount <= 0) { - String errorMsg = String.format("店铺[%s]订单分账金额异常,跳过分账", shopOrderLkl.getStore_id()); + String errorMsg = String.format("店铺[%s]订单%s分账金额异常,跳过分账", shopOrderLkl.getStore_id(), orderId); log.error(errorMsg); errorMessages.append(errorMsg).append("; "); lklOrderSeparateService.updateRemark(lklOrderSeparateExist.getId(), errorMsg); @@ -1667,7 +1667,6 @@ public class LakalaApiServiceImpl implements LakalaApiService { String errorMsg = String.format("店铺[%s]未绑定平台方接收账户,跳过分账", shopOrderLkl.getStore_id()); log.error(errorMsg); errorMessages.append(errorMsg).append("; "); -// lklOrderSeparateService.updateRemark(lklOrderSeparateExist.getId(), errorMsg); continue; } diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/DelayMessageReceiver.java b/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/DelayMessageReceiver.java index aa677045..5bbc5b58 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/DelayMessageReceiver.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/DelayMessageReceiver.java @@ -116,7 +116,8 @@ public class DelayMessageReceiver { // 根据消息分类处理不同类型的消息 if (MqConstant.DEAD_EVENT_CATE_ORDER_EXPIRED.equals(category)) { log.info("处理订单超时消息: {}", message); - return handleOrderExpiredMessage(message); + // return handleOrderExpiredMessage(message); + return true; } else if (MqConstant.DEAD_EVENT_CATE_PRE_ORDER.equals(category)) { log.info("处理预订单消息: {}", message); return handlePreOrderMessage(message); diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/OrderPayedListener.java b/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/OrderPayedListener.java index 24cc9c4a..28db9ee8 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/OrderPayedListener.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/OrderPayedListener.java @@ -7,9 +7,10 @@ import com.rabbitmq.client.Channel; import com.suisung.mall.common.api.StateCode; import com.suisung.mall.common.constant.CommonConstant; import com.suisung.mall.common.constant.MqConstant; +import com.suisung.mall.common.constant.RedisConstant; import com.suisung.mall.common.modules.order.ShopOrderInfo; import com.suisung.mall.common.utils.DateTimeUtils; -import com.suisung.mall.shop.message.service.MqMessageService; +import com.suisung.mall.core.web.service.RedisService; import com.suisung.mall.shop.message.service.PushMessageService; import com.suisung.mall.shop.order.service.ShopOrderBaseService; import com.suisung.mall.shop.order.service.ShopOrderInfoService; @@ -41,6 +42,9 @@ public class OrderPayedListener { private static final Logger logger = LoggerFactory.getLogger(OrderPayedListener.class); + // 最大重试次数 + private static final int MAX_RETRY_COUNT = 5; + @Lazy @Autowired private ShopOrderBaseService shopOrderBaseService; @@ -56,7 +60,7 @@ public class OrderPayedListener { @Lazy @Autowired - private MqMessageService mqMessageService; + private RedisService redisService; @RabbitHandler public void listener(byte[] data, Channel channel, Message message) throws IOException, InterruptedException { @@ -69,53 +73,72 @@ public class OrderPayedListener { public void listener(String data, Channel channel, Message message) throws IOException, InterruptedException { // String messageId = message.getMessageProperties().getMessageId(); if (StrUtil.isBlank(data)) { - logger.info("收到空订单消息"); + logger.info("[订单支付监听] 收到空订单消息"); + channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } List order_id_row = Convert.toList(String.class, data); - try { - boolean flag = false; - logger.info("收到微信异步通知消息data:{}-chanel:{}-message:{},订单ID:{}", data, channel, message, order_id_row); + long deliveryTag = message.getMessageProperties().getDeliveryTag(); + boolean isRedelivered = message.getMessageProperties().isRedelivered(); + String retryCountKey = RedisConstant.Order_Pay_Retry_Count_Key + order_id_row.toString(); + + try { + logger.info("[订单支付监听] 收到微信异步通知消息. 数据: {}, 订单ID: {}, 是否重投: {}", data, order_id_row, isRedelivered); + + // 检查重试次数 + int retryCount = getRetryCount(retryCountKey); + + if (retryCount >= MAX_RETRY_COUNT) { + logger.error("[订单支付监听] 订单处理已达到最大重试次数: {}, 订单ID: {}, 不再处理该消息", MAX_RETRY_COUNT, order_id_row); + channel.basicAck(deliveryTag, false); // 确认消息,避免无限重试 + redisService.del(retryCountKey); // 清除重试计数 + return; + } + + boolean flag = false; for (String orderId : order_id_row) { //判断是否为线下支付订单 ShopOrderInfo orderInfoOld = shopOrderInfoService.get(orderId); if (orderInfoOld == null) { + // 记录重试次数 + incrementRetryCount(retryCountKey); + logger.warn("[订单支付监听] 订单在数据库中不存在,增加重试计数. 订单ID: {}, 当前重试次数: {}", orderId, retryCount + 1); continue; } // 订单状态 int order_state_id = orderInfoOld.getOrder_state_id().intValue(); - logger.info("#### 当前订单状态: {} ####", order_state_id); + logger.info("[订单支付监听] 当前订单状态: {}, 订单ID: {}", order_state_id, orderId); // 检查订单是否已经处理过(幂等性检查) -// if (isOrderPaid(orderInfoOld)) { -// logger.info("订单已支付,无需重复处理,订单ID: {}", orderId); -// flag = true; -// continue; -// } - + if (isOrderPaid(orderInfoOld)) { + logger.info("[订单支付监听] 订单已支付,无需重复处理,订单ID: {}", orderId); + flag = true; + continue; + } + if (order_state_id == StateCode.ORDER_STATE_WAIT_PAY) { // 待支付状态 - logger.info("#### 待支付业务分支 ####"); + logger.info("[订单支付监听] 处理待支付订单. 订单ID: {}", orderId); flag = shopOrderBaseService.setPaidYes(Collections.singletonList(orderId)); } else { //判断是否线下支付 if (StateCode.PAYMENT_TYPE_OFFLINE == orderInfoOld.getPayment_type_id().intValue()) { //线下支付,直接处理订单支付状态, 不处理订单状态 - logger.info("#### 线下业务分支 ####"); + logger.info("[订单支付监听] 处理线下支付订单. 订单ID: {}", orderId); ShopOrderInfo orderInfo = new ShopOrderInfo(); orderInfo.setOrder_id(orderId); orderInfo.setOrder_is_paid(StateCode.ORDER_PAID_STATE_YES); flag = shopOrderInfoService.edit(orderInfo); } else { - logger.info("#### 非线下业务分支 ####"); + logger.info("[订单支付监听] 处理其他支付订单. 订单ID: {}", orderId); flag = shopOrderBaseService.setPaidYes(Collections.singletonList(orderId)); } } - logger.info("#### 支付异步通知回调处理是否成功:{} ####", flag); + logger.info("[订单支付监听] 支付异步通知回调处理结果: {}, 订单ID: {}", flag, orderId); // 生成取单号和打印小票 if (flag) { // TODO 以下仅处理下单打印的情况,还需要处理退单打印分支。 @@ -134,16 +157,16 @@ public class OrderPayedListener { // 发送顺丰同城快递 Pair pairCreateSfOrder = sfExpressApiService.innerCreateSfExpressOrder(orderId, orderPickupNum); if (pairCreateSfOrder == null) { - logger.error("顺丰同城下单失败!pairCreateSfOrder 返回空值"); - return; + logger.error("[订单支付监听] 顺丰同城下单失败!pairCreateSfOrder 返回空值. 订单ID: {}", orderId); + continue; } if (!pairCreateSfOrder.getFirst()) { - logger.error("顺丰同城下单失败:{}", pairCreateSfOrder.getSecond()); - return; + logger.error("[订单支付监听] 顺丰同城下单失败: {}, 订单ID: {}", pairCreateSfOrder.getSecond(), orderId); + continue; } - logger.info("顺丰同城下单成功"); + logger.info("[订单支付监听] 顺丰同城下单成功. 订单ID: {}", orderId); } @@ -158,18 +181,12 @@ public class OrderPayedListener { payload.put("orderId", orderId); pushMessageService.noticeMerchantEmployeeOrderAction(orderInfoOld.getStore_id(), orderId, title, content, payload); - // 发送 预过期 MQ 的推送消息 - JSONObject jsonObject = new JSONObject(); - jsonObject.put("category", MqConstant.DEAD_EVENT_CATE_ORDER_EXPIRED); // 消息分类:1-订单超时消息 - jsonObject.put("orderId", orderId); // 订单ID - jsonObject.put("storeId", orderInfoOld.getStore_id()); // 店铺ID - jsonObject.put("title", "您有一笔将超时的订单。"); // 消息标题 - jsonObject.put("message", String.format("您有一笔将超时的订单:%s,请您及时处理。", orderId)); // 消息内容 - // 发送延迟消息(提前10分钟) - Long mchOrderExpireSeconds = shopOrderBaseService.sameCityOrderExpireSeconds(1500L) - 600; - mqMessageService.sendDelayMessage(jsonObject.toString(), mchOrderExpireSeconds * 1000); // 转换为毫秒 + // 发送延迟消息 25分钟拣货配送时间(提前5分钟,下单20分钟后,提醒商家及时拣货) + Long mchOrderExpireSeconds = shopOrderBaseService.sameCityOrderExpireSeconds(1500L) - 300; + redisService.set(RedisConstant.SF_Order_Proc_Expire_Key + String.format("%d&%s", orderInfoOld.getStore_id(), orderId), orderId, mchOrderExpireSeconds); + } catch (Exception e) { - log.error("发送推送消息失败:{}", e.getMessage()); + log.error("[订单支付监听] 发送推送消息失败. 订单ID: {}", orderId, e); } } } @@ -177,15 +194,22 @@ public class OrderPayedListener { if (flag) { // 操作成功,标记消息消费成功 - channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); + logger.info("[订单支付监听] 消息处理成功,确认消息. 订单ID: {}", order_id_row); + channel.basicAck(deliveryTag, false); + // 清除重试计数 + redisService.del(RedisConstant.Order_Pay_Retry_Count_Key + order_id_row); } else { - log.error("消息消费失败,执行setPaidYes异常,当前订单编号:{}", order_id_row); - channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); + log.error("[订单支付监听] 消息处理失败,执行setPaidYes异常,当前订单编号:{}", order_id_row); + // 增加重试计数 + incrementRetryCount(retryCountKey); + channel.basicReject(deliveryTag, true); Thread.sleep(1000); } } catch (Exception e) { - log.error("消息消费失败,执行setPaidYes异常,当前订单编号:{},失败原因:", order_id_row, e); - channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); + log.error("[订单支付监听] 消息处理异常,当前订单编号:{},异常信息:", order_id_row, e); + // 增加重试计数 + incrementRetryCount(retryCountKey); + channel.basicReject(deliveryTag, true); Thread.sleep(1000); } } @@ -197,12 +221,33 @@ public class OrderPayedListener { * @return 是否已支付 */ private boolean isOrderPaid(ShopOrderInfo orderInfo) { -// public static final int ORDER_PAID_STATE_NO = 3010; //未付款 -// public static final int ORDER_PAID_STATE_FINANCE_REVIEW = 3011; //待付款审核 -// public static final int ORDER_PAID_STATE_PART = 3012; //部分付款 -// public static final int ORDER_PAID_STATE_YES = 3013; //已付款 +// ORDER_PAID_STATE_NO = 3010; //未付款 +// ORDER_PAID_STATE_FINANCE_REVIEW = 3011; //待付款审核 +// ORDER_PAID_STATE_PART = 3012; //部分付款 +// ORDER_PAID_STATE_YES = 3013; //已付款 return orderInfo.getOrder_is_paid() != null && - orderInfo.getOrder_is_paid().equals(StateCode.ORDER_PAID_STATE_YES); + orderInfo.getOrder_is_paid().intValue() == StateCode.ORDER_PAID_STATE_YES; + } + + /** + * 获取重试次数 + * + * @param retryCountKey 重试计数键 + * @return 当前重试次数 + */ + private int getRetryCount(String retryCountKey) { + Object countObj = redisService.get(retryCountKey); + return countObj == null ? 0 : Convert.toInt(countObj, 0); + } + + /** + * 增加重试次数 + * + * @param retryCountKey 重试计数键 + */ + private void incrementRetryCount(String retryCountKey) { + int currentCount = getRetryCount(retryCountKey); + redisService.set(retryCountKey, currentCount + 1, 3600); // 1小时过期 } } diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/RedisKeyExpiredListener.java b/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/RedisKeyExpiredListener.java new file mode 100644 index 00000000..b580bf9f --- /dev/null +++ b/mall-shop/src/main/java/com/suisung/mall/shop/order/listener/RedisKeyExpiredListener.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2025. Lorem ipsum dolor sit amet, consectetur adipiscing elit. + * Morbi non lorem porttitor neque feugiat blandit. Ut vitae ipsum eget quam lacinia accumsan. + * Etiam sed turpis ac ipsum condimentum fringilla. Maecenas magna. + * Proin dapibus sapien vel ante. Aliquam erat volutpat. Pellentesque sagittis ligula eget metus. + * Vestibulum commodo. Ut rhoncus gravida arcu. + */ + +package com.suisung.mall.shop.order.listener; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import com.suisung.mall.common.constant.CommonConstant; +import com.suisung.mall.common.constant.RedisConstant; +import com.suisung.mall.shop.message.service.PushMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import javax.annotation.Resource; + +@Slf4j +public class RedisKeyExpiredListener implements MessageListener { + + @Lazy + @Resource + private PushMessageService pushMessageService; + + /** + * Callback for processing received objects through Redis. + * + * @param message message must not be {@literal null}. + * @param pattern pattern matching the channel (if specified) - can be {@literal null}. + */ + @Override + public void onMessage(Message message, byte[] pattern) { + String expiredKey = new String(message.getBody()); + String patternStr = pattern != null ? new String(pattern) : "无模式"; + + // 基本日志记录 + log.info("[Redis过期监听] 接收到Redis键过期事件. 过期键: {}, 订阅模式: {}", expiredKey, patternStr); + + // 参数验证 + if (StrUtil.isBlank(expiredKey)) { + log.warn("[Redis过期监听] 接收到空的过期键. 订阅模式: {}", patternStr); + return; + } + + if (expiredKey.startsWith(RedisConstant.SF_Order_Proc_Expire_Key)) { + log.debug("[Redis过期监听] 开始处理订单超时事件. 过期键: {}", expiredKey); + + // storeId&orderId + String[] args = expiredKey.replace(RedisConstant.SF_Order_Proc_Expire_Key, "").split("&"); + if (ArrayUtil.isEmpty(args) || args.length < 2) { + log.error("[Redis过期监听] 订单处理超时键格式错误. 键: {} 不符合预期格式", expiredKey); + return; + } + + log.info("[Redis过期监听] 处理订单超时消息. 店铺ID: {}, 订单号: {}", args[0], args[1]); + boolean result = handleOrderExpiredMessage(args[0], args[1]); + + if (result) { + log.info("[Redis过期监听] 订单超时事件处理成功. 店铺ID: {}, 订单号: {}", args[0], args[1]); + } else { + log.error("[Redis过期监听] 订单超时事件处理失败. 店铺ID: {}, 订单号: {}", args[0], args[1]); + } + } else { + log.debug("[Redis过期监听] 忽略非订单超时事件. 过期键: {}", expiredKey); + } + } + + /** + * 处理订单超时消息 + * + * @param storeId 店铺ID + * @param orderId 订单ID + * @return 处理结果 + */ + private boolean handleOrderExpiredMessage(String storeId, String orderId) { + try { + log.debug("[订单超时处理] 开始处理订单超时消息. 店铺ID: {}, 订单号: {}", storeId, orderId); + + // 参数验证 + if (StrUtil.isBlank(orderId) && StrUtil.isBlank(storeId)) { + log.warn("[订单超时处理] 订单ID和店铺ID不能同时为空. 店铺ID: {}, 订单号: {}", storeId, orderId); + return false; + } + + if (StrUtil.isBlank(orderId)) { + log.warn("[订单超时处理] 订单号为空. 店铺ID: {}", storeId); + return false; + } + + if (StrUtil.isBlank(storeId)) { + log.warn("[订单超时处理] 店铺ID为空. 订单号: {}", orderId); + return false; + } + + String title = "有一笔即将超时的订单!"; + String content = "您有一笔即将超时的订单[" + orderId + "],请及时处理。"; + log.debug("[订单超时处理] 准备推送消息. 标题: {}, 内容: {}", title, content); + + // 构造推送消息内容 + JSONObject payload = new JSONObject(); + payload.put("category", CommonConstant.PUSH_MSG_CATE_MCH_ABNORMAL_ORDER_LIST); + payload.put("orderId", orderId); + log.debug("[订单超时处理] 推送消息载荷已准备: {}", payload); + + // 发送推送消息给商家员工 + log.debug("[订单超时处理] 发送推送消息给商家员工. 店铺ID: {}, 订单号: {}", storeId, orderId); + pushMessageService.noticeMerchantEmployeeOrderAction( + Convert.toInt(storeId), orderId, title, + content, payload); + + log.info("[订单超时处理] 订单超时消息处理完成. 订单号: {}, 店铺ID: {}, 推送类别: {}", + orderId, storeId, CommonConstant.PUSH_MSG_CATE_MCH_ABNORMAL_ORDER_LIST); + return true; + } catch (Exception e) { + log.error("[订单超时处理] 处理订单超时消息时发生异常. 订单号: {}, 店铺ID: {}", orderId, storeId, e); + return false; + } + } +} diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopMchEntryService.java b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopMchEntryService.java index 5832aee4..97f7c6a6 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopMchEntryService.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopMchEntryService.java @@ -214,7 +214,7 @@ public interface ShopMchEntryService { * @param storeId * @return */ - Boolean updateMerchEntryStoreId(Long id, Integer storeId); + Boolean updateMerchEntryStoreStatus(Long id, Integer storeId); /** * 更新商户入驻信息的拉卡拉电子合同相关信息 diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreAnalyticsService.java b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreAnalyticsService.java index 2f1e940d..54306697 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreAnalyticsService.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreAnalyticsService.java @@ -17,6 +17,14 @@ import java.util.Map; */ public interface ShopStoreAnalyticsService extends IBaseService { + /** + * 根据店铺id查询店铺统计信息 + * + * @param storeId + * @return + */ + ShopStoreAnalytics getByStoreId(Integer storeId); + List getAnalytics(List store_id); boolean saveProductAnalyticsNum(Integer store_id); diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreCompanyService.java b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreCompanyService.java index 4f1bbb17..9ac016d9 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreCompanyService.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreCompanyService.java @@ -14,5 +14,13 @@ import com.suisung.mall.core.web.service.IBaseService; */ public interface ShopStoreCompanyService extends IBaseService { + /** + * 获取店铺公司信息 + * + * @param storeId + * @return + */ + ShopStoreCompany getCompany(Integer storeId); + boolean saveOrUpdateCompany(ShopStoreCompany shopStoreCompany); } diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreEmployeeService.java b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreEmployeeService.java index 22b36063..e6336e6a 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreEmployeeService.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/ShopStoreEmployeeService.java @@ -38,6 +38,17 @@ public interface ShopStoreEmployeeService extends IBaseService selectEmployeeByStoreId(Integer storeId, String rightsGroupName); + /** + * 根据店铺Id和员工Id获取员工信息 + * + * @param storeId + * @param userId + * @param isAdmin + * @return + */ + List selectEmployeeByCondition(Integer storeId, Integer userId, Boolean isAdmin); + + /** * 根据店铺Id获取店铺的所有员工的个推 CID 列表 * diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopMchEntryServiceImpl.java b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopMchEntryServiceImpl.java index 1e974e43..774b8669 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopMchEntryServiceImpl.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopMchEntryServiceImpl.java @@ -637,7 +637,6 @@ public class ShopMchEntryServiceImpl extends BaseServiceImpl retPair = shopStoreBaseService.covMerchEntryInfo2StoreInfo(mchId, false); - if (retPair.getFirst() <= 0) { - updateMerchEntryApprovalByMchId(mchId, null, "创建并初始化店铺失败:" + retPair.getSecond()); - log.error("进件成功,但初始化店铺失败: mchId={}, reason={}", mchId, retPair.getSecond()); - } else { - log.debug("进件成功,创建并初始化店铺成功!mchId={}", mchId); - } + checkAndFixMchStoreInfo(mchId); // 8. 检查商户绑定状态是否完成, 更改总的审核状态 checkMerchEntryFinished(mchId); @@ -737,6 +726,32 @@ public class ShopMchEntryServiceImpl extends BaseServiceImpl entryRecords = selectByMobile(loginMobile); + if (CollUtil.isEmpty(entryRecords)) { + log.debug("未找到商户入驻记录,手机号: {}", loginMobile); + return false; + } + + int fixCount = 0; + // 3. 遍历处理每个入驻记录 + for (ShopMchEntry entry : entryRecords) { + // 3.1 验证入驻记录有效性 + if (checkAndFixMchStoreInfo(entry.getId())) { + fixCount++; + } + } + + return fixCount > 0; + } + + public Boolean checkAndFixMchStoreInfo(Long mchId) { + // 1. 参数校验 + if (CheckUtil.isEmpty(mchId)) { + log.warn("商户入驻ID为空"); + return false; + } + try { // 2. 获取当前商户用户信息 UserDto currentUser = getCurrentUser(); @@ -745,80 +760,72 @@ public class ShopMchEntryServiceImpl extends BaseServiceImpl entryRecords = selectByMobile(loginMobile); - if (CollUtil.isEmpty(entryRecords)) { - log.debug("未找到商户入驻记录,手机号: {}", loginMobile); + // 4.3 获取用户基础信息 + AccountUserBase userBase = accountService.getUserBase(currentUser.getId()); + if (userBase == null) { + log.warn("商户用户基础信息不存在,用户ID: {}", currentUser.getId()); return false; } - int fixCount = 0; - // 4. 遍历处理每个入驻记录 - for (ShopMchEntry entry : entryRecords) { - // 4.1 验证入驻记录有效性 - if (entry == null || entry.getId() == null) { - log.warn("无效的入驻记录"); - continue; - } + // 3. 查询商户入驻记录 + ShopMchEntry shopMchEntry = get(mchId); + if (shopMchEntry == null) { + log.debug("未找到商户入驻记录 mchId {}", mchId); + return false; + } - // 4.2 检查入驻状态 - boolean isValidStatus = CommonConstant.Enable.equals(entry.getHas_ec_signed()) - && CommonConstant.Enable.equals(entry.getHas_apply_mer()) - && CommonConstant.Enable.equals(entry.getHas_apply_split()) - && CommonConstant.Enable.equals(entry.getHas_apply_receiver()) - && CommonConstant.Enable.equals(entry.getHas_bind_receiver()); + // 4.2 检查入驻状态 + boolean isValidStatus = CommonConstant.Enable.equals(shopMchEntry.getHas_ec_signed()) + && CommonConstant.Enable.equals(shopMchEntry.getHas_apply_mer()) + && CommonConstant.Enable.equals(shopMchEntry.getHas_apply_split()) + && CommonConstant.Enable.equals(shopMchEntry.getHas_apply_receiver()) + && CommonConstant.Enable.equals(shopMchEntry.getHas_bind_receiver()); - if (!isValidStatus) { - log.debug("入驻记录状态不满足修复条件,入驻ID: {}", entry.getId()); - continue; - } + if (isValidStatus) { + log.debug("入驻记录状态都通过了审核,进一步验证修复条件,入驻ID: {}", mchId); + } else { + log.debug("入驻记录状态个别未通过审核,接着检查验证修复条件,入驻ID: {}", mchId); + } - // 4.3 获取用户基础信息 - AccountUserBase userBase = accountService.getUserBase(currentUser.getId()); - if (userBase == null) { - log.warn("商户用户基础信息不存在,用户ID: {}", currentUser.getId()); - continue; - } - // 4.4 获取店铺员工信息 - ShopStoreEmployee storeEmployee = null; - if (StrUtil.isNotBlank(entry.getStore_id())) { - storeEmployee = shopStoreEmployeeService.getStoreEmployeeByUserId( - Convert.toInt(entry.getStore_id()), - currentUser.getId() - ); - } + // 4.4 获取店铺员工信息 + ShopStoreEmployee storeEmployee = null; + if (StrUtil.isNotBlank(shopMchEntry.getStore_id())) { + storeEmployee = shopStoreEmployeeService.getStoreEmployeeByUserId( + Convert.toInt(shopMchEntry.getStore_id()), + currentUser.getId() + ); + } - // 4.5 检查店铺关联关系 - boolean hasStoreRelation = StrUtil.isNotBlank(userBase.getStore_ids()) && - userBase.getStore_ids().contains(entry.getStore_id()); + // 4.5 检查店铺关联关系 + boolean hasStoreRelation = StrUtil.isNotBlank(userBase.getStore_ids()) && + userBase.getStore_ids().contains(shopMchEntry.getStore_id()); - // 4.6 检查权限组信息 - boolean hasPermission = storeEmployee != null && - StrUtil.isNotBlank(storeEmployee.getRights_group_id()); + // 4.6 检查权限组信息 + boolean hasRoles = storeEmployee != null && + StrUtil.isNotBlank(storeEmployee.getRights_group_id()); - // 4.7 当以下任一条件满足时,需要进行修复 - // - 无店铺员工信息 - // - 无权限组信息 - // - 无店铺关联 - if (storeEmployee == null || - !hasPermission || - !hasStoreRelation) { + // 4.7 当以下任一条件满足时,需要进行修复 + // - 无店铺员工信息 + // - 无权限组信息 + // - 无店铺关联 + if (storeEmployee == null || + !hasRoles || + !hasStoreRelation) { - // 4.8 补偿创建初始化店铺部分赋值 - Pair result = shopStoreBaseService.covMerchEntryInfo2StoreInfo(entry.getId(), false); - if (result != null && result.getFirst() > 0) { - fixCount++; - log.warn("商户信息修复成功,入驻ID: {}", entry.getId()); - } else { - log.error("商户信息修复失败,入驻ID: {}, 错误信息:{}", entry.getId(), result != null ? result.getSecond() : "未知错误"); - } + // 4.8 补偿创建初始化店铺部分赋值 + Pair result = shopStoreBaseService.covMerchEntryInfo2StoreInfo(mchId, false); + if (result != null && result.getFirst() > 0) { + log.warn("商户信息修复成功,入驻ID: {}", mchId); + } else { + log.error("商户信息修复失败,入驻ID: {}, 错误信息:{}", mchId, result != null ? result.getSecond() : "未知错误"); + return false; } } - return fixCount > 0; + return true; } catch (Exception e) { - log.error("商户信息修复异常,手机号: {}", loginMobile, e); + log.error("商户信息修复异常,mchId: {}", mchId, e); return false; } } @@ -1391,7 +1398,7 @@ public class ShopMchEntryServiceImpl extends BaseServiceImpl shopStoreEmployees = shopStoreEmployeeService.selectEmployeeByCondition(storeId, null, null); - QueryWrapper queryWrapper = new QueryWrapper<>(); - queryWrapper.eq("store_id", storeId).eq("user_id", userId).eq("employee_is_admin", CommonConstant.Enable); - if (shopStoreEmployeeService.count(queryWrapper) <= 0) { + ShopStoreEmployee shopStoreEmployee = new ShopStoreEmployee(); + shopStoreEmployee.setStore_id(storeId); + shopStoreEmployee.setUser_id(userId); + shopStoreEmployee.setRights_group_id(""); // 店铺管理员,店铺 + shopStoreEmployee.setEmployee_is_kefu(CommonConstant.Enable); + + if (CollUtil.isEmpty(shopStoreEmployees)) { // 添加管理员 // shop_store_employee 店铺员工,添加管理员 - ShopStoreEmployee shopStoreEmployee = new ShopStoreEmployee(); - shopStoreEmployee.setStore_id(storeId); - shopStoreEmployee.setUser_id(userId); - shopStoreEmployee.setRights_group_id(""); // 店铺管理员,店铺 shopStoreEmployee.setEmployee_is_admin(CommonConstant.Enable); - shopStoreEmployee.setEmployee_is_kefu(CommonConstant.Enable); - if (!shopStoreEmployeeService.save(shopStoreEmployee)) { - logger.error("生成店铺:新增店铺员工失败"); - if (allowThrown) { - throw new ApiException(I18nUtil._("新增店铺员工失败")); - } - return Pair.of(0, "新增店铺员工失败"); + } else { // 添加店员或管理员 + shopStoreEmployees = shopStoreEmployeeService.selectEmployeeByCondition(storeId, userId, null); + if (CollUtil.isEmpty(shopStoreEmployees)) { + shopStoreEmployee.setEmployee_is_admin(CommonConstant.Disable); + } else { + shopStoreEmployee.setEmployee_is_admin(shopStoreEmployees.get(0).getEmployee_is_admin()); } } + if (!shopStoreEmployeeService.save(shopStoreEmployee)) { + logger.error("生成店铺:新增店铺员工失败"); + if (allowThrown) { + throw new ApiException(I18nUtil._("新增店铺员工失败")); + } + return Pair.of(0, "新增店铺员工失败"); + } + // 生成店铺的太阳码 2025-03-31 if (StrUtil.isBlank(shopStoreBase.getWx_qrcode())) { Pair resp = wxQrCodeService.genUnlimitedWxQrCode("pagesub/index/store", "store_id=" + storeId); @@ -3331,10 +3341,10 @@ public class ShopStoreBaseServiceImpl extends BaseServiceImpl queryWrapper = new QueryWrapper(); + queryWrapper.eq("store_id", storeId).orderByAsc("company_id"); + return findOne(queryWrapper); + } + @Override public boolean saveOrUpdateCompany(ShopStoreCompany shopStoreCompany) { diff --git a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopStoreEmployeeServiceImpl.java b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopStoreEmployeeServiceImpl.java index 4ac0df00..60d1696d 100644 --- a/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopStoreEmployeeServiceImpl.java +++ b/mall-shop/src/main/java/com/suisung/mall/shop/store/service/impl/ShopStoreEmployeeServiceImpl.java @@ -243,6 +243,33 @@ public class ShopStoreEmployeeServiceImpl extends BaseServiceImpl selectEmployeeByCondition(Integer storeId, Integer userId, Boolean isAdmin) { + if (CheckUtil.isEmpty(storeId)) { + return null; + } + + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("store_id", storeId); + if (CheckUtil.isNotEmpty(userId)) { + queryWrapper.eq("user_id", userId); + } + + if (isAdmin != null) { + queryWrapper.eq("employee_is_admin", isAdmin); + } + + return list(queryWrapper); + } + /** * 根据店铺Id获取店铺的所有员工的个推 CID 列表 *