文章目录
- 前言
- 1. 项目简介
- 1.1 什么是微信模板消息?
- 1.2 本项目实现了什么功能?
- 1.3 技术栈
- 2. 项目结构
- 3. 核心功能实现
- 3.1 access_token 自动管理
- 3.2 模板消息发送
- 3.3 配置文件
- 4. 踩坑经验分享
- 4.1 测试公众号 vs 正式公众号
- openId 可以这里取(也可以通过code获取):
- 消息模版id 这里取
- 4.2 openid 注意事项
- 4.3 关于 .gitignore
- 5. 完整代码获取
- 6. 单元测试与集成测试实战
- 6.1 什么是单元测试?
- 6.2 什么是集成测试?
- 6.3 我们项目的测试架构
- 6.4 WxPushServiceTest(单元测试)详解
- 6.5 WxPushApplicationTests(集成测试)详解
- 6.6 如何选择什么时候用什么测试?
- 7. 总结
前言
1. 项目简介
微信公众号模板消息推送服务Demo
1.1 什么是微信模板消息?
微信模板消息是微信提供给公众号的一种消息推送能力,允许公众号主动向关注者发送符合特定模板的消息,比如订单通知、物流提醒、账户变动等。
1.2 本项目实现了什么功能?
- ✅access_token 自动获取和刷新(有效期7200秒,提前10分钟刷新)
- ✅模板消息发送(支持自定义数据和颜色)
- ✅获取关注者列表(通过
/wx/users接口) - ✅OAuth2 授权回调(通过 code 获取 openid)
1.3 技术栈
| 技术 | 用途 |
|---|---|
| Java 1.8 | 开发语言 |
| Spring Boot 2.7.18 | 应用框架 |
| OkHttp 4.12.0 | HTTP 客户端 |
| Lombok | 简化代码 |
| Maven | 构建工具 |
2. 项目结构
wx-push-demo/├── pom.xml ├── src/│ └── main/│ ├── java/com/example/wxpush/│ │ ├── WxPushApplication.java # 启动类 │ │ ├── config/│ │ │ └── WxMpConfig.java # 微信配置 │ │ ├── dto/│ │ │ ├── AccessTokenResponse.java # access_token 响应 │ │ │ ├── TemplateDataItem.java # 模板数据项 │ │ │ ├── TemplateMessage.java # 模板消息请求 │ │ │ └── WxBaseResponse.java # 微信通用响应 │ │ ├── service/│ │ │ ├── AccessTokenService.java # access_token 管理 │ │ │ └── WxPushService.java # 模板消息发送 │ │ └── controller/│ │ └── WxUserController.java # 关注者/授权接口 │ └── resources/│ └── application.yml # 配置文件3. 核心功能实现
3.1 access_token 自动管理
微信的 access_token 有效期 7200 秒(2小时),我们需要:
- 启动时立即获取
- 缓存到内存
- 定时刷新(提前10分钟)
- 双重检查锁保证线程安全
关键代码:
@Slf4j@ServicepublicclassAccessTokenService{@AutowiredprivateWxMpConfigwxMpConfig;privatevolatileStringaccessToken;privatevolatilelongexpireTime=0;privateScheduledExecutorServicescheduler;@PostConstructpublicvoidinit(){refreshToken();scheduler=Executors.newSingleThreadScheduledExecutor(r->{Threadt=newThread(r,"wx-token-refresh");t.setDaemon(true);returnt;});scheduler.scheduleAtFixedRate(this::refreshToken,110,110,TimeUnit.MINUTES);}publicStringgetAccessToken(){if(System.currentTimeMillis()>expireTime-5*60*1000){synchronized(this){if(System.currentTimeMillis()>expireTime-5*60*1000){refreshToken();}}}returnaccessToken;}privatevoidrefreshToken(){try{Stringurl=String.format(WxMpConfig.TOKEN_URL,wxMpConfig.getAppId(),wxMpConfig.getAppSecret());Requestrequest=newRequest.Builder().url(url).get().build();try(Responseresponse=newOkHttpClient().newCall(request).execute()){Stringbody=response.body().string();AccessTokenResponsetokenResp=newObjectMapper().readValue(body,AccessTokenResponse.class);if(tokenResp.isSuccess()){this.accessToken=tokenResp.getAccessToken();this.expireTime=System.currentTimeMillis()+tokenResp.getExpiresIn()*1000L;log.info("access_token 刷新成功");}}}catch(IOExceptione){log.error("刷新 access_token 异常",e);}}}3.2 模板消息发送
发送模板消息的步骤:
- 构造模板数据
- 获取有效的 access_token
- 调用微信 API
关键代码:
@Slf4j@ServicepublicclassWxPushService{@AutowiredprivateWxMpConfigwxMpConfig;@AutowiredprivateAccessTokenServiceaccessTokenService;publicbooleansendTemplateMessage(StringopenId,Map<String,TemplateDataItem>data,Stringurl){returnsendTemplateMessage(openId,wxMpConfig.getTemplateId(),data,url,null);}publicbooleansendTemplateMessage(StringopenId,StringtemplateId,Map<String,TemplateDataItem>data,Stringurl,TemplateMessage.MiniProgramminiProgram){TemplateMessagemessage=TemplateMessage.builder().toUser(openId).templateId(templateId).url(url).miniProgram(miniProgram).data(data).build();Stringtoken=accessTokenService.getAccessToken();if(token==null||token.isEmpty()){log.error("发送模板消息失败: access_token 为空");returnfalse;}StringapiUrl=String.format(WxMpConfig.TEMPLATE_SEND_URL,token);try{StringjsonBody=newObjectMapper().writeValueAsString(message);log.info("发送模板消息, openId: {}",openId);Requestrequest=newRequest.Builder().url(apiUrl).post(RequestBody.create(jsonBody,MediaType.get("application/json; charset=utf-8"))).build();try(Responseresponse=newOkHttpClient().newCall(request).execute()){Stringbody=response.body().string();WxBaseResponsewxResp=newObjectMapper().readValue(body,WxBaseResponse.class);if(wxResp.isSuccess()){log.info("模板消息发送成功");returntrue;}else{log.error("模板消息发送失败: {}",wxResp.getErrMsg());returnfalse;}}}catch(IOExceptione){log.error("发送模板消息异常",e);returnfalse;}}}3.3 配置文件
server:port:8081# 微信公众号配置wx:mp:app-id:你的AppIDapp-secret:你的AppSecrettemplate-id:你的模板IDlogging:level:com.example.wxpush:DEBUG4. 踩坑经验分享
4.1 测试公众号 vs 正式公众号
| 特性 | 测试公众号 | 正式公众号 |
|---|---|---|
first、remark占位符 | ❌ 不支持 | ✅ 支持 |
keyword1~keywordN | ✅ 支持 | ✅ 支持 |
| 正式使用 | ❌ 仅测试 | ✅ 推荐 |
测试公众号平台,没有的可以申请一个:
https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
openId 可以这里取(也可以通过code获取):
消息模版id 这里取
4.2 openid 注意事项
- 每个公众号的 openid 不同:同一用户在不同公众号的 openid 不一样
- 测试号 openid:通过测试号二维码关注后,用
/wx/users接口获取 - 用户必须关注:发送模板消息前,用户必须已关注该公众号
4.3 关于 .gitignore
一定要配置.gitignore,忽略以下文件:
target/ .idea/ *.log application-local.yml5. 完整代码获取
项目已完整实现,所有代码都在:
[wx-push-demo 项目地址]
6. 单元测试与集成测试实战
现在我们的项目有完整的测试覆盖了!这是非常重要的一步。
6.1 什么是单元测试?
单元测试是对代码中最小可测试单元的测试,目的是验证这些单元在隔离状态下是否正常工作。
| 特点:
- ❌ **不调用真实接口和依赖
- ⚡运行速度快
- 🎯只测单个类或方法
- ✅ **使用 Mock 模拟外部依赖
6.2 什么是集成测试?
集成测试是验证多个模块或整个系统一起工作是否正常。
| 特点:
- ✅ **调用真实接口和依赖
- 🐢运行速度较慢
- 🌐启动完整 Spring 容器
- 🎯测试系统集成效果
6.3 我们项目的测试架构
| 测试类型 | 测试类 | 测试内容 |
|---|---|---|
| 单元测试 | WxPushServiceTest | 参数校验、核心业务逻辑 |
| 单元测试 | AccessTokenServiceTest | 组件加载验证 |
| 集成测试 | WxPushApplicationTests | 完整流程、真实微信 API |
6.4 WxPushServiceTest(单元测试)详解
我们用 Mockito 模拟依赖,只测试 WxPushService 自身逻辑:
@ExtendWith(MockitoExtension.class)classWxPushServiceTest{@MockprivateWxMpConfigwxMpConfig;@MockprivateAccessTokenServiceaccessTokenService;@InjectMocksprivateWxPushServicewxPushService;@TestvoidsendTemplateMessage_WhenTokenIsNull_ShouldReturnFalse(){when(accessTokenService.getAccessToken()).thenReturn(null);booleanresult=wxPushService.sendTemplateMessage("test-openid",testData,null);assertFalse(result);}}**测试了以下边界情况:
- token 为空
- openId 为空
- data 为 null
- data 为空 Map
6.5 WxPushApplicationTests(集成测试)详解
使用 @SpringBootTest 注解启动完整 Spring 上下文,调用真实接口:
@SpringBootTestclassWxPushApplicationTests{@AutowiredprivateWxPushServicewxPushService;@TestvoidcontextLoads(){assertNotNull(wxPushService);}@TestvoidsendTemplateMessage(){StringopenId="oiAR-xxxxx";Map<String,TemplateDataItem>data=newHashMap<>();data.put("keyword1",TemplateDataItem.of("测试内容1"));data.put("keyword2",TemplateDataItem.of("测试内容2"));booleanresult=wxPushService.sendTemplateMessage(openId,data,null);System.out.println("发送结果: "+(result?"成功":"失败");}}6.6 如何选择什么时候用什么测试?
| 场景 | 推荐测试类型 |
|---|---|
| 日常开发、写代码时 | 单元测试(速度快,反馈及时) |
| 提交代码前、发布前 | 所有测试(确保集成没问题) |
| 调试微信接口时 | 单独运行集成测试 |
7. 总结
通过这个项目,我们学习了:
- ✅ Spring Boot 项目搭建
- ✅ 微信公众平台 API 调用
- ✅ access_token 的管理和刷新
- ✅ 模板消息发送
- ✅ Maven 项目测试和构建
- ✅ **单元测试与 Mockito 使用
- ✅ **集成测试与 @SpringBootTest
- ✅ **测试最佳实践
希望这篇博客对你有帮助!🎉