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