Jiaqi Xia

feat: 微信点餐推送

......@@ -83,4 +83,9 @@ public interface ConfigConstants {
String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}";
String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}";
// 公众号配置(用于订阅消息推送)
String OFFICIAL_ACCOUNT_APP_ID = "${officialAccount.appId}";
String OFFICIAL_ACCOUNT_APP_SECRET = "${officialAccount.appSecret}";
String OFFICIAL_ACCOUNT_ORDER_REMINDER_TEMPLATE_ID = "${officialAccount.orderReminderTemplateId}";
}
......
package com.infoloop.tianting.logic.task;
import com.infoloop.tianting.mealorderservice.PendingOrderSubscriptionMessageRpcResponse;
import com.infoloop.tianting.mealorderservice.SingleMpAccountRpcResponse;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.service.OrderReminderService;
import com.infoloop.tianting.service.client.DeliveryRuleServiceRpcClient;
import com.infoloop.tianting.service.client.MealOrderServiceRpcClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static com.infoloop.tianting.constant.ConfigConstants.ASYNC_EXECUTOR;
/**
* 订餐提醒定时任务
* 在订餐周期开启前(周一 9:00)向已授权的家长发送微信服务通知
*
* 推送逻辑:
* 1. 查询所有待发送的订阅消息(未删除的记录)
* 2. 按 openId 去重,每个用户只推送一次
* 3. 通过 openId 查询活跃的小程序用户
* 4. 根据配送规则配置生成订餐时间提示
* 5. 发送消息,发送成功后标记为已删除
*/
@Slf4j
@Component
public class OrderReminderTask {
private static final int ENTERPRISE_ID = 449;
/**
* 用餐类型
*/
private static final String MEAL_TYPE = "下次订餐即将开启";
/**
* 温馨提醒内容
*/
private static final String TIPS = "请点击卡片,准时进入小程序完成预订";
/**
* 订餐时间配置(格式:HH:mm~HH:mm)
*/
private record OrderTimeConfig(String orderTime) {}
private final MealOrderServiceRpcClient mealOrderServiceRpcClient;
private final DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient;
private final OrderReminderService orderReminderService;
private final Executor asyncExecutor;
public OrderReminderTask(
MealOrderServiceRpcClient mealOrderServiceRpcClient,
DeliveryRuleServiceRpcClient deliveryRuleServiceRpcClient,
OrderReminderService orderReminderService,
@Qualifier(ASYNC_EXECUTOR) Executor asyncExecutor) {
this.mealOrderServiceRpcClient = mealOrderServiceRpcClient;
this.deliveryRuleServiceRpcClient = deliveryRuleServiceRpcClient;
this.orderReminderService = orderReminderService;
this.asyncExecutor = asyncExecutor;
}
/**
* 每周一 9:00 执行订餐提醒推送
*/
@Scheduled(cron = "0 0 9 * * MON")
public void executeTask() {
log.info("OrderReminderTask started");
try {
// 1. 获取配送规则配置,用于生成订餐时间提示
OrderTimeConfig orderTimeConfig = getOrderTimeConfigFromRule();
if (orderTimeConfig == null) {
log.warn("Failed to get order time config from rule, using default");
orderTimeConfig = new OrderTimeConfig("请查看小程序");
}
log.info("Order time config: orderTime={}", orderTimeConfig.orderTime());
// 2. 查询所有待发送的订阅消息(未删除的记录)
List<PendingOrderSubscriptionMessageRpcResponse> pendingMessages =
mealOrderServiceRpcClient.queryPendingOrderSubscriptionMessages(ENTERPRISE_ID);
if (pendingMessages.isEmpty()) {
log.info("No pending order subscription messages to send");
return;
}
log.info("Found {} pending order subscription messages", pendingMessages.size());
// 3. 提取所有 openId,查询活跃的小程序用户
Set<String> openIds = pendingMessages.stream()
.map(PendingOrderSubscriptionMessageRpcResponse::getOpenId)
.collect(Collectors.toSet());
Map<String, SingleMpAccountRpcResponse> activeMpAccountMap = getActiveMpAccountMap(openIds);
log.info("Found {} active mp accounts from {} openIds", activeMpAccountMap.size(), openIds.size());
int totalMessages = pendingMessages.size();
AtomicInteger skippedInactive = new AtomicInteger(0);
AtomicInteger skippedDuplicate = new AtomicInteger(0);
AtomicInteger sentCount = new AtomicInteger(0);
List<Long> sentMessageIds = Collections.synchronizedList(new ArrayList<>());
Set<String> sentOpenIds = ConcurrentHashMap.newKeySet(); // 线程安全的 Set,用于 openId 去重
// 4. 按 openId 去重,筛选出需要发送的消息
List<PendingOrderSubscriptionMessageRpcResponse> messagesToSend = new ArrayList<>();
for (PendingOrderSubscriptionMessageRpcResponse message : pendingMessages) {
String openId = message.getOpenId();
// 检查用户是否活跃
if (!activeMpAccountMap.containsKey(openId)) {
log.debug("Skipping message {} - user {} is not active", message.getId(), openId);
skippedInactive.incrementAndGet();
sentMessageIds.add(message.getId());
continue;
}
// openId 去重:每个用户只推送一次
if (!sentOpenIds.add(openId)) {
log.debug("Skipping message {} - already queued for openId: {}", message.getId(), openId);
skippedDuplicate.incrementAndGet();
sentMessageIds.add(message.getId());
continue;
}
messagesToSend.add(message);
}
log.info("Messages to send: {}, Skipped inactive: {}, Skipped duplicate: {}",
messagesToSend.size(), skippedInactive.get(), skippedDuplicate.get());
// 5. 多线程并发发送消息
final OrderTimeConfig finalOrderTimeConfig = orderTimeConfig;
List<CompletableFuture<Void>> futures = messagesToSend.stream()
.map(message -> CompletableFuture.runAsync(() -> {
try {
boolean success = sendReminderMessage(message.getOpenId(), message, finalOrderTimeConfig);
if (success) {
sentMessageIds.add(message.getId());
sentCount.incrementAndGet();
}
} catch (Exception e) {
log.error("Failed to send reminder to openId: {}, messageId: {}",
message.getOpenId(), message.getId(), e);
}
}, asyncExecutor))
.toList();
// 等待所有任务完成
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
// 6. 批量标记已发送的消息(逻辑删除)
if (!sentMessageIds.isEmpty()) {
int affectedRows = mealOrderServiceRpcClient.batchDeleteOrderSubscriptionMessagesByIds(
ENTERPRISE_ID, sentMessageIds);
log.info("Marked {} messages as sent", affectedRows);
}
log.info("OrderReminderTask completed. Total: {}, Sent: {}, SkippedInactive: {}, SkippedDuplicate: {}",
totalMessages, sentCount.get(), skippedInactive.get(), skippedDuplicate.get());
} catch (Exception e) {
log.error("OrderReminderTask failed", e);
}
}
/**
* 从配送规则配置中获取订餐时间配置
* 返回订餐时间,格式:yyyy年MM月dd日 HH:mm~HH:mm
*/
private OrderTimeConfig getOrderTimeConfigFromRule() {
try {
final var response = deliveryRuleServiceRpcClient.getDefaultDeliveryTemplateAndRule(ENTERPRISE_ID);
final var rule = response.getRule();
if (rule == null || rule.getConfigJson().getRulesList().isEmpty()) {
log.warn("Delivery rule not configured");
return null;
}
final var cfg = rule.getConfigJson().getRules(0);
final String startTimeStr = cfg.getStartTime();
final String endTimeStr = cfg.getEndTime();
if (startTimeStr == null || endTimeStr == null) {
log.warn("Delivery rule time config incomplete");
return null;
}
// 格式化时间(去掉秒)
String startTime = formatTime(startTimeStr);
String endTime = formatTime(endTimeStr);
// 获取当前日期,格式化为 yyyy年MM月dd日 HH:mm
java.time.LocalDate today = java.time.LocalDate.now();
java.time.format.DateTimeFormatter dateFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy年MM月dd日");
String dateStr = today.format(dateFormatter);
// 返回订餐时间,格式:yyyy年MM月dd日 HH:mm~yyyy年MM月dd日 HH:mm
String orderTime = dateStr + " " + startTime + "~" + dateStr + " " + endTime;
return new OrderTimeConfig(orderTime);
} catch (Exception e) {
log.error("Failed to get order time config from rule", e);
return null;
}
}
/**
* 格式化时间字符串(HH:mm:ss -> HH:mm)
*/
private String formatTime(String timeStr) {
if (timeStr == null || timeStr.length() < 5) {
return timeStr;
}
// 取前5位 HH:mm
return timeStr.substring(0, 5);
}
/**
* 获取活跃的小程序用户 Map(openId -> MpAccount)
*/
private Map<String, SingleMpAccountRpcResponse> getActiveMpAccountMap(Set<String> openIds) {
if (CollectionUtils.isEmpty(openIds)) {
return Collections.emptyMap();
}
List<SingleMpAccountRpcResponse> allAccounts = mealOrderServiceRpcClient.queryAllActiveMpAccounts(ENTERPRISE_ID);
return allAccounts.stream()
.filter(account -> openIds.contains(account.getOpenId()))
.collect(Collectors.toMap(SingleMpAccountRpcResponse::getOpenId, account -> account, (a, b) -> a));
}
/**
* 发送单条提醒消息
*
* 小程序模板字段(模板ID: BCROwaS_oj1_b0fzc_qrgvtSeCtfxctghcLMu-Obkfw):
* - thing8: 用餐类型(如:下次订单即将开始)
* - time1: 订餐时间(如:13:00~21:00)
* - thing2: 温馨提示
*/
private boolean sendReminderMessage(String openId, PendingOrderSubscriptionMessageRpcResponse message, OrderTimeConfig orderTimeConfig) {
String jumpPath = message.getJumpPath();
// 发送消息
SendSubscriptionMessageResponse response = orderReminderService.sendOrderReminder(
openId,
jumpPath,
MEAL_TYPE, // thing8: 用餐类型
orderTimeConfig.orderTime(), // time1: 订餐时间
TIPS // thing2: 温馨提示
);
// 判断发送结果
if (response.getErrcode() != null && response.getErrcode() == 0) {
return true;
}
// 记录失败原因
log.warn("Failed to send reminder. openId: {}, errcode: {}, errmsg: {}",
openId, response.getErrcode(), response.getErrmsg());
return false;
}
}
package com.infoloop.tianting.logic.task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* 订餐提醒任务命令行执行器
* 通过命令行参数 --trigger-order-reminder 触发执行
*
* 使用方式:
* java -jar app.jar --trigger-order-reminder
* 或者
* java -jar app.jar --spring.main.web-application-type=none --trigger-order-reminder
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OrderReminderTaskRunner implements CommandLineRunner {
private final OrderReminderTask orderReminderTask;
@Override
public void run(String... args) {
boolean shouldTrigger = Arrays.asList(args).contains("--trigger-order-reminder");
if (shouldTrigger) {
log.info("OrderReminderTaskRunner: Triggering order reminder task via command line");
orderReminderTask.executeTask();
log.info("OrderReminderTaskRunner: Task execution completed");
}
}
}
......@@ -4,19 +4,24 @@ import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse
/**
* 订餐提醒服务接口
* 用于发送订餐周期开启前的提醒消息
* 用于发送订餐周期开启前的提醒消息(小程序订阅消息)
*/
public interface OrderReminderService {
/**
* 发送订餐提醒消息
*
* @param openId 用户 OpenID
* @param page 点击跳转页面路径,例如:pages/order/index?cycleId=202501
* @param reminderContent 提醒内容,例如:"下次订餐即将开启"
* @param orderTime 订餐时间,例如:"周一 09:00 至 周三 12:00"
* @param tips 温馨提示,例如:"请点击卡片,准时进入小程序完成预订"
* 小程序模板字段(模板ID: BCROwaS_oj1_b0fzc_qrgvtSeCtfxctghcLMu-Obkfw):
* - thing8: 用餐类型(如:下次订单即将开始)
* - time1: 订餐时间(如:13:00~21:00)
* - thing2: 温馨提示(如:为了保证您明日正常用餐,立即订餐!)
*
* @param openId 用户 OpenID(小程序的 openId)
* @param page 点击跳转小程序页面路径
* @param mealType 用餐类型,例如:"下次订单即将开始"
* @param orderTime 订餐时间,例如:"13:00~21:00"
* @param tips 温馨提示,例如:"为了保证您明日正常用餐,立即订餐!"
* @return 发送结果
*/
SendSubscriptionMessageResponse sendOrderReminder(String openId, String page,
String reminderContent, String orderTime, String tips);
String mealType, String orderTime, String tips);
}
......
......@@ -12,8 +12,13 @@ import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcRequest;
import com.infoloop.tianting.mealorderservice.CreateMpAccountDinerRefRpcResponse;
import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcRequest;
import com.infoloop.tianting.mealorderservice.CreateMpAccountRpcResponse;
import com.infoloop.tianting.mealorderservice.BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.CreateOrderSubscriptionMessageRpcRequest;
import com.infoloop.tianting.mealorderservice.DeleteDinersByIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.GetUserOrderSubscriptionMessageHistoryRpcRequest;
import com.infoloop.tianting.mealorderservice.OrderSubscriptionMessageRpcResponse;
import com.infoloop.tianting.mealorderservice.PendingOrderSubscriptionMessageRpcResponse;
import com.infoloop.tianting.mealorderservice.QueryPendingOrderSubscriptionMessagesRpcRequest;
import com.infoloop.tianting.mealorderservice.DeleteMpAccountDinerRefsByIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.DinerCreation;
import com.infoloop.tianting.mealorderservice.DinerModification;
......@@ -37,6 +42,7 @@ import com.infoloop.tianting.mealorderservice.QueryGradeClassesByConditionRpcReq
import com.infoloop.tianting.mealorderservice.QueryLatestMealMenuRecordsByClientIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.QueryMealOrdersByConditionRpcRequest;
import com.infoloop.tianting.mealorderservice.QueryMpAccountDinerRefsByConditionRpcRequest;
import com.infoloop.tianting.mealorderservice.QueryMpAccountsByConditionRpcRequest;
import com.infoloop.tianting.mealorderservice.QueryReserveMealOrdersByDinerIdsRpcRequest;
import com.infoloop.tianting.mealorderservice.SingleDinerRpcResponse;
import com.infoloop.tianting.mealorderservice.SingleGradeClassRpcResponse;
......@@ -105,6 +111,19 @@ public class MealOrderServiceRpcClient {
.build());
}
/**
* 查询所有活跃的小程序用户
*/
public List<SingleMpAccountRpcResponse> queryAllActiveMpAccounts(int enterpriseId) {
return mealOrderServiceRpcBlockingStub.queryMpAccountsByCondition(
QueryMpAccountsByConditionRpcRequest.newBuilder()
.setEnterpriseId(enterpriseId)
.setShouldFilterStatus(false)
.setIncludeDeleted(false)
.build()
).getResponsesList();
}
public List<SingleMpAccountDinerRefRpcResponse> queryMpAccountDinerRefsByCondition(int enterpriseId, List<Integer> mpAccountIds, List<Integer> dinerIds) {
return mealOrderServiceRpcBlockingStub.queryMpAccountDinerRefsByCondition(QueryMpAccountDinerRefsByConditionRpcRequest.newBuilder()
.setEnterpriseId(enterpriseId)
......@@ -328,4 +347,48 @@ public class MealOrderServiceRpcClient {
}
return mealOrderServiceRpcBlockingStub.createOrderSubscriptionMessage(req.build()).getId();
}
/**
* 查询用户订阅消息历史(未删除的记录)
*/
public List<OrderSubscriptionMessageRpcResponse> getUserOrderSubscriptionMessageHistory(
int enterpriseId,
String openId,
@Nullable Long orderPeriodStartDate
) {
final var reqBuilder = GetUserOrderSubscriptionMessageHistoryRpcRequest.newBuilder()
.setEnterpriseId(enterpriseId)
.setOpenId(openId);
if (orderPeriodStartDate != null) {
reqBuilder.setOrderPeriodStartDate(orderPeriodStartDate);
}
return mealOrderServiceRpcBlockingStub.getUserOrderSubscriptionMessageHistory(reqBuilder.build())
.getResponseList();
}
/**
* 批量删除订阅消息(逻辑删除,发送成功后调用)
*/
public int batchDeleteOrderSubscriptionMessagesByIds(int enterpriseId, List<Long> ids) {
if (CollectionUtils.isEmpty(ids)) {
return 0;
}
return mealOrderServiceRpcBlockingStub.batchDeleteOrderSubscriptionMessagesByIds(
BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest.newBuilder()
.setEnterpriseId(enterpriseId)
.addAllIds(ids)
.build()
).getAffectedRows();
}
/**
* 查询待发送的订阅消息(未删除的记录)
*/
public List<PendingOrderSubscriptionMessageRpcResponse> queryPendingOrderSubscriptionMessages(int enterpriseId) {
return mealOrderServiceRpcBlockingStub.queryPendingOrderSubscriptionMessages(
QueryPendingOrderSubscriptionMessagesRpcRequest.newBuilder()
.setEnterpriseId(enterpriseId)
.build()
).getResponsesList();
}
}
......
package com.infoloop.tianting.service.client;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto;
import com.infoloop.tianting.utils.JsonUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_ID;
import static com.infoloop.tianting.constant.ConfigConstants.OFFICIAL_ACCOUNT_APP_SECRET;
/**
* 微信公众号 HTTP 客户端
* 用于发送公众号一次性订阅消息
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class WxOfficialAccountHttpClient {
/**
* 公众号一次性订阅消息接口
* 文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/One-time_subscription_info.html
*/
private static final String SUBSCRIBE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/subscribe?access_token={accessToken}";
private static final String ACCESS_TOKEN_CACHE_KEY = "wx:official_account:access_token";
private final RestTemplate restTemplate;
private final StringRedisTemplate stringRedisTemplate;
@Value(OFFICIAL_ACCOUNT_APP_ID)
private String appId;
@Value(OFFICIAL_ACCOUNT_APP_SECRET)
private String appSecret;
private final Object lock = new Object();
/**
* 获取公众号 access_token(带缓存)
*/
public String fetchStableAccessToken() {
log.info("Fetching Official Account access token...");
String cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY);
if (StringUtils.isNotEmpty(cachedToken)) {
log.info("Using cached Official Account access token");
return cachedToken;
}
synchronized (lock) {
cachedToken = stringRedisTemplate.opsForValue().get(ACCESS_TOKEN_CACHE_KEY);
if (StringUtils.isNotEmpty(cachedToken)) {
log.info("Using cached Official Account access token after lock");
return cachedToken;
}
String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, String> requestBody = new HashMap<>();
requestBody.put("grant_type", "client_credential");
requestBody.put("appid", appId);
requestBody.put("secret", appSecret);
HttpEntity<Map<String, String>> request = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.postForEntity(url, request, String.class);
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
log.error("Failed to fetch Official Account access token: {}", response);
throw new IllegalArgumentException("获取公众号 access_token 失败");
}
WxUserTokenDto tokenDto = JsonUtil.readJsonAs(response.getBody(), WxUserTokenDto.class);
log.info("Official Account access token received, expires_in: {}", tokenDto.getExpires_in());
String accessToken = tokenDto.getAccess_token();
// 缓存 token,提前 5 分钟过期
long expireSeconds = tokenDto.getExpires_in() - 300;
if (expireSeconds > 0) {
stringRedisTemplate.opsForValue().set(ACCESS_TOKEN_CACHE_KEY, accessToken, expireSeconds, TimeUnit.SECONDS);
}
return accessToken;
}
}
/**
* 发送公众号一次性订阅消息
*
* @param openId 用户在公众号下的 OpenID
* @param templateId 公众号订阅消息模板 ID
* @param scene 订阅场景值(用户授权时传入的 scene)
* @param title 消息标题(15字以内)
* @param data 模板数据,格式为 Map<String, Map<String, String>>
* @param url 点击跳转的 URL(可选)
* @param miniprogram 跳转小程序配置(可选)
* @return 发送结果
*/
public SendSubscriptionMessageResponse sendSubscriptionMessage(
String openId,
String templateId,
String scene,
String title,
Map<String, Object> data,
String url,
Map<String, String> miniprogram) {
try {
String accessToken = fetchStableAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
Map<String, Object> requestBody = new HashMap<>();
requestBody.put("touser", openId);
requestBody.put("template_id", templateId);
requestBody.put("scene", scene);
requestBody.put("title", title);
requestBody.put("data", data);
if (StringUtils.isNotEmpty(url)) {
requestBody.put("url", url);
}
if (miniprogram != null && !miniprogram.isEmpty()) {
requestBody.put("miniprogram", miniprogram);
}
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<SendSubscriptionMessageResponse> response = restTemplate.postForEntity(
SUBSCRIBE_MESSAGE_URL, requestEntity, SendSubscriptionMessageResponse.class, accessToken);
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
log.error("Failed to send Official Account subscription message: {}", response);
throw new IllegalArgumentException("发送公众号订阅消息失败");
}
SendSubscriptionMessageResponse result = response.getBody();
if (result.getErrcode() != null && result.getErrcode() != 0) {
log.error("Official Account subscription message error: errcode={}, errmsg={}",
result.getErrcode(), result.getErrmsg());
} else {
log.info("Official Account subscription message sent successfully to openId: {}", openId);
}
return result;
} catch (Exception e) {
log.error("Error sending Official Account subscription message to openId: {}", openId, e);
throw new IllegalArgumentException("发送公众号订阅消息失败: " + e.getMessage());
}
}
/**
* 发送公众号一次性订阅消息(简化版,跳转到小程序)
*/
public SendSubscriptionMessageResponse sendSubscriptionMessageToMiniProgram(
String openId,
String templateId,
String scene,
String title,
Map<String, Object> data,
String miniProgramAppId,
String miniProgramPagePath) {
Map<String, String> miniprogram = new HashMap<>();
miniprogram.put("appid", miniProgramAppId);
miniprogram.put("pagepath", miniProgramPagePath);
return sendSubscriptionMessage(openId, templateId, scene, title, data, null, miniprogram);
}
}
......@@ -16,7 +16,16 @@ import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_ORDER_
/**
* 订餐提醒服务实现
* 将业务参数转换为微信订阅消息模板格式并发送
* 使用小程序订阅消息接口发送提醒
*
* 小程序模板字段(模板ID: BCROwaS_oj1_b0fzc_qrgvtSeCtfxctghcLMu-Obkfw):
* - thing8: 用餐类型(如:下次订单即将开始)
* - time1: 订餐时间(如:13:00~21:00)
* - thing2: 温馨提示(如:为了保证您明日正常用餐,立即订餐!)
*
* 注意:
* 1. 使用的是小程序的 access_token 和 template_id
* 2. openId 是用户在小程序下的 openId
*/
@Slf4j
@Service
......@@ -31,36 +40,41 @@ public class OrderReminderServiceImpl implements OrderReminderService {
@Override
public SendSubscriptionMessageResponse sendOrderReminder(String openId,
String page,
String reminderContent,
String mealType,
String orderTime,
String tips) {
log.info("Sending order reminder to openId: {}, page: {}", openId, page);
log.info("Sending order reminder via MiniProgram to openId: {}, page: {}, mealType: {}, orderTime: {}, tips: {}",
openId, page, mealType, orderTime, tips);
// 将业务参数转换为微信模板数据格式
// 根据文档,模板字段为:thing1 (提醒内容), time2 (订餐时间), thing3 (温馨提示)
// 构建小程序订阅消息模板数据
Map<String, Object> data = new HashMap<>();
// thing1: 提醒内容
Map<String, String> thing1Value = new HashMap<>();
thing1Value.put("value", reminderContent);
data.put("thing1", thing1Value);
// thing8: 用餐类型(如:下次订单即将开始)
Map<String, String> thing8Value = new HashMap<>();
thing8Value.put("value", mealType);
data.put("thing8", thing8Value);
// time2: 订餐时间
Map<String, String> time2Value = new HashMap<>();
time2Value.put("value", orderTime);
data.put("time2", time2Value);
// time1: 订餐时间(如:13:00~21:00)
Map<String, String> time1Value = new HashMap<>();
time1Value.put("value", orderTime);
data.put("time1", time1Value);
// thing3: 温馨提示
Map<String, String> thing3Value = new HashMap<>();
thing3Value.put("value", tips);
data.put("thing3", thing3Value);
// thing2: 温馨提示
Map<String, String> thing2Value = new HashMap<>();
thing2Value.put("value", tips);
data.put("thing2", thing2Value);
try {
// 使用小程序订阅消息接口
SendSubscriptionMessageResponse response = wxMiniProgramService.sendSubscriptionMessage(
openId, templateId, page, data);
openId,
templateId,
page,
data
);
if (response.getErrcode() != null && response.getErrcode() == 0) {
log.info("Order reminder sent successfully to openId: {}", openId);
log.info("Order reminder sent successfully via MiniProgram to openId: {}", openId);
} else {
log.warn("Order reminder sent with error. openId: {}, errcode: {}, errmsg: {}",
openId, response.getErrcode(), response.getErrmsg());
......@@ -68,7 +82,7 @@ public class OrderReminderServiceImpl implements OrderReminderService {
return response;
} catch (Exception e) {
log.error("Failed to send order reminder to openId: {}", openId, e);
log.error("Failed to send order reminder via MiniProgram to openId: {}", openId, e);
throw e;
}
}
......
......@@ -69,6 +69,7 @@ service MealOrderServiceRpc {
rpc GetUserOrderSubscriptionMessageHistory (GetUserOrderSubscriptionMessageHistoryRpcRequest) returns (GetUserOrderSubscriptionMessageHistoryRpcResponse) {}
rpc BatchDeleteOrderSubscriptionMessagesByIds (BatchDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchDeleteOrderSubscriptionMessagesByIdsRpcResponse) {}
rpc BatchPhysicalDeleteOrderSubscriptionMessagesByIds (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest) returns (BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse) {}
rpc QueryPendingOrderSubscriptionMessages (QueryPendingOrderSubscriptionMessagesRpcRequest) returns (QueryPendingOrderSubscriptionMessagesRpcResponse) {}
}
enum MealOrderOrderMethodEnum {
......@@ -1013,3 +1014,24 @@ message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcRequest {
message BatchPhysicalDeleteOrderSubscriptionMessagesByIdsRpcResponse {
int32 affectedRows = 1; // 受影响的行数
}
// 查询待发送的订阅消息(未删除的记录)
message QueryPendingOrderSubscriptionMessagesRpcRequest {
int32 enterpriseId = 1;
}
message PendingOrderSubscriptionMessageRpcResponse {
int64 id = 1;
int32 enterpriseId = 2;
int32 dinerId = 3;
string openId = 4;
string templateId = 5;
int64 orderPeriodStartDate = 6;
string jumpPath = 7;
int64 orderPeriodEndDate = 8;
int64 createdAt = 9;
}
message QueryPendingOrderSubscriptionMessagesRpcResponse {
repeated PendingOrderSubscriptionMessageRpcResponse responses = 1;
}
......