Showing
8 changed files
with
351 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 | } | ... | ... |
This diff is collapsed. Click to expand it.
| 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