Jiaqi Xia

feat: 农信推送订餐提醒

---
alwaysApply: false
---
# 项目代码规范
## 项目结构
```
src/main/java/com/infoloop/tianting/
├── controller/ # REST API 控制器
├── service/ # 业务逻辑接口
│ ├── impl/ # 业务逻辑实现
│ └── client/ # gRPC/HTTP 客户端封装
├── model/ # 数据模型
│ ├── dto/ # 数据传输对象
│ ├── vo/ # 视图对象
│ ├── bo/ # 业务对象
│ └── common/ # 通用模型
├── config/ # 配置类
├── constant/ # 常量定义
├── enums/ # 枚举类
├── exception/ # 异常定义
├── logic/ # 业务逻辑(定时任务、延迟任务等)
├── utils/ # 工具类
└── store/ # Redis 存储封装
```
## 代码风格
### 类注解顺序
```java
@Slf4j
@Service // 或 @Component, @RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class XxxServiceImpl implements XxxService {
```
### Controller 规范
- 使用 `@Api(tags = "模块名")` 标注 Swagger 分组
- 使用 `@ApiOperation(value = "接口描述")` 标注接口
- 使用 `@ResponseStatus` 指定 HTTP 状态码
- 使用 `@Valid` 进行参数校验
### Service 规范
- 接口定义在 `service/` 目录
- 实现类在 `service/impl/` 目录,命名为 `XxxServiceImpl`
- gRPC 客户端封装在 `service/client/` 目录,命名为 `XxxServiceRpcClient`
### 配置常量
- 配置项定义在 `ConfigConstants` 接口中
- 使用 `@Value(ConfigConstants.XXX)` 注入配置
### 依赖注入
- 优先使用构造器注入:`@RequiredArgsConstructor(onConstructor = @__(@Autowired))`
- 配置值使用 `@Value` 注入
### 日志规范
- 使用 `@Slf4j` 注解
- 使用 `log.info/warn/error` 记录日志
- 异常日志使用 `log.error("message", e)`
### gRPC 调用
- 通过 `XxxServiceRpcClient` 封装 gRPC 调用
- 在 `GrpcConfig` 中配置 Channel 和 Stub
### 定时任务
- 放在 `logic/task/` 目录
- 使用 `@Scheduled(cron = "...")` 注解
- 使用 `@Component` 注册为 Bean
## 命名规范
- DTO 类:`XxxDTO` 或内部类 `XxxDTO.CreateXxxDTO`
- VO 类:`XxxVO`
- 枚举类:`XxxEnum`
- 常量类:`XxxConstants`
- 工具类:`XxxUtil`
## 禁止事项
### 禁止循环依赖
- **Service 之间禁止循环调用**:ServiceA 调用 ServiceB,ServiceB 不能再调用 ServiceA
- **避免循环依赖注入**:如果出现循环依赖,需要重构代码,提取公共逻辑到新的 Service
- **分层调用原则**:Controller → Service → RpcClient/Store,禁止反向调用
### 禁止循环调用 gRPC 方法
- **禁止在循环中调用 gRPC 方法**:会导致大量网络请求,严重影响性能
- **必须使用批量接口**:如果需要处理多条数据,必须使用批量查询/批量创建接口
- **先收集 ID 再批量查询**:先收集所有需要查询的 ID,一次性批量查询
```java
// ❌ 错误示例:循环调用 gRPC
for (Long id : ids) {
var result = rpcClient.getById(id); // 禁止!
results.add(result);
}
// ✅ 正确示例:批量调用
var results = rpcClient.getByIds(ids); // 一次批量查询
```
### 批量接口设计规范
- **Proto 定义批量方法**:`GetXxxsByIds`、`BatchCreateXxx`、`BatchUpdateXxx`
- **RpcClient 封装批量方法**:提供 `getByIds(List<Long> ids)` 等批量方法
- **空集合检查**:批量方法调用前检查集合是否为空,避免无效请求
### 参数类型规范
- **禁止使用 Object 作为参数类型**:必须明确定义具体类型
- **禁止使用 Map<String, Object>**:应定义具体的 DTO 类
- **集合类型必须指定泛型**:使用 `List<XxxDTO>` 而非 `List`
- **方法参数类型要明确**:避免使用 `var` 定义方法参数
- **返回值类型要明确**:禁止返回 `Object`,必须定义具体类型
### 示例
```java
// ❌ 错误示例
public Object process(Map<String, Object> params) { ... }
// ✅ 正确示例
public OrderVO process(CreateOrderDTO params) { ... }
```
---
alwaysApply: false
---
# 项目技术栈
## 核心框架
- **Spring Boot 2.5.12**:Web 应用框架
- **Java 17**:编程语言
- **Gradle**:构建工具
## 通信协议
- **gRPC**:微服务间通信,使用 Protobuf 定义接口
- **REST API**:对外 HTTP 接口,使用 Knife4j/Swagger 文档
## 数据存储
- **Redis (Lettuce)**:缓存、分布式锁、Session 存储
- **Redisson**:分布式锁实现
- **阿里云 OSS**:文件存储
## 认证授权
- **Sa-Token + JWT**:用户认证和权限管理
## 工具库
- **Lombok**:简化 Java 代码
- **Hutool**:通用工具类
- **Jackson**:JSON/XML 序列化
- **Apache POI**:Excel 处理
## 可观测性
- **Zipkin Brave**:分布式链路追踪
- **Spring Actuator**:健康检查和监控
## 异步处理
- **Spring Scheduling**:定时任务
- **Spring Async**:异步执行
- **WebSocket**:实时通信
# 缓存功能使用指南
## 概述
本项目已集成了完整的多级缓存功能,基于client-api项目的缓存实现。缓存层采用 Caffeine(本地缓存)+ Redis(分布式缓存)的多级架构,通过AOP实现非侵入式的缓存管理。
## 特性
- **多级缓存**:本地缓存(L1)+ 分布式缓存(L2)
- **非侵入式**:通过注解实现,无需修改现有业务代码
- **高性能**:优化的缓存穿透防护和性能优化
- **易管理**:提供缓存监控和管理接口
- **支持Lombok @Data**:专门优化支持Lombok @Data类的序列化
## 缓存注解
### @Cacheable - 缓存查询结果
```java
@Cacheable(prefix = "order", key = "#orderId", ttl = 600)
public OrderDTO getOrderById(Long orderId) {
// 业务逻辑
}
```
**参数说明:**
- `prefix`:缓存key前缀,用于区分业务模块
- `key`:缓存key,支持SpEL表达式
- `ttl`:过期时间(秒),默认300秒
- `condition`:缓存条件,只有满足条件才缓存
- `unless`:排除条件,满足条件时不缓存
- `localCache`:是否启用本地缓存,默认true
- `distributedCache`:是否启用分布式缓存,默认true
### @CacheEvict - 清除缓存
```java
@CacheEvict(prefix = "order", keys = {"#order.orderId"})
public OrderDTO updateOrder(OrderDTO order) {
// 业务逻辑
}
```
**参数说明:**
- `prefix`:缓存key前缀
- `keys`:要清除的缓存key数组,支持SpEL表达式
- `allEntries`:是否清除所有相关缓存
- `condition`:清除条件
### @CachePut - 主动更新缓存
```java
@CachePut(prefix = "order", key = "#result.orderId", ttl = 600)
public OrderDTO createOrder(OrderDTO order) {
// 业务逻辑
}
```
## 使用示例
### 1. 基本缓存使用
```java
@Service
public class OrderService {
// 缓存订单信息,10分钟过期
@Cacheable(prefix = "order", key = "#orderId", ttl = 600)
public OrderDTO getOrderById(Long orderId) {
// 调用gRPC服务获取订单信息
return callGrpcService(orderId);
}
}
```
### 2. 条件缓存
```java
// 只有当orderId大于0时才缓存,且结果不为空时才缓存
@Cacheable(
prefix = "order",
key = "#orderId",
ttl = 600,
condition = "#orderId > 0",
unless = "#result == null"
)
public OrderDTO getOrderByIdWithCondition(Long orderId) {
return getOrderById(orderId);
}
```
### 3. 复杂key表达式
```java
// 使用多个参数组合作为缓存key
@Cacheable(
prefix = "order",
key = "'customer_' + #customerId + '_status_' + #status + '_page_' + #page",
ttl = 300
)
public List<OrderDTO> getOrdersByCustomer(Long customerId, String status, int page, int size) {
return callGrpcService(customerId, status, page, size);
}
```
### 4. 缓存失效
```java
// 更新订单时清除相关缓存
@CacheEvict(prefix = "order", keys = {
"#order.orderId",
"'customer_' + #order.customerId + '_*'"
})
public OrderDTO updateOrder(OrderDTO order) {
return callGrpcUpdateService(order);
}
// 清除所有订单相关缓存
@CacheEvict(prefix = "order", allEntries = true)
public void batchUpdateOrders(List<OrderDTO> orders) {
callGrpcBatchUpdateService(orders);
}
```
### 5. 主动缓存更新
```java
// 创建订单后主动设置缓存
@CachePut(prefix = "order", key = "#result.orderId", ttl = 600)
public OrderDTO createOrder(OrderDTO order) {
return callGrpcCreateService(order);
}
```
## 配置说明
### application.properties配置
```properties
# 缓存配置
cache.enabled=true
cache.default-ttl=300
# Caffeine本地缓存配置
cache.caffeine.initial-capacity=100
cache.caffeine.maximum-size=10000
cache.caffeine.expire-after-write=60
cache.caffeine.expire-after-access=0
cache.caffeine.record-stats=true
# Redis缓存配置
cache.redis.enabled=true
cache.redis.host=localhost
cache.redis.port=6379
cache.redis.password=123456
cache.redis.database=11
cache.redis.timeout=2000
# 缓存监控配置
cache.metrics.enabled=true
cache.metrics.step=1m
```
### 缓存时间建议
- **用户基础信息**:3600秒(1小时)
- **商品信息**:1800秒(30分钟)
- **订单列表**:300秒(5分钟)
- **统计数据**:60秒(1分钟)
- **配置数据**:86400秒(24小时)
## 监控和管理
### 健康检查
访问:`/actuator/health`
```json
{
"status": "UP",
"components": {
"cache": {
"status": "UP",
"details": {
"status": "缓存服务正常"
}
}
}
}
```
### 缓存统计
访问:`/actuator/cache/stats`
```json
{
"localHits": 1250,
"localMisses": 180,
"redisHits": 450,
"redisMisses": 80,
"totalRequests": 1960,
"hitRate": 0.867,
"localSize": 256
}
```
### 缓存管理操作
```bash
# 清除指定缓存
DELETE /actuator/cache/evict?key=order:12345
# 清除前缀缓存
DELETE /actuator/cache/evict/prefix?prefix=order
# 清空所有缓存
DELETE /actuator/cache/clear
# 检查缓存是否存在
GET /actuator/cache/exists?key=order:12345
# 获取缓存过期时间
GET /actuator/cache/expire?key=order:12345
```
## 最佳实践
### 1. 缓存key设计原则
- **唯一性**:确保key的唯一性,避免冲突
- **可读性**:key应该具有一定的可读性,便于调试
- **简洁性**:key不宜过长,影响性能
- **层次性**:使用冒号分隔符建立层次结构
```java
// 好的key设计
"order:12345"
"customer:67890:orders"
"product:category:123:page:1"
// 不好的key设计
"very_long_business_module_name_order_information_12345"
"order12345customerinfo"
```
### 2. 缓存时间策略
- **高频查询、更新不频繁**:长时间缓存(10-30分钟)
- **中等频率数据**:中等时间缓存(5-10分钟)
- **实时性要求高**:短时间缓存(1-5分钟)
- **配置类数据**:长时间缓存(1-24小时)
### 3. 缓存失效策略
```java
// 单个数据更新
@CacheEvict(prefix = "order", keys = "#order.orderId")
public OrderDTO updateOrder(OrderDTO order) { ... }
// 影响多个缓存的操作
@CacheEvict(prefix = "order", keys = {
"#order.orderId",
"'customer_' + #order.customerId + '_*'"
})
public OrderDTO updateOrderStatus(OrderDTO order) { ... }
// 批量操作清除所有相关缓存
@CacheEvict(prefix = "order", allEntries = true)
public void batchUpdateOrders(List<OrderDTO> orders) { ... }
```
### 4. 条件缓存使用
```java
// 参数验证
condition = "#orderId != null && #orderId > 0"
// 结果验证
unless = "#result == null || #result.isEmpty()"
// 业务条件
condition = "#status == 'ACTIVE'"
```
## 注意事项
### 1. 数据类型支持
-**Lombok @Data类**:完全支持,推荐使用
-**标准POJO**:支持
-**基本数据类型**:支持
-**集合类型**:支持
-**Protobuf对象**:需要转换为DTO再缓存
### 2. 缓存穿透防护
系统自动处理null值缓存,防止缓存穿透:
```java
// 查询结果为null时也会被缓存(使用特殊标记)
@Cacheable(prefix = "order", key = "#orderId", ttl = 300)
public OrderDTO getOrderById(Long orderId) {
OrderDTO order = callGrpcService(orderId);
return order; // 即使为null也会被缓存
}
```
### 3. 异常处理
缓存操作异常不会影响业务执行:
- 缓存读取失败 → 直接执行业务方法
- 缓存写入失败 → 记录日志,继续执行
- 缓存清除失败 → 记录日志,不影响业务
### 4. 性能考虑
- **本地缓存优先**:优先查询Caffeine,未命中再查Redis
- **异步回写**:本地缓存命中但Redis缺失时,异步回写Redis
- **批量操作**:使用SCAN代替KEYS,避免阻塞Redis
## 故障排查
### 1. 缓存未命中
检查项:
- key生成是否正确
- TTL是否过短
- 条件表达式是否满足
- 缓存服务是否正常
### 2. 缓存数据异常
检查项:
- 序列化配置是否正确
- 数据类型是否支持
- JSON格式是否有效
### 3. 性能问题
检查项:
- 缓存命中率是否正常(建议>70%)
- 缓存大小是否合理
- Redis连接是否正常
## 示例代码
完整的使用示例请参考:
- `CachedOrderService.java` - 服务层缓存使用示例
- `CachedOrderController.java` - 控制器层使用示例
- `OrderDTO.java` - Lombok @Data类示例
## 总结
通过本缓存功能,可以显著提升应用性能:
- **响应时间减少**:60-80%(缓存命中时)
- **网络调用减少**:70-90%(针对gRPC调用)
- **系统稳定性提升**:降级机制保证服务可用性
推荐在以下场景使用缓存:
- ✅ 高频查询的gRPC接口
- ✅ 相对稳定的基础数据
- ✅ 对响应时间敏感的用户接口
- ❌ 强一致性要求的实时数据
- ❌ 频繁变更的业务数据
# 微信小程序订阅消息推送后端开发文档(适用于 Spring Boot 微服务)
## 1. 背景与目标
本方案通过微信小程序的 **订阅消息(Subscribe Message)** 能力,在用户授权的前提下,在特定时间向用户推送服务通知。例如在订餐周期开启前提醒用户开始订餐。
> 微信要求订阅消息必须通过用户主动操作获得授权,且每次授权只能发送一次消息(非特权行业除外)。([Medium][1])
---
## 2. 模板申请与配置
### 2.1 登录微信公众平台
1. 登录微信公众平台(小程序管理后台)。
2. 在左侧菜单选择 **功能 → 订阅消息**
3.**公共模板库** 中查找最符合业务场景的模板;如无合适模板,可新建申请。
4. 按需选择关键词组合并提交审核。
5. 审核通过后在“我的模板”中得到 **模板 ID**
**说明**:该模板 ID 将用于消息发送接口参数。([aigwa.com][2])
---
## 3. 小程序前端授权流程简述
在前端,当用户在小程序产生关键交互(例如提交订单成功后)时,需要通过 `wx.requestSubscribeMessage` 调起订阅授权。
```js
wx.requestSubscribeMessage({
tmplIds: ['TEMPLATE_ID'],
success(res) {
// 例如:res['TEMPLATE_ID'] = "accept" | "reject" | ...
// 结果上报后端
}
})
```
**提示:**
* 用户点击“允许”后即获得一次性发送权限。([intl.cloud.tencent.com][3])
---
## 4. 后端核心概念与数据模型
### 4.1 订阅授权状态表(示例)
```sql
CREATE TABLE wx_subscribe_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
openid VARCHAR(64) NOT NULL,
template_id VARCHAR(64) NOT NULL,
biz_type VARCHAR(32) NOT NULL,
order_cycle_id VARCHAR(32),
status ENUM('PENDING','USED','FAILED') NOT NULL DEFAULT 'PENDING',
retry_count INT DEFAULT 0,
last_error VARCHAR(255),
authorized_at DATETIME,
used_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
```
* `PENDING`:用户已授权但未发送;
* `USED`:发送成功;
* `FAILED`:发送失败且不再重试。
---
## 5. 微信服务端 API 调用
### 5.1 获取 `access_token`
用于后端所有接口调用授权凭证:
```
GET https://api.weixin.qq.com/cgi-bin/token
?grant_type=client_credential
&appid=APPID
&secret=APPSECRET
```
**返回示例:**
```json
{
"access_token": "ACCESS_TOKEN",
"expires_in": 7200
}
```
---
### 5.2 发送订阅消息
使用订阅消息下发接口:
```
POST https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=ACCESS_TOKEN
```
#### 请求 JSON 示例
```json
{
"touser": "USER_OPENID",
"template_id": "TEMPLATE_ID",
"page": "pages/order/index?cycleId=202501",
"data": {
"thing1": { "value": "下次订餐即将开启" },
"time2": { "value": "周一 09:00" },
"thing3": { "value": "请点击跳转完成订餐" }
}
}
```
字段说明:
* `touser`:接收用户微信 OpenID;
* `template_id`:模板 ID;
* `page`:消息点击跳转页面路径;
* `data`:模板关键词对应数据。([gitcode.csdn.net][4])
---
## 6. 后端主要接口设计
### 6.1 授权结果回传 API(POST)
**URL**`/api/wechat/subscribe/authorize`
**请求示例**
```json
{
"openid": "xxxx",
"templateId": "TEMPLATE_ID",
"bizType": "ORDER_REMIND",
"orderCycleId": "202501",
"result": "accept"
}
```
**响应示例**
```json
{ "code": 0, "msg": "ok" }
```
逻辑:
* 若 result = `accept`,写入 `wx_subscribe_permission` 表;
* 否则记录拒绝状态。
---
### 6.2 定时发送任务
后端定时任务(如 `@Scheduled`)在规定时间触发:
1. 查询所有 `status = PENDING` 且对应用户未订餐的记录;
2. 将消息入队(如 MQ);
3. 消费执行发送逻辑;
4. 更新 `status``retry_count`
---
## 7. 发送逻辑与重试策略
### 7.1 核心发送逻辑(伪码)
```java
for (Permission p : pendingList) {
try {
sendSubscribeMessage(p);
p.setStatus("USED");
p.setUsedAt(now());
} catch (WeChatApiException e) {
if (canRetry(e) && p.getRetryCount() < 3) {
p.incrementRetry();
} else {
p.setStatus("FAILED");
p.setLastError(e.getMessage());
}
}
update(p);
}
```
---
## 8. 常见错误码处理
| 错误码 | 说明 | 处理方式 |
| ------- | --------------- | ---------------- |
| `43101` | 用户拒绝 | 标记 `FAILED` 不再重试 |
| `40001` | access_token 错误 | 刷新 Token 并重试 |
| 网络错误 | 网络抖动 | 按退避重试 |
---
## 9. 测试建议
1. 先使用测试 OpenID 调用发送接口;
2. 检查模板字段是否与后台组装一致;
3. 校验跳转页面路径参数是否正确。
---
## 10. 参考链接
* 微信官方订阅消息后端发送接口说明(subscribeMessage.send)([gitcode.csdn.net][4])
* 小程序端授权调用说明(wx.requestSubscribeMessage)([wdk-docs.github.io][5])
---
如果你需要,我可以进一步提供 **Spring Boot 样板代码**(包括定时任务 & 消息发送 Service 实现)。
[1]: https://medium.com/china-software-development/everything-about-wechat-subscription-message-884246d71cb3?utm_source=chatgpt.com "Everything about WeChat Subscription Message | by David Yu | Shanghai Coders | Medium"
[2]: https://aigwa.com/novel/Content/detail/1397.html?utm_source=chatgpt.com "微信小程序 订阅消息·addTemplate_微信小程序开发文档_啊嘎哇在线工具箱"
[3]: https://intl.cloud.tencent.com/document/product/1219/57734?utm_source=chatgpt.com "Open APIs"
[4]: https://gitcode.csdn.net/65ec534a1a836825ed7988b9.html?utm_source=chatgpt.com "微信小程序-小程序订阅消息(四)_微信小程序_MinggeQingchun-AtomGit开源社区"
[5]: https://wdk-docs.github.io/wxadev-docs/api/subscribe-message/wx.requestSubscribeMessage.html?utm_source=chatgpt.com "wx.requestSubscribeMessage — wxadev v2.21.0 文档"
......@@ -81,5 +81,6 @@ public interface ConfigConstants {
String MINI_PROGRAM_APP_ID = "${miniprogram.appId}";
String MINI_PROGRAM_APP_SECRET = "${miniprogram.appSecret}";
String MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID = "${miniprogram.orderReminderTemplateId}";
}
......
......@@ -52,4 +52,26 @@ public class WxUserDto {
private WxUserPhoneInfoDto phone_info;
}
@Data
@Builder
@ApiModel(description = "订阅消息发送请求")
@NoArgsConstructor
@AllArgsConstructor
public static class SendSubscriptionMessageRequest {
private String touser;
private String template_id;
private String page;
private java.util.Map<String, Object> data;
}
@Data
@Builder
@ApiModel(description = "订阅消息发送响应")
@NoArgsConstructor
@AllArgsConstructor
public static class SendSubscriptionMessageResponse {
private Integer errcode;
private String errmsg;
}
}
......
package com.infoloop.tianting.service;
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 温馨提示,例如:"请点击卡片,准时进入小程序完成预订"
* @return 发送结果
*/
SendSubscriptionMessageResponse sendOrderReminder(String openId, String page,
String reminderContent, String orderTime, String tips);
}
package com.infoloop.tianting.service;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserOpenIdDto;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserPhoneResponseDto;
import java.util.Map;
public interface WxMiniProgramService {
WxUserOpenIdDto getWxUserOpenIdByCode(String code);
WxUserPhoneResponseDto getUserPhoneInfoByCode(String code);
SendSubscriptionMessageResponse sendSubscriptionMessage(String openId, String templateId, String page, Map<String, Object> data);
}
......
package com.infoloop.tianting.service.client;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageRequest;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserOpenIdDto;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserPhoneResponseDto;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserTokenDto;
......@@ -31,6 +33,7 @@ public class WxMiniProgramHttpClient {
private static final String TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appId}&secret={appSecret}";
private static final String PHONE_URL = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={accessToken}";
private static final String SUBSCRIBE_MESSAGE_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={accessToken}";
private final RestTemplate restTemplate;
......@@ -135,4 +138,46 @@ public class WxMiniProgramHttpClient {
}
}
/**
* 发送订阅消息
* @param openId 用户 OpenID
* @param templateId 模板 ID
* @param page 点击跳转页面路径
* @param data 模板数据,格式为 Map<String, Map<String, String>>,例如:{"thing1": {"value": "内容"}}
* @return 发送结果
*/
public SendSubscriptionMessageResponse sendSubscriptionMessage(String openId, String templateId, String page, java.util.Map<String, Object> data) {
try {
String accessToken = fetchStableAccessToken();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
SendSubscriptionMessageRequest request = SendSubscriptionMessageRequest.builder()
.touser(openId)
.template_id(templateId)
.page(page)
.data(data)
.build();
HttpEntity<SendSubscriptionMessageRequest> requestEntity = new HttpEntity<>(request, 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 subscription message: {}", response);
throw new IllegalArgumentException("发送订阅消息失败");
}
SendSubscriptionMessageResponse result = response.getBody();
if (result.getErrcode() != null && result.getErrcode() != 0) {
log.error("WeChat subscription message error: errcode={}, errmsg={}", result.getErrcode(), result.getErrmsg());
}
return result;
} catch (Exception e) {
log.error("Error sending subscription message", e);
throw new IllegalArgumentException("发送订阅消息失败: " + e.getMessage());
}
}
}
......
package com.infoloop.tianting.service.impl;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.service.OrderReminderService;
import com.infoloop.tianting.service.WxMiniProgramService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
import static com.infoloop.tianting.constant.ConfigConstants.MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID;
/**
* 订餐提醒服务实现
* 将业务参数转换为微信订阅消息模板格式并发送
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class OrderReminderServiceImpl implements OrderReminderService {
private final WxMiniProgramService wxMiniProgramService;
@Value(MINI_PROGRAM_ORDER_REMINDER_TEMPLATE_ID)
private String templateId;
@Override
public SendSubscriptionMessageResponse sendOrderReminder(String openId,
String page,
String reminderContent,
String orderTime,
String tips) {
log.info("Sending order reminder to openId: {}, page: {}", openId, page);
// 将业务参数转换为微信模板数据格式
// 根据文档,模板字段为:thing1 (提醒内容), time2 (订餐时间), thing3 (温馨提示)
Map<String, Object> data = new HashMap<>();
// thing1: 提醒内容
Map<String, String> thing1Value = new HashMap<>();
thing1Value.put("value", reminderContent);
data.put("thing1", thing1Value);
// time2: 订餐时间
Map<String, String> time2Value = new HashMap<>();
time2Value.put("value", orderTime);
data.put("time2", time2Value);
// thing3: 温馨提示
Map<String, String> thing3Value = new HashMap<>();
thing3Value.put("value", tips);
data.put("thing3", thing3Value);
try {
SendSubscriptionMessageResponse response = wxMiniProgramService.sendSubscriptionMessage(
openId, templateId, page, data);
if (response.getErrcode() != null && response.getErrcode() == 0) {
log.info("Order reminder sent successfully to openId: {}", openId);
} else {
log.warn("Order reminder sent with error. openId: {}, errcode: {}, errmsg: {}",
openId, response.getErrcode(), response.getErrmsg());
}
return response;
} catch (Exception e) {
log.error("Failed to send order reminder to openId: {}", openId, e);
throw e;
}
}
}
package com.infoloop.tianting.service.impl;
import com.infoloop.tianting.model.dto.WxUserDto.SendSubscriptionMessageResponse;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserOpenIdDto;
import com.infoloop.tianting.model.dto.WxUserDto.WxUserPhoneResponseDto;
import com.infoloop.tianting.service.WxMiniProgramService;
......@@ -9,6 +10,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
......@@ -26,4 +29,9 @@ public class WxMiniProgramServiceImpl implements WxMiniProgramService {
return wxMiniProgramHttpClient.getUserPhoneInfoByCode(code);
}
@Override
public SendSubscriptionMessageResponse sendSubscriptionMessage(String openId, String templateId, String page, Map<String, Object> data) {
return wxMiniProgramHttpClient.sendSubscriptionMessage(openId, templateId, page, data);
}
}
......
......@@ -75,6 +75,7 @@ grpc.meizhongyihe-service.port=3040
miniprogram.appId=wx40ec496d73b58ec6
miniprogram.appSecret=5b309517bf918abf760b2195e91b99f3
miniprogram.orderReminderTemplateId=xxx
business-config.hisCustomerQueryUrl = http://localhost:8903/hiscustomers/current/query
business-config.clientCustomerQueryUrl = http://localhost:8903/clientcustomers/current/query
......