Showing
8 changed files
with
657 additions
and
28 deletions
| ... | @@ -83,4 +83,9 @@ public interface ConfigConstants { | ... | @@ -83,4 +83,9 @@ public interface ConfigConstants { |
| 83 | String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}"; | 83 | String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}"; |
| 84 | String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}"; | 84 | String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}"; |
| 85 | 85 | ||
| 86 | + // 公众号配置(用于订阅消息推送) | ||
| 87 | + String OFFICIAL_ACCOUNT_APP_ID = "${officialAccount.appId}"; | ||
| 88 | + String OFFICIAL_ACCOUNT_APP_SECRET = "${officialAccount.appSecret}"; | ||
| 89 | + String OFFICIAL_ACCOUNT_ORDER_REMINDER_TEMPLATE_ID = "${officialAccount.orderReminderTemplateId}"; | ||
| 90 | + | ||
| 86 | } | 91 | } | ... | ... |
| 1 | +package com.infoloop.tianting.logic.task; | ||
| 2 | + | ||
| 3 | +import com.infoloop.tianting.deliveryruleservice.Rule; | ||
| 4 | +import com.infoloop.tianting.mealorderservice.PendingOrderSubscriptionMessageRpcResponse; | ||
| 5 | +import com.infoloop.tianting.mealorderservice.SingleMpAccountRpcResponse; | ||
| 6 | +import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse; | ||
| 7 | +import com.infoloop.tianting.service.OrderReminderService; | ||
| 8 | +import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient; | ||
| 9 | +import com.infoloop.tianting.service.client.MealOrderServiceRpcClient; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | +import org.apache.commons.collections4.CollectionUtils; | ||
| 12 | +import org.springframework.beans.factory.annotation.Qualifier; | ||
| 13 | +import org.springframework.scheduling.annotation.Scheduled; | ||
| 14 | +import org.springframework.stereotype.Component; | ||
| 15 | + | ||
| 16 | +import java.time.DayOfWeek; | ||
| 17 | +import java.time.LocalDate; | ||
| 18 | +import java.util.ArrayList; | ||
| 19 | +import java.util.Collections; | ||
| 20 | +import java.util.List; | ||
| 21 | +import java.util.Map; | ||
| 22 | +import java.util.Set; | ||
| 23 | +import java.util.concurrent.CompletableFuture; | ||
| 24 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 25 | +import java.util.concurrent.Executor; | ||
| 26 | +import java.util.concurrent.atomic.AtomicInteger; | ||
| 27 | +import java.util.stream.Collectors; | ||
| 28 | + | ||
| 29 | +import static com.infoloop.tianting.constant.ConfigConstants.ASYNC_EXECUTOR; | ||
| 30 | + | ||
| 31 | +/** | ||
| 32 | + * 订餐提醒定时任务 | ||
| 33 | + * 在订餐周期开启前(周一 9:00)向已授权的家长发送微信服务通知 | ||
| 34 | + * | ||
| 35 | + * 推送逻辑: | ||
| 36 | + * 1. 查询所有待发送的订阅消息(未删除的记录) | ||
| 37 | + * 2. 按 openId 去重,每个用户只推送一次 | ||
| 38 | + * 3. 通过 openId 查询活跃的小程序用户 | ||
| 39 | + * 4. 根据配送规则配置生成订餐时间提示 | ||
| 40 | + * 5. 发送消息,发送成功后标记为已删除 | ||
| 41 | + */ | ||
| 42 | +@Slf4j | ||
| 43 | +@Component | ||
| 44 | +public class OrderReminderTask { | ||
| 45 | + | ||
| 46 | + private static final int ENTERPRISE_ID = 449; | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * 温馨提醒内容 | ||
| 50 | + */ | ||
| 51 | + private static final String TIPS = "请点击卡片,准时进入小程序完成预订"; | ||
| 52 | + | ||
| 53 | + /** | ||
| 54 | + * 订餐时间配置 | ||
| 55 | + */ | ||
| 56 | + private record OrderTimeConfig(String startTime, String endTime) {} | ||
| 57 | + | ||
| 58 | + private final MealOrderServiceRpcClient mealOrderServiceRpcClient; | ||
| 59 | + private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient; | ||
| 60 | + private final OrderReminderService orderReminderService; | ||
| 61 | + private final Executor asyncExecutor; | ||
| 62 | + | ||
| 63 | + public OrderReminderTask( | ||
| 64 | + MealOrderServiceRpcClient mealOrderServiceRpcClient, | ||
| 65 | + DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient, | ||
| 66 | + OrderReminderService orderReminderService, | ||
| 67 | + @Qualifier(ASYNC_EXECUTOR) Executor asyncExecutor) { | ||
| 68 | + this.mealOrderServiceRpcClient = mealOrderServiceRpcClient; | ||
| 69 | + this.deliveryRuleServiceRpcClient = deliveryRuleServiceRpcClient; | ||
| 70 | + this.orderReminderService = orderReminderService; | ||
| 71 | + this.asyncExecutor = asyncExecutor; | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 每周一 9:00 执行订餐提醒推送 | ||
| 76 | + */ | ||
| 77 | + @Scheduled(cron = "0 0 9 * * MON") | ||
| 78 | + public void executeTask() { | ||
| 79 | + log.info("OrderReminderTask started"); | ||
| 80 | + | ||
| 81 | + try { | ||
| 82 | + // 1. 获取配送规则配置,用于生成订餐时间提示 | ||
| 83 | + OrderTimeConfig orderTimeConfig = getOrderTimeConfigFromRule(); | ||
| 84 | + if (orderTimeConfig == null) { | ||
| 85 | + log.warn("Failed to get order time config from rule, using default"); | ||
| 86 | + orderTimeConfig = new OrderTimeConfig("请查看小程序", "请查看小程序"); | ||
| 87 | + } | ||
| 88 | + log.info("Order time config: startTime={}, endTime={}", orderTimeConfig.startTime(), orderTimeConfig.endTime()); | ||
| 89 | + | ||
| 90 | + // 2. 查询所有待发送的订阅消息(未删除的记录) | ||
| 91 | + List<PendingOrderSubscriptionMessageRpcResponse> pendingMessages = | ||
| 92 | + mealOrderServiceRpcClient.queryPendingOrderSubscriptionMessages(ENTERPRISE_ID); | ||
| 93 | + | ||
| 94 | + if (pendingMessages.isEmpty()) { | ||
| 95 | + log.info("No pending order subscription messages to send"); | ||
| 96 | + return; | ||
| 97 | + } | ||
| 98 | + log.info("Found {} pending order subscription messages", pendingMessages.size()); | ||
| 99 | + | ||
| 100 | + // 3. 提取所有 openId,查询活跃的小程序用户 | ||
| 101 | + Set<String> openIds = pendingMessages.stream() | ||
| 102 | + .map(PendingOrderSubscriptionMessageRpcResponse::getOpenId) | ||
| 103 | + .collect(Collectors.toSet()); | ||
| 104 | + | ||
| 105 | + Map<String, SingleMpAccountRpcResponse> activeMpAccountMap = getActiveMpAccountMap(openIds); | ||
| 106 | + log.info("Found {} active mp accounts from {} openIds", activeMpAccountMap.size(), openIds.size()); | ||
| 107 | + | ||
| 108 | + int totalMessages = pendingMessages.size(); | ||
| 109 | + AtomicInteger skippedInactive = new AtomicInteger(0); | ||
| 110 | + AtomicInteger skippedDuplicate = new AtomicInteger(0); | ||
| 111 | + AtomicInteger sentCount = new AtomicInteger(0); | ||
| 112 | + List<Long> sentMessageIds = Collections.synchronizedList(new ArrayList<>()); | ||
| 113 | + Set<String> sentOpenIds = ConcurrentHashMap.newKeySet(); // 线程安全的 Set,用于 openId 去重 | ||
| 114 | + | ||
| 115 | + // 4. 按 openId 去重,筛选出需要发送的消息 | ||
| 116 | + List<PendingOrderSubscriptionMessageRpcResponse> messagesToSend = new ArrayList<>(); | ||
| 117 | + for (PendingOrderSubscriptionMessageRpcResponse message : pendingMessages) { | ||
| 118 | + String openId = message.getOpenId(); | ||
| 119 | + | ||
| 120 | + // 检查用户是否活跃 | ||
| 121 | + if (!activeMpAccountMap.containsKey(openId)) { | ||
| 122 | + log.debug("Skipping message {} - user {} is not active", message.getId(), openId); | ||
| 123 | + skippedInactive.incrementAndGet(); | ||
| 124 | + sentMessageIds.add(message.getId()); | ||
| 125 | + continue; | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + // openId 去重:每个用户只推送一次 | ||
| 129 | + if (!sentOpenIds.add(openId)) { | ||
| 130 | + log.debug("Skipping message {} - already queued for openId: {}", message.getId(), openId); | ||
| 131 | + skippedDuplicate.incrementAndGet(); | ||
| 132 | + sentMessageIds.add(message.getId()); | ||
| 133 | + continue; | ||
| 134 | + } | ||
| 135 | + | ||
| 136 | + messagesToSend.add(message); | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + log.info("Messages to send: {}, Skipped inactive: {}, Skipped duplicate: {}", | ||
| 140 | + messagesToSend.size(), skippedInactive.get(), skippedDuplicate.get()); | ||
| 141 | + | ||
| 142 | + // 5. 多线程并发发送消息 | ||
| 143 | + final OrderTimeConfig finalOrderTimeConfig = orderTimeConfig; | ||
| 144 | + List<CompletableFuture<Void>> futures = messagesToSend.stream() | ||
| 145 | + .map(message -> CompletableFuture.runAsync(() -> { | ||
| 146 | + try { | ||
| 147 | + boolean success = sendReminderMessage(message.getOpenId(), message, finalOrderTimeConfig); | ||
| 148 | + if (success) { | ||
| 149 | + sentMessageIds.add(message.getId()); | ||
| 150 | + sentCount.incrementAndGet(); | ||
| 151 | + } | ||
| 152 | + } catch (Exception e) { | ||
| 153 | + log.error("Failed to send reminder to openId: {}, messageId: {}", | ||
| 154 | + message.getOpenId(), message.getId(), e); | ||
| 155 | + } | ||
| 156 | + }, asyncExecutor)) | ||
| 157 | + .toList(); | ||
| 158 | + | ||
| 159 | + // 等待所有任务完成 | ||
| 160 | + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); | ||
| 161 | + | ||
| 162 | + // 6. 批量标记已发送的消息(逻辑删除) | ||
| 163 | + if (!sentMessageIds.isEmpty()) { | ||
| 164 | + int affectedRows = mealOrderServiceRpcClient.batchDeleteOrderSubscriptionMessagesByIds( | ||
| 165 | + ENTERPRISE_ID, sentMessageIds); | ||
| 166 | + log.info("Marked {} messages as sent", affectedRows); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + log.info("OrderReminderTask completed. Total: {}, Sent: {}, SkippedInactive: {}, SkippedDuplicate: {}", | ||
| 170 | + totalMessages, sentCount.get(), skippedInactive.get(), skippedDuplicate.get()); | ||
| 171 | + | ||
| 172 | + } catch (Exception e) { | ||
| 173 | + log.error("OrderReminderTask failed", e); | ||
| 174 | + } | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | + /** | ||
| 178 | + * 从配送规则配置中获取订餐时间配置 | ||
| 179 | + * 返回开始时间和结束时间,格式:周x xx:xx | ||
| 180 | + */ | ||
| 181 | + private OrderTimeConfig getOrderTimeConfigFromRule() { | ||
| 182 | + try { | ||
| 183 | + final var response = deliveryRuleServiceRpcClient.getDefaultDeliveryTemplateAndRule(ENTERPRISE_ID); | ||
| 184 | + final var rule = response.getRule(); | ||
| 185 | + if (rule == null || rule.getConfigJson().getRulesList().isEmpty()) { | ||
| 186 | + log.warn("Delivery rule not configured"); | ||
| 187 | + return null; | ||
| 188 | + } | ||
| 189 | + | ||
| 190 | + final var cfg = rule.getConfigJson().getRules(0); | ||
| 191 | + final String startTimeStr = cfg.getStartTime(); | ||
| 192 | + final String endTimeStr = cfg.getEndTime(); | ||
| 193 | + if (startTimeStr == null || endTimeStr == null) { | ||
| 194 | + log.warn("Delivery rule time config incomplete"); | ||
| 195 | + return null; | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + // 获取启用的日期 | ||
| 199 | + List<DayOfWeek> enabledDays = getEnabledDays(cfg); | ||
| 200 | + if (enabledDays.isEmpty()) { | ||
| 201 | + log.warn("No enabled days in delivery rule"); | ||
| 202 | + return null; | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + // 格式化时间(去掉秒) | ||
| 206 | + String startTime = formatTime(startTimeStr); | ||
| 207 | + String endTime = formatTime(endTimeStr); | ||
| 208 | + | ||
| 209 | + // 获取第一个和最后一个启用日 | ||
| 210 | + DayOfWeek firstDay = enabledDays.get(0); | ||
| 211 | + DayOfWeek lastDay = enabledDays.get(enabledDays.size() - 1); | ||
| 212 | + | ||
| 213 | + // 计算本周的实际日期(微信time类型需要具体日期格式) | ||
| 214 | + LocalDate today = LocalDate.now(); | ||
| 215 | + LocalDate startDate = today.with(java.time.temporal.TemporalAdjusters.nextOrSame(firstDay)); | ||
| 216 | + LocalDate endDate = today.with(java.time.temporal.TemporalAdjusters.nextOrSame(lastDay)); | ||
| 217 | + | ||
| 218 | + // 如果结束日期在开始日期之前,说明跨周了,结束日期加一周 | ||
| 219 | + if (endDate.isBefore(startDate)) { | ||
| 220 | + endDate = endDate.plusWeeks(1); | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + // 格式化为微信接受的格式:yyyy年MM月dd日 HH:mm | ||
| 224 | + java.time.format.DateTimeFormatter dateFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy年MM月dd日"); | ||
| 225 | + String formattedStartTime = startDate.format(dateFormatter) + " " + startTime; | ||
| 226 | + String formattedEndTime = endDate.format(dateFormatter) + " " + endTime; | ||
| 227 | + | ||
| 228 | + return new OrderTimeConfig(formattedStartTime, formattedEndTime); | ||
| 229 | + | ||
| 230 | + } catch (Exception e) { | ||
| 231 | + log.error("Failed to get order time config from rule", e); | ||
| 232 | + return null; | ||
| 233 | + } | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + /** | ||
| 237 | + * 获取启用的日期列表(按周一到周日排序) | ||
| 238 | + */ | ||
| 239 | + private List<DayOfWeek> getEnabledDays(Rule cfg) { | ||
| 240 | + List<DayOfWeek> enabledDays = new ArrayList<>(); | ||
| 241 | + if (Boolean.TRUE.equals(cfg.getMonday())) enabledDays.add(DayOfWeek.MONDAY); | ||
| 242 | + if (Boolean.TRUE.equals(cfg.getTuesday())) enabledDays.add(DayOfWeek.TUESDAY); | ||
| 243 | + if (Boolean.TRUE.equals(cfg.getWednesday())) enabledDays.add(DayOfWeek.WEDNESDAY); | ||
| 244 | + if (Boolean.TRUE.equals(cfg.getThursday())) enabledDays.add(DayOfWeek.THURSDAY); | ||
| 245 | + if (Boolean.TRUE.equals(cfg.getFriday())) enabledDays.add(DayOfWeek.FRIDAY); | ||
| 246 | + if (Boolean.TRUE.equals(cfg.getSaturday())) enabledDays.add(DayOfWeek.SATURDAY); | ||
| 247 | + if (Boolean.TRUE.equals(cfg.getSunday())) enabledDays.add(DayOfWeek.SUNDAY); | ||
| 248 | + return enabledDays; | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + /** | ||
| 252 | + * 格式化时间字符串(HH:mm:ss -> HH:mm) | ||
| 253 | + */ | ||
| 254 | + private String formatTime(String timeStr) { | ||
| 255 | + if (timeStr == null || timeStr.length() < 5) { | ||
| 256 | + return timeStr; | ||
| 257 | + } | ||
| 258 | + // 取前5位 HH:mm | ||
| 259 | + return timeStr.substring(0, 5); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + /** | ||
| 263 | + * 获取活跃的小程序用户 Map(openId -> MpAccount) | ||
| 264 | + */ | ||
| 265 | + private Map<String, SingleMpAccountRpcResponse> getActiveMpAccountMap(Set<String> openIds) { | ||
| 266 | + if (CollectionUtils.isEmpty(openIds)) { | ||
| 267 | + return Collections.emptyMap(); | ||
| 268 | + } | ||
| 269 | + List<SingleMpAccountRpcResponse> allAccounts = mealOrderServiceRpcClient.queryAllActiveMpAccounts(ENTERPRISE_ID); | ||
| 270 | + return allAccounts.stream() | ||
| 271 | + .filter(account -> openIds.contains(account.getOpenId())) | ||
| 272 | + .collect(Collectors.toMap(SingleMpAccountRpcResponse::getOpenId, account -> account, (a, b) -> a)); | ||
| 273 | + } | ||
| 274 | + | ||
| 275 | + /** | ||
| 276 | + * 发送单条提醒消息 | ||
| 277 | + * | ||
| 278 | + * 小程序模板字段: | ||
| 279 | + * - time4: 开始时间(周x xx:xx) | ||
| 280 | + * - time5: 结束时间(周x xx:xx) | ||
| 281 | + * - thing1: 温馨提醒 | ||
| 282 | + */ | ||
| 283 | + private boolean sendReminderMessage(String openId, PendingOrderSubscriptionMessageRpcResponse message, OrderTimeConfig orderTimeConfig) { | ||
| 284 | + String jumpPath = message.getJumpPath(); | ||
| 285 | + | ||
| 286 | + // 发送消息 | ||
| 287 | + SendSubscriptionMessageResponse response = orderReminderService.sendOrderReminder( | ||
| 288 | + openId, | ||
| 289 | + jumpPath, | ||
| 290 | + orderTimeConfig.startTime(), // time4: 开始时间 | ||
| 291 | + orderTimeConfig.endTime(), // time5: 结束时间 | ||
| 292 | + TIPS // thing1: 温馨提醒 | ||
| 293 | + ); | ||
| 294 | + | ||
| 295 | + // 判断发送结果 | ||
| 296 | + if (response.getErrcode() != null && response.getErrcode() == 0) { | ||
| 297 | + return true; | ||
| 298 | + } | ||
| 299 | + | ||
| 300 | + // 记录失败原因 | ||
| 301 | + log.warn("Failed to send reminder. openId: {}, errcode: {}, errmsg: {}", | ||
| 302 | + openId, response.getErrcode(), response.getErrmsg()); | ||
| 303 | + return false; | ||
| 304 | + } | ||
| 305 | + | ||
| 306 | +} |
| 1 | +package com.infoloop.tianting.logic.task; | ||
| 2 | + | ||
| 3 | +import lombok.RequiredArgsConstructor; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.boot.CommandLineRunner; | ||
| 6 | +import org.springframework.stereotype.Component; | ||
| 7 | + | ||
| 8 | +import java.util.Arrays; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 订餐提醒任务命令行执行器 | ||
| 12 | + * 通过命令行参数 --trigger-order-reminder 触发执行 | ||
| 13 | + * | ||
| 14 | + * 使用方式: | ||
| 15 | + * java -jar app.jar --trigger-order-reminder | ||
| 16 | + * 或者 | ||
| 17 | + * java -jar app.jar --spring.main.web-application-type=none --trigger-order-reminder | ||
| 18 | + */ | ||
| 19 | +@Slf4j | ||
| 20 | +@Component | ||
| 21 | +@RequiredArgsConstructor | ||
| 22 | +public class OrderReminderTaskRunner implements CommandLineRunner { | ||
| 23 | + | ||
| 24 | + private final OrderReminderTask orderReminderTask; | ||
| 25 | + | ||
| 26 | + @Override | ||
| 27 | + public void run(String... args) { | ||
| 28 | + boolean shouldTrigger = Arrays.asList(args).contains("--trigger-order-reminder"); | ||
| 29 | + if (shouldTrigger) { | ||
| 30 | + log.info("OrderReminderTaskRunner: Triggering order reminder task via command line"); | ||
| 31 | + orderReminderTask.executeTask(); | ||
| 32 | + log.info("OrderReminderTaskRunner: Task execution completed"); | ||
| 33 | + } | ||
| 34 | + } | ||
| 35 | +} |
| ... | @@ -4,19 +4,24 @@ import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse | ... | @@ -4,19 +4,24 @@ import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse |
| 4 | 4 | ||
| 5 | /** | 5 | /** |
| 6 | * 订餐提醒服务接口 | 6 | * 订餐提醒服务接口 |
| 7 | - * 用于发送订餐周期开启前的提醒消息 | 7 | + * 用于发送订餐周期开启前的提醒消息(小程序订阅消息) |
| 8 | */ | 8 | */ |
| 9 | public interface OrderReminderService { | 9 | public interface OrderReminderService { |
| 10 | /** | 10 | /** |
| 11 | * 发送订餐提醒消息 | 11 | * 发送订餐提醒消息 |
| 12 | * | 12 | * |
| 13 | - * @param openId 用户 OpenID | 13 | + * 小程序模板字段(模板ID: LtCsW7ciF0edUwNwt3h5C_ZCIRzOeO7MnvozRl2CxlE): |
| 14 | - * @param page 点击跳转页面路径,例如:pages/order/index?cycleId=202501 | 14 | + * - time4: 开始时间 |
| 15 | - * @param reminderContent 提醒内容,例如:"下次订餐即将开启" | 15 | + * - time5: 结束时间 |
| 16 | - * @param orderTime 订餐时间,例如:"周一 09:00 至 周三 12:00" | 16 | + * - thing1: 温馨提醒 |
| 17 | - * @param tips 温馨提示,例如:"请点击卡片,准时进入小程序完成预订" | 17 | + * |
| 18 | + * @param openId 用户 OpenID(小程序的 openId) | ||
| 19 | + * @param page 点击跳转小程序页面路径 | ||
| 20 | + * @param startTime 开始时间,例如:"周一 09:00" | ||
| 21 | + * @param endTime 结束时间,例如:"周四 18:00" | ||
| 22 | + * @param tips 温馨提醒,例如:"请点击卡片,准时进入小程序完成预订" | ||
| 18 | * @return 发送结果 | 23 | * @return 发送结果 |
| 19 | */ | 24 | */ |
| 20 | SendSubscriptionMessageResponse sendOrderReminder(String openId, String page, | 25 | SendSubscriptionMessageResponse sendOrderReminder(String openId, String page, |
| 21 | - String reminderContent, String orderTime, String tips); | 26 | + String startTime, String endTime, String tips); |
| 22 | } | 27 | } | ... | ... |
| ... | @@ -12,8 +12,13 @@ import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcRequest; | ... | @@ -12,8 +12,13 @@ import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcRequest; |
| 12 | import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcResponse; | 12 | import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcResponse; |
| 13 | import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcRequest; | 13 | import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcRequest; |
| 14 | import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcResponse; | 14 | import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcResponse; |
| 15 | +import com.infoloop.tianting.mealorderservice.BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest; | ||
| 15 | import com.infoloop.tianting.mealorderservice.CreateOrderSubscriptionMessageRpcRequest; | 16 | import com.infoloop.tianting.mealorderservice.CreateOrderSubscriptionMessageRpcRequest; |
| 16 | import com.infoloop.tianting.mealorderservice.DeleteDinersByIdsRpcRequest; | 17 | import com.infoloop.tianting.mealorderservice.DeleteDinersByIdsRpcRequest; |
| 18 | +import com.infoloop.tianting.mealorderservice.GetUserOrderSubscriptionMessageHistoryRpcRequest; | ||
| 19 | +import com.infoloop.tianting.mealorderservice.OrderSubscriptionMessageRpcResponse; | ||
| 20 | +import com.infoloop.tianting.mealorderservice.PendingOrderSubscriptionMessageRpcResponse; | ||
| 21 | +import com.infoloop.tianting.mealorderservice.QueryPendingOrderSubscriptionMessagesRpcRequest; | ||
| 17 | import com.infoloop.tianting.mealorderservice.DeleteMpAccountDinerRefsByIdsRpcRequest; | 22 | import com.infoloop.tianting.mealorderservice.DeleteMpAccountDinerRefsByIdsRpcRequest; |
| 18 | import com.infoloop.tianting.mealorderservice.DinerCreation; | 23 | import com.infoloop.tianting.mealorderservice.DinerCreation; |
| 19 | import com.infoloop.tianting.mealorderservice.DinerModification; | 24 | import com.infoloop.tianting.mealorderservice.DinerModification; |
| ... | @@ -37,6 +42,7 @@ import com.infoloop.tianting.mealorderservice.QueryGradeClassesByConditionRpcReq | ... | @@ -37,6 +42,7 @@ import com.infoloop.tianting.mealorderservice.QueryGradeClassesByConditionRpcReq |
| 37 | import com.infoloop.tianting.mealorderservice.QueryLatestMealMenuRecordsByClientIdsRpcRequest; | 42 | import com.infoloop.tianting.mealorderservice.QueryLatestMealMenuRecordsByClientIdsRpcRequest; |
| 38 | import com.infoloop.tianting.mealorderservice.QueryMealOrdersByConditionRpcRequest; | 43 | import com.infoloop.tianting.mealorderservice.QueryMealOrdersByConditionRpcRequest; |
| 39 | import com.infoloop.tianting.mealorderservice.QueryMpAccountDinerRefsByConditionRpcRequest; | 44 | import com.infoloop.tianting.mealorderservice.QueryMpAccountDinerRefsByConditionRpcRequest; |
| 45 | +import com.infoloop.tianting.mealorderservice.QueryMpAccountsByConditionRpcRequest; | ||
| 40 | import com.infoloop.tianting.mealorderservice.QueryReserveMealOrdersByDinerIdsRpcRequest; | 46 | import com.infoloop.tianting.mealorderservice.QueryReserveMealOrdersByDinerIdsRpcRequest; |
| 41 | import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse; | 47 | import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse; |
| 42 | import com.infoloop.tianting.mealorderservice.SingleGradeClassRpcResponse; | 48 | import com.infoloop.tianting.mealorderservice.SingleGradeClassRpcResponse; |
| ... | @@ -105,6 +111,19 @@ public class MealOrderServiceRpcClient { | ... | @@ -105,6 +111,19 @@ public class MealOrderServiceRpcClient { |
| 105 | .build()); | 111 | .build()); |
| 106 | } | 112 | } |
| 107 | 113 | ||
| 114 | + /** | ||
| 115 | + * 查询所有活跃的小程序用户 | ||
| 116 | + */ | ||
| 117 | + public List<SingleMpAccountRpcResponse> queryAllActiveMpAccounts(int enterpriseId) { | ||
| 118 | + return mealOrderServiceRpcBlockingStub.queryMpAccountsByCondition( | ||
| 119 | + QueryMpAccountsByConditionRpcRequest.newBuilder() | ||
| 120 | + .setEnterpriseId(enterpriseId) | ||
| 121 | + .setShouldFilterStatus(false) | ||
| 122 | + .setIncludeDeleted(false) | ||
| 123 | + .build() | ||
| 124 | + ).getResponsesList(); | ||
| 125 | + } | ||
| 126 | + | ||
| 108 | public List<SingleMpAccountDinerRefRpcResponse> queryMpAccountDinerRefsByCondition(int enterpriseId, List<Integer> mpAccountIds, List<Integer> dinerIds) { | 127 | public List<SingleMpAccountDinerRefRpcResponse> queryMpAccountDinerRefsByCondition(int enterpriseId, List<Integer> mpAccountIds, List<Integer> dinerIds) { |
| 109 | return mealOrderServiceRpcBlockingStub.queryMpAccountDinerRefsByCondition(QueryMpAccountDinerRefsByConditionRpcRequest.newBuilder() | 128 | return mealOrderServiceRpcBlockingStub.queryMpAccountDinerRefsByCondition(QueryMpAccountDinerRefsByConditionRpcRequest.newBuilder() |
| 110 | .setEnterpriseId(enterpriseId) | 129 | .setEnterpriseId(enterpriseId) |
| ... | @@ -328,4 +347,48 @@ public class MealOrderServiceRpcClient { | ... | @@ -328,4 +347,48 @@ public class MealOrderServiceRpcClient { |
| 328 | } | 347 | } |
| 329 | return mealOrderServiceRpcBlockingStub.createOrderSubscriptionMessage(req.build()).getId(); | 348 | return mealOrderServiceRpcBlockingStub.createOrderSubscriptionMessage(req.build()).getId(); |
| 330 | } | 349 | } |
| 350 | + | ||
| 351 | + /** | ||
| 352 | + * 查询用户订阅消息历史(未删除的记录) | ||
| 353 | + */ | ||
| 354 | + public List<OrderSubscriptionMessageRpcResponse> getUserOrderSubscriptionMessageHistory( | ||
| 355 | + int enterpriseId, | ||
| 356 | + String openId, | ||
| 357 | + @Nullable Long orderPeriodStartDate | ||
| 358 | + ) { | ||
| 359 | + final var reqBuilder = GetUserOrderSubscriptionMessageHistoryRpcRequest.newBuilder() | ||
| 360 | + .setEnterpriseId(enterpriseId) | ||
| 361 | + .setOpenId(openId); | ||
| 362 | + if (orderPeriodStartDate != null) { | ||
| 363 | + reqBuilder.setOrderPeriodStartDate(orderPeriodStartDate); | ||
| 364 | + } | ||
| 365 | + return mealOrderServiceRpcBlockingStub.getUserOrderSubscriptionMessageHistory(reqBuilder.build()) | ||
| 366 | + .getResponseList(); | ||
| 367 | + } | ||
| 368 | + | ||
| 369 | + /** | ||
| 370 | + * 批量删除订阅消息(逻辑删除,发送成功后调用) | ||
| 371 | + */ | ||
| 372 | + public int batchDeleteOrderSubscriptionMessagesByIds(int enterpriseId, List<Long> ids) { | ||
| 373 | + if (CollectionUtils.isEmpty(ids)) { | ||
| 374 | + return 0; | ||
| 375 | + } | ||
| 376 | + return mealOrderServiceRpcBlockingStub.batchDeleteOrderSubscriptionMessagesByIds( | ||
| 377 | + BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest.newBuilder() | ||
| 378 | + .setEnterpriseId(enterpriseId) | ||
| 379 | + .addAllIds(ids) | ||
| 380 | + .build() | ||
| 381 | + ).getAffectedRows(); | ||
| 382 | + } | ||
| 383 | + | ||
| 384 | + /** | ||
| 385 | + * 查询待发送的订阅消息(未删除的记录) | ||
| 386 | + */ | ||
| 387 | + public List<PendingOrderSubscriptionMessageRpcResponse> queryPendingOrderSubscriptionMessages(int enterpriseId) { | ||
| 388 | + return mealOrderServiceRpcBlockingStub.queryPendingOrderSubscriptionMessages( | ||
| 389 | + QueryPendingOrderSubscriptionMessagesRpcRequest.newBuilder() | ||
| 390 | + .setEnterpriseId(enterpriseId) | ||
| 391 | + .build() | ||
| 392 | + ).getResponsesList(); | ||
| 393 | + } | ||
| 331 | } | 394 | } | ... | ... |
| 1 | +package com.infoloop.tianting.service.client; | ||
| 2 | + | ||
| 3 | +import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse; | ||
| 4 | +import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto; | ||
| 5 | +import com.infoloop.tianting.utils.JsonUtil; | ||
| 6 | +import lombok.RequiredArgsConstructor; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | +import org.apache.commons.lang3.StringUtils; | ||
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 10 | +import org.springframework.beans.factory.annotation.Value; | ||
| 11 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 12 | +import org.springframework.http.HttpEntity; | ||
| 13 | +import org.springframework.http.HttpHeaders; | ||
| 14 | +import org.springframework.http.MediaType; | ||
| 15 | +import org.springframework.http.ResponseEntity; | ||
| 16 | +import org.springframework.stereotype.Service; | ||
| 17 | +import org.springframework.web.client.RestTemplate; | ||
| 18 | + | ||
| 19 | +import java.util.HashMap; | ||
| 20 | +import java.util.Map; | ||
| 21 | +import java.util.concurrent.TimeUnit; | ||
| 22 | + | ||
| 23 | +import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_ID; | ||
| 24 | +import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_SECRET; | ||
| 25 | + | ||
| 26 | +/** | ||
| 27 | + * 微信公众号 HTTP 客户端 | ||
| 28 | + * 用于发送公众号一次性订阅消息 | ||
| 29 | + */ | ||
| 30 | +@Slf4j | ||
| 31 | +@Service | ||
| 32 | +@RequiredArgsConstructor(onConstructor = @__(@Autowired)) | ||
| 33 | +public class WxOfficialAccountHttpClient { | ||
| 34 | + | ||
| 35 | + /** | ||
| 36 | + * 公众号一次性订阅消息接口 | ||
| 37 | + * 文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/One-time_subscription_info.html | ||
| 38 | + */ | ||
| 39 | + private static final String SUBSCRIBE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/subscribe?access_token={accessToken}"; | ||
| 40 | + | ||
| 41 | + private static final String ACCESS_TOKEN_CACHE_KEY = "wx:official_account:access_token"; | ||
| 42 | + | ||
| 43 | + private final RestTemplate restTemplate; | ||
| 44 | + private final StringRedisTemplate stringRedisTemplate; | ||
| 45 | + | ||
| 46 | + @Value(OFFICIAL_ACCOUNT_APP_ID) | ||
| 47 | + private String appId; | ||
| 48 | + | ||
| 49 | + @Value(OFFICIAL_ACCOUNT_APP_SECRET) | ||
| 50 | + private String appSecret; | ||
| 51 | + | ||
| 52 | + private final Object lock = new Object(); | ||
| 53 | + | ||
| 54 | + /** | ||
| 55 | + * 获取公众号 access_token(带缓存) | ||
| 56 | + */ | ||
| 57 | + public String fetchStableAccessToken() { | ||
| 58 | + log.info("Fetching Official Account access token..."); | ||
| 59 | + String cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY); | ||
| 60 | + if (StringUtils.isNotEmpty(cachedToken)) { | ||
| 61 | + log.info("Using cached Official Account access token"); | ||
| 62 | + return cachedToken; | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + synchronized (lock) { | ||
| 66 | + cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY); | ||
| 67 | + if (StringUtils.isNotEmpty(cachedToken)) { | ||
| 68 | + log.info("Using cached Official Account access token after lock"); | ||
| 69 | + return cachedToken; | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + String url = "https://api.weixin.qq.com/cgi-bin/stable_token"; | ||
| 73 | + HttpHeaders headers = new HttpHeaders(); | ||
| 74 | + headers.setContentType(MediaType.APPLICATION_JSON); | ||
| 75 | + Map<String, String> requestBody = new HashMap<>(); | ||
| 76 | + requestBody.put("grant_type", "client_credential"); | ||
| 77 | + requestBody.put("appid", appId); | ||
| 78 | + requestBody.put("secret", appSecret); | ||
| 79 | + HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers); | ||
| 80 | + | ||
| 81 | + ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class); | ||
| 82 | + if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { | ||
| 83 | + log.error("Failed to fetch Official Account access token: {}", response); | ||
| 84 | + throw new IllegalArgumentException("获取公众号 access_token 失败"); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | + WxUserTokenDto tokenDto = JsonUtil.readJsonAs(response.getBody(), WxUserTokenDto.class); | ||
| 88 | + log.info("Official Account access token received, expires_in: {}", tokenDto.getExpires_in()); | ||
| 89 | + | ||
| 90 | + String accessToken = tokenDto.getAccess_token(); | ||
| 91 | + // 缓存 token,提前 5 分钟过期 | ||
| 92 | + long expireSeconds = tokenDto.getExpires_in() - 300; | ||
| 93 | + if (expireSeconds > 0) { | ||
| 94 | + stringRedisTemplate.opsForValue().set(ACCESS_TOKEN_CACHE_KEY, accessToken, expireSeconds, TimeUnit.SECONDS); | ||
| 95 | + } | ||
| 96 | + return accessToken; | ||
| 97 | + } | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | + /** | ||
| 101 | + * 发送公众号一次性订阅消息 | ||
| 102 | + * | ||
| 103 | + * @param openId 用户在公众号下的 OpenID | ||
| 104 | + * @param templateId 公众号订阅消息模板 ID | ||
| 105 | + * @param scene 订阅场景值(用户授权时传入的 scene) | ||
| 106 | + * @param title 消息标题(15字以内) | ||
| 107 | + * @param data 模板数据,格式为 Map<String, Map<String, String>> | ||
| 108 | + * @param url 点击跳转的 URL(可选) | ||
| 109 | + * @param miniprogram 跳转小程序配置(可选) | ||
| 110 | + * @return 发送结果 | ||
| 111 | + */ | ||
| 112 | + public SendSubscriptionMessageResponse sendSubscriptionMessage( | ||
| 113 | + String openId, | ||
| 114 | + String templateId, | ||
| 115 | + String scene, | ||
| 116 | + String title, | ||
| 117 | + Map<String, Object> data, | ||
| 118 | + String url, | ||
| 119 | + Map<String, String> miniprogram) { | ||
| 120 | + try { | ||
| 121 | + String accessToken = fetchStableAccessToken(); | ||
| 122 | + HttpHeaders headers = new HttpHeaders(); | ||
| 123 | + headers.setContentType(MediaType.APPLICATION_JSON); | ||
| 124 | + | ||
| 125 | + Map<String, Object> requestBody = new HashMap<>(); | ||
| 126 | + requestBody.put("touser", openId); | ||
| 127 | + requestBody.put("template_id", templateId); | ||
| 128 | + requestBody.put("scene", scene); | ||
| 129 | + requestBody.put("title", title); | ||
| 130 | + requestBody.put("data", data); | ||
| 131 | + | ||
| 132 | + if (StringUtils.isNotEmpty(url)) { | ||
| 133 | + requestBody.put("url", url); | ||
| 134 | + } | ||
| 135 | + if (miniprogram != null && !miniprogram.isEmpty()) { | ||
| 136 | + requestBody.put("miniprogram", miniprogram); | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers); | ||
| 140 | + ResponseEntity<SendSubscriptionMessageResponse> response = restTemplate.postForEntity( | ||
| 141 | + SUBSCRIBE_MESSAGE_URL, requestEntity, SendSubscriptionMessageResponse.class, accessToken); | ||
| 142 | + | ||
| 143 | + if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { | ||
| 144 | + log.error("Failed to send Official Account subscription message: {}", response); | ||
| 145 | + throw new IllegalArgumentException("发送公众号订阅消息失败"); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + SendSubscriptionMessageResponse result = response.getBody(); | ||
| 149 | + if (result.getErrcode() != null && result.getErrcode() != 0) { | ||
| 150 | + log.error("Official Account subscription message error: errcode={}, errmsg={}", | ||
| 151 | + result.getErrcode(), result.getErrmsg()); | ||
| 152 | + } else { | ||
| 153 | + log.info("Official Account subscription message sent successfully to openId: {}", openId); | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + return result; | ||
| 157 | + } catch (Exception e) { | ||
| 158 | + log.error("Error sending Official Account subscription message to openId: {}", openId, e); | ||
| 159 | + throw new IllegalArgumentException("发送公众号订阅消息失败: " + e.getMessage()); | ||
| 160 | + } | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + /** | ||
| 164 | + * 发送公众号一次性订阅消息(简化版,跳转到小程序) | ||
| 165 | + */ | ||
| 166 | + public SendSubscriptionMessageResponse sendSubscriptionMessageToMiniProgram( | ||
| 167 | + String openId, | ||
| 168 | + String templateId, | ||
| 169 | + String scene, | ||
| 170 | + String title, | ||
| 171 | + Map<String, Object> data, | ||
| 172 | + String miniProgramAppId, | ||
| 173 | + String miniProgramPagePath) { | ||
| 174 | + Map<String, String> miniprogram = new HashMap<>(); | ||
| 175 | + miniprogram.put("appid", miniProgramAppId); | ||
| 176 | + miniprogram.put("pagepath", miniProgramPagePath); | ||
| 177 | + return sendSubscriptionMessage(openId, templateId, scene, title, data, null, miniprogram); | ||
| 178 | + } | ||
| 179 | +} |
| ... | @@ -16,7 +16,16 @@ import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_ORDER_ | ... | @@ -16,7 +16,16 @@ import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_ORDER_ |
| 16 | 16 | ||
| 17 | /** | 17 | /** |
| 18 | * 订餐提醒服务实现 | 18 | * 订餐提醒服务实现 |
| 19 | - * 将业务参数转换为微信订阅消息模板格式并发送 | 19 | + * 使用小程序订阅消息接口发送提醒 |
| 20 | + * | ||
| 21 | + * 小程序模板字段(模板ID: LtCsW7ciF0edUwNwt3h5C_ZCIRzOeO7MnvozRl2CxlE): | ||
| 22 | + * - time4: 开始时间 | ||
| 23 | + * - time5: 结束时间 | ||
| 24 | + * - thing1: 温馨提醒 | ||
| 25 | + * | ||
| 26 | + * 注意: | ||
| 27 | + * 1. 使用的是小程序的 access_token 和 template_id | ||
| 28 | + * 2. openId 是用户在小程序下的 openId | ||
| 20 | */ | 29 | */ |
| 21 | @Slf4j | 30 | @Slf4j |
| 22 | @Service | 31 | @Service |
| ... | @@ -31,36 +40,41 @@ public class OrderReminderServiceImpl implements OrderReminderService { | ... | @@ -31,36 +40,41 @@ public class OrderReminderServiceImpl implements OrderReminderService { |
| 31 | @Override | 40 | @Override |
| 32 | public SendSubscriptionMessageResponse sendOrderReminder(String openId, | 41 | public SendSubscriptionMessageResponse sendOrderReminder(String openId, |
| 33 | String page, | 42 | String page, |
| 34 | - String reminderContent, | 43 | + String startTime, |
| 35 | - String orderTime, | 44 | + String endTime, |
| 36 | String tips) { | 45 | String tips) { |
| 37 | - log.info("Sending order reminder to openId: {}, page: {}", openId, page); | 46 | + log.info("Sending order reminder via MiniProgram to openId: {}, page: {}, startTime: {}, endTime: {}, tips: {}", |
| 47 | + openId, page, startTime, endTime, tips); | ||
| 38 | 48 | ||
| 39 | - // 将业务参数转换为微信模板数据格式 | 49 | + // 构建小程序订阅消息模板数据 |
| 40 | - // 根据文档,模板字段为:thing1 (提醒内容), time2 (订餐时间), thing3 (温馨提示) | ||
| 41 | Map<String, Object> data = new HashMap<>(); | 50 | Map<String, Object> data = new HashMap<>(); |
| 42 | 51 | ||
| 43 | - // thing1: 提醒内容 | 52 | + // time4: 开始时间 |
| 44 | - Map<String, String> thing1Value = new HashMap<>(); | 53 | + Map<String, String> time4Value = new HashMap<>(); |
| 45 | - thing1Value.put("value", reminderContent); | 54 | + time4Value.put("value", startTime); |
| 46 | - data.put("thing1", thing1Value); | 55 | + data.put("time4", time4Value); |
| 47 | 56 | ||
| 48 | - // time2: 订餐时间 | 57 | + // time5: 结束时间 |
| 49 | - Map<String, String> time2Value = new HashMap<>(); | 58 | + Map<String, String> time5Value = new HashMap<>(); |
| 50 | - time2Value.put("value", orderTime); | 59 | + time5Value.put("value", endTime); |
| 51 | - data.put("time2", time2Value); | 60 | + data.put("time5", time5Value); |
| 52 | 61 | ||
| 53 | - // thing3: 温馨提示 | 62 | + // thing1: 温馨提醒 |
| 54 | - Map<String, String> thing3Value = new HashMap<>(); | 63 | + Map<String, String> thing1Value = new HashMap<>(); |
| 55 | - thing3Value.put("value", tips); | 64 | + thing1Value.put("value", tips); |
| 56 | - data.put("thing3", thing3Value); | 65 | + data.put("thing1", thing1Value); |
| 57 | 66 | ||
| 58 | try { | 67 | try { |
| 68 | + // 使用小程序订阅消息接口 | ||
| 59 | SendSubscriptionMessageResponse response = wxMiniProgramService.sendSubscriptionMessage( | 69 | SendSubscriptionMessageResponse response = wxMiniProgramService.sendSubscriptionMessage( |
| 60 | - openId, templateId, page, data); | 70 | + openId, |
| 71 | + templateId, | ||
| 72 | + page, | ||
| 73 | + data | ||
| 74 | + ); | ||
| 61 | 75 | ||
| 62 | if (response.getErrcode() != null && response.getErrcode() == 0) { | 76 | if (response.getErrcode() != null && response.getErrcode() == 0) { |
| 63 | - log.info("Order reminder sent successfully to openId: {}", openId); | 77 | + log.info("Order reminder sent successfully via MiniProgram to openId: {}", openId); |
| 64 | } else { | 78 | } else { |
| 65 | log.warn("Order reminder sent with error. openId: {}, errcode: {}, errmsg: {}", | 79 | log.warn("Order reminder sent with error. openId: {}, errcode: {}, errmsg: {}", |
| 66 | openId, response.getErrcode(), response.getErrmsg()); | 80 | openId, response.getErrcode(), response.getErrmsg()); |
| ... | @@ -68,7 +82,7 @@ public class OrderReminderServiceImpl implements OrderReminderService { | ... | @@ -68,7 +82,7 @@ public class OrderReminderServiceImpl implements OrderReminderService { |
| 68 | 82 | ||
| 69 | return response; | 83 | return response; |
| 70 | } catch (Exception e) { | 84 | } catch (Exception e) { |
| 71 | - log.error("Failed to send order reminder to openId: {}", openId, e); | 85 | + log.error("Failed to send order reminder via MiniProgram to openId: {}", openId, e); |
| 72 | throw e; | 86 | throw e; |
| 73 | } | 87 | } |
| 74 | } | 88 | } | ... | ... |
| ... | @@ -69,6 +69,7 @@ service MealOrderServiceRpc { | ... | @@ -69,6 +69,7 @@ service MealOrderServiceRpc { |
| 69 | rpc GetUserOrderSubscriptionMessageHistory (GetUserOrderSubscriptionMessageHistoryRpcRequest) returns (GetUserOrderSubscriptionMessageHistoryRpcResponse) {} | 69 | rpc GetUserOrderSubscriptionMessageHistory (GetUserOrderSubscriptionMessageHistoryRpcRequest) returns (GetUserOrderSubscriptionMessageHistoryRpcResponse) {} |
| 70 | rpc BatchDeleteOrderSubscriptionMessagesByIds (BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse) {} | 70 | rpc BatchDeleteOrderSubscriptionMessagesByIds (BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse) {} |
| 71 | rpc BatchPhysicalDeleteOrderSubscriptionMessagesByIds (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse) {} | 71 | rpc BatchPhysicalDeleteOrderSubscriptionMessagesByIds (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse) {} |
| 72 | + rpc QueryPendingOrderSubscriptionMessages (QueryPendingOrderSubscriptionMessagesRpcRequest) returns (QueryPendingOrderSubscriptionMessagesRpcResponse) {} | ||
| 72 | } | 73 | } |
| 73 | 74 | ||
| 74 | enum MealOrderOrderMethodEnum { | 75 | enum MealOrderOrderMethodEnum { |
| ... | @@ -1013,3 +1014,24 @@ message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest { | ... | @@ -1013,3 +1014,24 @@ message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest { |
| 1013 | message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse { | 1014 | message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse { |
| 1014 | int32 affectedRows = 1; // 受影响的行数 | 1015 | int32 affectedRows = 1; // 受影响的行数 |
| 1015 | } | 1016 | } |
| 1017 | + | ||
| 1018 | +// 查询待发送的订阅消息(未删除的记录) | ||
| 1019 | +message QueryPendingOrderSubscriptionMessagesRpcRequest { | ||
| 1020 | + int32 enterpriseId = 1; | ||
| 1021 | +} | ||
| 1022 | + | ||
| 1023 | +message PendingOrderSubscriptionMessageRpcResponse { | ||
| 1024 | + int64 id = 1; | ||
| 1025 | + int32 enterpriseId = 2; | ||
| 1026 | + int32 dinerId = 3; | ||
| 1027 | + string openId = 4; | ||
| 1028 | + string templateId = 5; | ||
| 1029 | + int64 orderPeriodStartDate = 6; | ||
| 1030 | + string jumpPath = 7; | ||
| 1031 | + int64 orderPeriodEndDate = 8; | ||
| 1032 | + int64 createdAt = 9; | ||
| 1033 | +} | ||
| 1034 | + | ||
| 1035 | +message QueryPendingOrderSubscriptionMessagesRpcResponse { | ||
| 1036 | + repeated PendingOrderSubscriptionMessageRpcResponse responses = 1; | ||
| 1037 | +} | ... | ... |
-
Please register or login to post a comment