From 498a47977ee44ecbea3a0b78633aff82ed58a9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Fri, 10 Apr 2026 02:16:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(notification):=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=80=81=E6=B0=B4=E8=AE=A2=E5=8D=95=E6=96=B0=E5=8D=95=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E9=85=8D=E9=80=81=E5=91=98=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在订单创建成功后异步通知所有在线配送员有新订单信息 - 查询在线且启用状态的配送员列表,并发送微信订阅消息提醒 - 新增 GltSubscribeMessageService 接口及实现,封装微信小程序订阅消息发送逻辑 - 实现新订单和订单状态变更的微信订阅消息发送方法 - 配置Redis缓存access_token,提升微信接口调用效率 - 日志记录订阅消息发送状态及异常,确保通知稳定性 - ShopDealerUser实体新增分销商等级字段 - ShopGoods实体支持活动方式和配送方式字段增加相关查询条件 - 更新相关Mapper XML文件增加对dealerLevel、activityType及deliveryMode字段的支持 - 修改application-glt2.yml更新Redis host配置 --- .../controller/GltTicketOrderController.java | 80 +++++- .../service/GltSubscribeMessageService.java | 28 ++ .../impl/GltSubscribeMessageServiceImpl.java | 240 ++++++++++++++++++ .../gxwebsoft/shop/entity/ShopDealerUser.java | 3 + .../com/gxwebsoft/shop/entity/ShopGoods.java | 6 + .../shop/mapper/xml/ShopDealerUserMapper.xml | 3 + .../shop/mapper/xml/ShopGoodsMapper.xml | 6 + .../gxwebsoft/shop/param/ShopGoodsParam.java | 8 + src/main/resources/application-glt2.yml | 2 +- 9 files changed, 373 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/gxwebsoft/glt/service/GltSubscribeMessageService.java create mode 100644 src/main/java/com/gxwebsoft/glt/service/impl/GltSubscribeMessageServiceImpl.java diff --git a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java index 7fa519e..09ab3e8 100644 --- a/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java +++ b/src/main/java/com/gxwebsoft/glt/controller/GltTicketOrderController.java @@ -1,15 +1,18 @@ package com.gxwebsoft.glt.controller; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.gxwebsoft.common.core.annotation.OperationLog; +import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.core.web.ApiResult; import com.gxwebsoft.common.core.web.BaseController; import com.gxwebsoft.common.core.web.BatchParam; import com.gxwebsoft.common.core.web.PageResult; -import com.gxwebsoft.common.core.exception.BusinessException; import com.gxwebsoft.common.system.entity.User; +import com.gxwebsoft.common.system.mapper.UserMapper; import com.gxwebsoft.glt.entity.GltTicketOrder; import com.gxwebsoft.glt.param.GltTicketOrderDeliveredParam; import com.gxwebsoft.glt.param.GltTicketOrderParam; +import com.gxwebsoft.glt.service.GltSubscribeMessageService; import com.gxwebsoft.glt.service.GltTicketOrderService; import com.gxwebsoft.shop.entity.ShopStoreRider; import com.gxwebsoft.shop.entity.ShopUserAddress; @@ -19,12 +22,13 @@ import com.gxwebsoft.shop.service.ShopUserAddressService; import cn.hutool.core.util.StrUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; +import java.util.stream.Collectors; /** * 送水订单控制器 @@ -32,6 +36,7 @@ import java.util.List; * @author 科技小王子 * @since 2026-02-05 18:50:21 */ +@Slf4j @Tag(name = "送水订单管理") @RestController @RequestMapping("/api/glt/glt-ticket-order") @@ -44,6 +49,10 @@ public class GltTicketOrderController extends BaseController { private ShopStoreFenceService shopStoreFenceService; @Resource private ShopStoreRiderService shopStoreRiderService; + @Resource + private GltSubscribeMessageService gltSubscribeMessageService; + @Resource + private UserMapper userMapper; @Operation(summary = "分页查询送水订单") @GetMapping("/page") @@ -167,9 +176,76 @@ public class GltTicketOrderController extends BaseController { } gltTicketOrderService.createWithWriteOff(gltTicketOrder, loginUser.getUserId(), loginUser.getTenantId()); + + // 订单创建成功后,异步通知所有在线配送员有新订单 + try { + notifyRidersOfNewOrder(gltTicketOrder, loginUser.getTenantId()); + } catch (Exception e) { + log.warn("通知配送员失败(不影响下单): {}", e.getMessage()); + } + return success("下单成功"); } + /** + * 通知所有在线配送员有新订单 + */ + private void notifyRidersOfNewOrder(GltTicketOrder order, Integer tenantId) { + if (order == null || tenantId == null) { + return; + } + + // 查询所有启用且在线的配送员 + List onlineRiders = shopStoreRiderService.list( + new LambdaQueryWrapper() + .eq(ShopStoreRider::getTenantId, tenantId) + .eq(ShopStoreRider::getIsDelete, 0) + .eq(ShopStoreRider::getStatus, 1) + .eq(ShopStoreRider::getWorkStatus, 1) // 在线状态 + .or() + .eq(ShopStoreRider::getTenantId, tenantId) + .eq(ShopStoreRider::getIsDelete, 0) + .eq(ShopStoreRider::getStatus, 1) + .isNull(ShopStoreRider::getWorkStatus) // 兼容未设置状态的配送员 + ); + + if (onlineRiders == null || onlineRiders.isEmpty()) { + log.info("当前无在线配送员,无需发送订阅消息"); + return; + } + + // 获取配送员的 userId 列表 + List riderUserIds = onlineRiders.stream() + .map(ShopStoreRider::getUserId) + .filter(id -> id != null && id > 0) + .collect(Collectors.toList()); + + if (riderUserIds.isEmpty()) { + return; + } + + // 批量查询配送员的 openId + List riders = userMapper.selectList( + new LambdaQueryWrapper() + .select(User::getUserId, User::getOpenid) + .in(User::getUserId, riderUserIds) + .isNotNull(User::getOpenid) + ); + + // 发送订阅消息 + for (User rider : riders) { + if (StrUtil.isNotBlank(rider.getOpenid())) { + try { + gltSubscribeMessageService.sendNewOrderNotice(order, rider.getOpenid(), tenantId); + } catch (Exception e) { + log.warn("发送订阅消息给配送员失败 - userId={}, error={}", rider.getUserId(), e.getMessage()); + } + } + } + + log.info("已向 {} 位配送员发送新订单通知", riders.size()); + } + @PreAuthorize("isAuthenticated()") @Operation(summary = "配送员接单") @PostMapping("/{id}/accept") diff --git a/src/main/java/com/gxwebsoft/glt/service/GltSubscribeMessageService.java b/src/main/java/com/gxwebsoft/glt/service/GltSubscribeMessageService.java new file mode 100644 index 0000000..1183754 --- /dev/null +++ b/src/main/java/com/gxwebsoft/glt/service/GltSubscribeMessageService.java @@ -0,0 +1,28 @@ +package com.gxwebsoft.glt.service; + +import com.gxwebsoft.glt.entity.GltTicketOrder; + +/** + * 微信订阅消息服务接口 + */ +public interface GltSubscribeMessageService { + + /** + * 发送新订单通知给配送员 + * @param order 订单信息 + * @param riderOpenId 配送员微信openId + * @param tenantId 租户ID + * @return 是否发送成功 + */ + boolean sendNewOrderNotice(GltTicketOrder order, String riderOpenId, Integer tenantId); + + /** + * 发送订单状态变更通知 + * @param order 订单信息 + * @param riderOpenId 配送员微信openId + * @param statusText 状态描述 + * @param tenantId 租户ID + * @return 是否发送成功 + */ + boolean sendOrderStatusNotice(GltTicketOrder order, String riderOpenId, String statusText, Integer tenantId); +} diff --git a/src/main/java/com/gxwebsoft/glt/service/impl/GltSubscribeMessageServiceImpl.java b/src/main/java/com/gxwebsoft/glt/service/impl/GltSubscribeMessageServiceImpl.java new file mode 100644 index 0000000..76e8314 --- /dev/null +++ b/src/main/java/com/gxwebsoft/glt/service/impl/GltSubscribeMessageServiceImpl.java @@ -0,0 +1,240 @@ +package com.gxwebsoft.glt.service.impl; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.gxwebsoft.common.core.exception.BusinessException; +import com.gxwebsoft.common.core.utils.RedisUtil; +import com.gxwebsoft.glt.entity.GltTicketOrder; +import com.gxwebsoft.glt.service.GltSubscribeMessageService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +import static com.gxwebsoft.common.core.constants.RedisConstants.*; + +/** + * 微信订阅消息服务实现 + * + *

功能: + *

    + *
  • 新订单通知配送员
  • + *
  • 订单状态变更通知
  • + *
+ *

+ */ +@Slf4j +@Service +public class GltSubscribeMessageServiceImpl implements GltSubscribeMessageService { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 订阅消息模板ID(需在微信公众平台配置) + * 模板名称:订单配送通知 + * 关键词:订单编号、订单内容、配送地址、订单金额 + */ + private static final String SUBSCRIBE_TEMPLATE_ID = "YOUR_TEMPLATE_ID"; // TODO: 替换为实际模板ID + + /** + * 发送新订单通知给配送员 + */ + @Override + public boolean sendNewOrderNotice(GltTicketOrder order, String riderOpenId, Integer tenantId) { + if (order == null || StrUtil.isBlank(riderOpenId) || tenantId == null) { + log.warn("发送订阅消息参数不完整"); + return false; + } + + try { + String accessToken = getAccessToken(tenantId); + if (StrUtil.isBlank(accessToken)) { + log.warn("获取access_token失败"); + return false; + } + + // 构建消息内容 + Map data = new HashMap<>(); + data.put("phrase1", Map.of("value", "待配送")); // 订单状态 + data.put("character_string2", Map.of("value", String.valueOf(order.getId()))); // 订单编号 + data.put("thing3", Map.of("value", truncateStr(order.getAddress(), 20))); // 配送地址 + data.put("number4", Map.of("value", String.valueOf(order.getTotalNum()))); // 商品数量 + data.put("time5", Map.of("value", formatTime(order.getSendTime()))); // 期望送达时间 + + // 发送订阅消息 + return sendSubscribeMessage(accessToken, riderOpenId, data); + } catch (Exception e) { + log.error("发送新订单订阅消息失败 - orderId={}, riderOpenId={}, error={}", + order.getId(), riderOpenId, e.getMessage(), e); + return false; + } + } + + /** + * 发送订单状态变更通知 + */ + @Override + public boolean sendOrderStatusNotice(GltTicketOrder order, String riderOpenId, String statusText, Integer tenantId) { + if (order == null || StrUtil.isBlank(riderOpenId) || tenantId == null) { + log.warn("发送订阅消息参数不完整"); + return false; + } + + try { + String accessToken = getAccessToken(tenantId); + if (StrUtil.isBlank(accessToken)) { + log.warn("获取access_token失败"); + return false; + } + + // 构建消息内容 + Map data = new HashMap<>(); + data.put("phrase1", Map.of("value", truncateStr(statusText, 5))); // 状态描述 + data.put("character_string2", Map.of("value", String.valueOf(order.getId()))); // 订单编号 + data.put("time3", Map.of("value", formatTime(null))); // 通知时间 + + // 发送订阅消息 + return sendSubscribeMessage(accessToken, riderOpenId, data); + } catch (Exception e) { + log.error("发送订单状态变更订阅消息失败 - orderId={}, riderOpenId={}, error={}", + order.getId(), riderOpenId, e.getMessage(), e); + return false; + } + } + + /** + * 获取小程序的 access_token + */ + private String getAccessToken(Integer tenantId) { + if (tenantId == null) { + throw new BusinessException("tenantId 不能为空"); + } + + final String tokenCacheKey = ACCESS_TOKEN_KEY + ":" + tenantId; + + // 1) 优先从缓存取 + String cachedValue = stringRedisTemplate.opsForValue().get(tokenCacheKey); + if (StrUtil.isNotBlank(cachedValue)) { + try { + JSONObject cachedJson = JSON.parseObject(cachedValue); + String accessToken = cachedJson.getString("access_token"); + if (StrUtil.isNotBlank(accessToken)) { + return accessToken; + } + } catch (Exception ignore) { + // 旧格式:直接存 token + return cachedValue; + } + } + + // 2) 缓存没有则从租户配置获取 appId/appSecret + final String wxConfigKey = MP_WX_KEY + tenantId; + final String wxConfigValue = stringRedisTemplate.opsForValue().get(wxConfigKey); + if (StrUtil.isBlank(wxConfigValue)) { + log.warn("未找到微信小程序配置,请检查缓存key: {}", wxConfigKey); + return null; + } + + JSONObject wxConfig; + try { + wxConfig = JSON.parseObject(wxConfigValue); + } catch (Exception e) { + log.error("微信小程序配置格式错误: {}", e.getMessage()); + return null; + } + + final String appId = wxConfig.getString("appId"); + final String appSecret = wxConfig.getString("appSecret"); + if (StrUtil.isBlank(appId) || StrUtil.isBlank(appSecret)) { + log.error("微信小程序配置不完整(appId/appSecret)"); + return null; + } + + // 3) 调用微信接口获取 token + final String apiUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + + "&appid=" + appId + "&secret=" + appSecret; + String result = HttpUtil.get(apiUrl); + + JSONObject json = JSON.parseObject(result); + if (json.containsKey("errcode") && json.getIntValue("errcode") != 0) { + log.error("获取小程序access_token失败: {}", json.getString("errmsg")); + return null; + } + + String accessToken = json.getString("access_token"); + Integer expiresIn = json.getInteger("expires_in"); + if (StrUtil.isBlank(accessToken)) { + log.error("获取小程序access_token失败: access_token为空"); + return null; + } + + // 4) 缓存,提前5分钟过期 + long ttlSeconds = 7000L; + if (expiresIn != null && expiresIn > 300) { + ttlSeconds = expiresIn - 300L; + } + stringRedisTemplate.opsForValue().set(tokenCacheKey, result, ttlSeconds, java.util.concurrent.TimeUnit.SECONDS); + + log.info("获取小程序access_token成功 - tenantId={}, ttlSeconds={}", tenantId, ttlSeconds); + return accessToken; + } + + /** + * 发送订阅消息 + */ + private boolean sendSubscribeMessage(String accessToken, String openId, Map data) { + String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" + accessToken; + + Map params = new HashMap<>(); + params.put("touser", openId); // 用户 openid + params.put("template_id", SUBSCRIBE_TEMPLATE_ID); // 模板ID + params.put("page", "pages/rider/orders/index"); // 点击后跳转的页面 + params.put("data", data); + + String response = HttpUtil.createPost(url) + .contentType("application/json") + .body(JSON.toJSONString(params)) + .timeout(10000) + .execute() + .body(); + + JSONObject result = JSON.parseObject(response); + int errcode = result.getIntValue("errcode"); + + if (errcode == 0) { + log.info("订阅消息发送成功 - openId={}", openId); + return true; + } else { + log.warn("订阅消息发送失败 - openId={}, errcode={}, errmsg={}", + openId, errcode, result.getString("errmsg")); + return false; + } + } + + /** + * 截断字符串 + */ + private String truncateStr(String str, int maxLen) { + if (str == null) return ""; + return str.length() > maxLen ? str.substring(0, maxLen) : str; + } + + /** + * 格式化时间 + */ + private String formatTime(String timeStr) { + if (StrUtil.isBlank(timeStr)) { + return cn.hutool.core.date.DateUtil.now(); + } + return timeStr; + } +} diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopDealerUser.java b/src/main/java/com/gxwebsoft/shop/entity/ShopDealerUser.java index a3530cc..b067c3f 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopDealerUser.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopDealerUser.java @@ -116,4 +116,7 @@ public class ShopDealerUser implements Serializable { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime; + @Schema(description = "分销商等级:0-普通用户 1-超级管理员 2-合伙人(总店) 3-合伙人(分店)") + private Integer dealerLevel; + } diff --git a/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java b/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java index 7d31385..75bc1a3 100644 --- a/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java +++ b/src/main/java/com/gxwebsoft/shop/entity/ShopGoods.java @@ -153,6 +153,12 @@ public class ShopGoods implements Serializable { @Schema(description = "状态, 0上架 1待上架 2待审核 3审核不通过") private Integer status; + @Schema(description = "活动方式: 0全平台 1新用户专享") + private Integer activityType; + + @Schema(description = "配送方式: 0送上门 1限自提") + private Integer deliveryMode; + @Schema(description = "备注") private String comments; diff --git a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopDealerUserMapper.xml b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopDealerUserMapper.xml index f103c11..d6e3c30 100644 --- a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopDealerUserMapper.xml +++ b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopDealerUserMapper.xml @@ -63,6 +63,9 @@ AND a.sort_number = #{param.sortNumber} + + AND a.dealer_level = #{param.dealerLevel} + AND (a.comments LIKE CONCAT('%', #{param.keywords}, '%') OR a.user_id = #{param.keywords} OR a.dealer_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.real_name LIKE CONCAT('%', #{param.keywords}, '%') OR a.mobile LIKE CONCAT('%', #{param.keywords}, '%') diff --git a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopGoodsMapper.xml b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopGoodsMapper.xml index a14c354..0f57c75 100644 --- a/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopGoodsMapper.xml +++ b/src/main/java/com/gxwebsoft/shop/mapper/xml/ShopGoodsMapper.xml @@ -135,6 +135,12 @@ OR a.comments LIKE CONCAT('%', #{param.keywords}, '%') ) + + AND a.activity_type = #{param.activityType} + + + AND a.delivery_mode = #{param.deliveryMode} + diff --git a/src/main/java/com/gxwebsoft/shop/param/ShopGoodsParam.java b/src/main/java/com/gxwebsoft/shop/param/ShopGoodsParam.java index 0138743..a1f1880 100644 --- a/src/main/java/com/gxwebsoft/shop/param/ShopGoodsParam.java +++ b/src/main/java/com/gxwebsoft/shop/param/ShopGoodsParam.java @@ -150,4 +150,12 @@ public class ShopGoodsParam extends BaseParam { @QueryField(type = QueryType.EQ) private Integer deleted; + @Schema(description = "活动方式: 0全平台 1新用户专享") + @QueryField(type = QueryType.EQ) + private Integer activityType; + + @Schema(description = "配送方式: 0送上门 1限自提") + @QueryField(type = QueryType.EQ) + private Integer deliveryMode; + } diff --git a/src/main/resources/application-glt2.yml b/src/main/resources/application-glt2.yml index fdcd6ba..4a44e6b 100644 --- a/src/main/resources/application-glt2.yml +++ b/src/main/resources/application-glt2.yml @@ -16,7 +16,7 @@ spring: # redis redis: database: 0 - host: 8.134.55.105 + host: 47.107.249.41 port: 16379 password: redis_t74P8C