前言
本文完整演示如何在 Spring Boot + Spring AI MCP(Model Context Protocol)框架下,接入微信公众号模板消息通知能力,并将其以“AI 工具”MCP服务的形式对外提供,供 IDE/智能体直接调用。你将看到从架构分层、代码实现、配置运行到 MCP 客户端集成的全链路实践。
目标读者
- 需要在业务中实现“通过微信公众号发送通知”的后端开发
- 希望将传统 HTTP 能力封装为 AI 工具(MCP)并在 IDE/Agent 中一键调用的开发者
- 关注 DDD 分层与可维护性架构实践的工程师
核心能力一览
- 微信模板消息发送:构造模板数据、调用微信接口推送
- AccessToken 缓存:Guava Cache 本地缓存,避免频繁获取
- MCP 工具暴露:用
@Tool将能力注册为模型可调用工具 - DDD 分层:领域服务/端口、基础设施适配器、网关隔离
架构设计与目录概览
采用 DDD 分层,清晰划分领域与基础设施职责:
src/main/java/org/cheese/mcp/server/weixin/
├── McpServerApplication.java # 启动类与Bean装配
├── domain/ # 领域层
│ ├── adapter/IWeiXiPort.java # 端口接口(面向领域)
│ ├── model/
│ │ ├── WeiXinNoticeFunctionRequest.java
│ │ └── WeiXinNoticeFunctionResponse.java
│ └── service/WeiXinNoticeService.java# 领域服务,暴露@Tool
├── infrastructure/ # 基础设施层
│ ├── adapter/WeiXiPort.java # 端口实现,调微信网关
│ └── gateway/
│ ├── IWeixinApiService.java # Retrofit2 微信API定义
│ └── dto/
│ ├── WeixinTemplateMessageDTO.java
│ └── WeixinTokenResponseDTO.java
└── types/properties/WeiXinApiProperties.java # 配置属性
这种分层好处是:
- 领域层通过端口抽象依赖外部实现,内聚业务语义,降低耦合;
- 基础设施层专注对外部系统(微信)的集成,变更可控、可替换;
- 工具注册放在领域服务,使 AI 侧调用语义直观清晰。
业务调用链与关键代码
1) 领域服务:将能力注册为 MCP 工具
引入SpringAI框架的MCP服务依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-server-spring-boot-starter</artifactId>
</dependency>
下面是 WeiXinNoticeService 的核心逻辑,@Tool 注解会把方法暴露给 MCP 框架,供智能体直接调用:
@Tool(description = "微信公众号消息通知")
public WeiXinNoticeFunctionResponse weixinNotice(WeiXinNoticeFunctionRequest request) throws IOException {
log.info("微信消息通知,平台:{} 主题:{} 描述:{}", request.getPlatform(), request.getSubject(), request.getDescription());
return weiXiPort.weixinNotice(request);
}
调用链路:AI → MCP 工具 weixinNotice → 端口接口 IWeiXiPort → 端口实现 WeiXiPort。
2) 基础设施适配器:获取 Token 并发送模板消息
获取一个测试公众号:https://mp.weixin.qq.com/debug/cgi-bin/sandboxinfo?action=showinfo&t=sandbox/index
如下图,获取自己的wxid参数

关注自己的测试公众号,获得touser。新增一个测试模版,格式如图所示:

WeiXiPort 是对外系统(微信API)的适配层,实现了获取 AccessToken、组装模板数据、发送消息的完整流程:
@Override
public WeiXinNoticeFunctionResponse weixinNotice(WeiXinNoticeFunctionRequest request) throws IOException {
// 1. 获取 accessToken
String accessToken = weixinAccessToken.getIfPresent(properties.getAppid());
if (null == accessToken) {
Call<WeixinTokenResponseDTO> call = weixinApiService.getToken("client_credential", properties.getAppid(), properties.getAppsecret());
WeixinTokenResponseDTO weixinTokenResponseDTO = call.execute().body();
assert weixinTokenResponseDTO != null;
accessToken = weixinTokenResponseDTO.getAccess_token();
weixinAccessToken.put(properties.getAppid(), accessToken);
}
// 2. 发送模板消息
Map<String, Map<String, String>> data = new HashMap<>();
WeixinTemplateMessageDTO.put(data, WeixinTemplateMessageDTO.TemplateKey.platform, request.getPlatform());
WeixinTemplateMessageDTO.put(data, WeixinTemplateMessageDTO.TemplateKey.subject, request.getSubject());
WeixinTemplateMessageDTO.put(data, WeixinTemplateMessageDTO.TemplateKey.description, request.getDescription());
WeixinTemplateMessageDTO templateMessageDTO = new WeixinTemplateMessageDTO(properties.getTouser(), properties.getTemplate_id());
templateMessageDTO.setUrl(request.getJumpUrl());
templateMessageDTO.setData(data);
Call<Void> call = weixinApiService.sendMessage(accessToken, templateMessageDTO);
call.execute();
WeiXinNoticeFunctionResponse weiXinNoticeFunctionResponse = new WeiXinNoticeFunctionResponse();
weiXinNoticeFunctionResponse.setSuccess(true);
return weiXinNoticeFunctionResponse;
}
要点:
- 本地
Cache<String, String> weixinAccessToken缓存 Token,按 appId 维度复用; - 通过
WeixinTemplateMessageDTO按模板键platform/subject/description组装数据; - 最终调用
IWeixinApiService.sendMessage将模板消息推送出去。
3) 微信网关接口:Retrofit2 定义
public interface IWeixinApiService {
/**
* 获取 Access token
* 文档:<a href="https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html">Get_access_token</a>
*
* @param grantType 获取access_token填写client_credential
* @param appId 第三方用户唯一凭证
* @param appSecret 第三方用户唯一凭证密钥,即appsecret
* @return 响应结果
*/
@GET("cgi-bin/token")
Call<WeixinTokenResponseDTO> getToken(
@Query("grant_type") String grantType,
@Query("appid") String appId,
@Query("secret") String appSecret
);
/**
* 发送微信公众号模板消息
* 文档:<a href="https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl">https://mp.weixin.qq.com/debug/cgi-bin/readtmpl?t=tmplmsg/faq_tmpl</a>
*
* @param accessToken getToken 获取的 token 信息
* @param weixinTemplateMessageDTO 入参对象
* @return 应答结果
*/
@POST("cgi-bin/message/template/send")
Call<Void> sendMessage(@Query("access_token") String accessToken, @Body WeixinTemplateMessageDTO weixinTemplateMessageDTO);
}
4) 模板消息 DTO:统一键定义与便捷 put
public class WeixinTemplateMessageDTO {
private String touser = "";
private String template_id = "";
private String url = "https://weixin.qq.com";
private Map<String, Map<String, String>> data = new HashMap<>();
public static void put(Map<String, Map<String, String>> data, TemplateKey key, String value) {
data.put(key.getCode(), new HashMap<>() {{ put("value", value); }});
}
public enum TemplateKey {
platform("platform_name","平台"),
subject("article_name","主题"),
description("date_name","简述"),
;
// ... getter/setter 省略
}
}
生产环境请将默认字段改为由配置注入,避免硬编码。
配置与运行
application.yml 关键项
api:
original_id: gh_a551e78c8
app-id: wx08140ecee889
app-secret: 9360a58ed62642
template_id: PhDCRzPTLJCCCFGF4cTM
touser: olI3g16yOowoXDxU
建议通过环境变量或启动参数覆盖以上敏感配置,避免明文入库。
以 Jar 方式运行
mvn clean package
java -jar target/mcp-server-weixin-1.0.0.jar \
--weixin.api.original_id=gh_xxx \
--weixin.api.app-id=wx_xxx \
--weixin.api.app-secret=xxx \
--weixin.api.template_id=TEMPLATE_ID \
--weixin.api.touser=OPEN_ID
在 MCP 客户端(IDE/Agent)中启用
将服务注册为 MCP Server(示例片段,按你的客户端要求适配):
{
"mcpServers": {
"mcp-server-weixin": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-jar",
"/Users/mumu/Applications/apache-maven-3.8.4/repository/org/cheese/mcp/mcp-server-weixin/1.0.0/mcp-server-weixin-1.0.0.jar",
"--weixin.api.original_id=gh_e06e056",
"--weixin.api.app-id=wx5a8a91f",
"--weixin.api.app-secret=0bea03e79dd8703928",
"--weixin.api.template_id=O8qI6gy7aMff9WSBb16cFk",
"--weixin.api.touser=or0Ab6ivw2T6SvU"
]
}
}
}
上述为演示配置,请替换为你的 Jar 路径与安全参数。
端到端调用示例(从 AI 到微信)
当 IDE/Agent 侧需要发送通知时,直接调用 weixinNotice 工具,传入领域请求:
{
"platform": "GitHub",
"subject": "新的 Pull Request",
"description": "有代码更新,请及时 Review",
"jumpUrl": "https://github.com/your/repo/pull/123"
}
服务会:
- 记录日志并进入端口实现;
- 拿到/刷新 AccessToken;
- 组合模板数据并调用微信 API 发送。
测试一下
@org.junit.Test
public void test_weixinNotice() {
String userInput = """
我需要你帮我生成一篇文章,要求如下;
1. 场景为互联网大厂java求职者面试
2. 面试管提问 Java 核心知识、JUC、JVM、多线程、线程池、HashMap、ArrayList、Spring、SpringBoot、MyBatis、Dubbo、RabbitMQ、xxl-job、Redis、MySQL、Linux、Docker、设计模式、DDD等不限于此的各项技术问题。
3. 按照故事场景,以严肃的面试官和搞笑的水货程序员谢飞机进行提问,谢飞机对简单问题可以回答,回答好了面试官还会夸赞。复杂问题胡乱回答,回答的不清晰。
4. 每次进行3轮提问,每轮可以有3-5个问题。这些问题要有技术业务场景上的衔接性,循序渐进引导提问。最后是面试官让程序员回家等通知类似的话术。
5. 提问后把问题的答案,写到文章最后,最后的答案要详细讲述出技术点,让小白可以学习下来。
调用工具,将以上内容发布文章到CSDN。
并进行微信公众号消息通知,调用通知工具,参数信息获取如下,平台:CSDN、文章:为文章标题 、简述:为文章简述、跳转地址:从发布文章的回复中获取 url
""";
System.out.println("\n>>> QUESTION: " + userInput);
ChatResponse response = chatClient.prompt().user(userInput).call().chatResponse();
System.out.println(response);
}

- 敏感信息管理:统一使用启动参数/环境变量/密钥管理服务注入,不要写死在
application.yml。 - 可观察性:为 Token 拉取、消息请求/响应增加结构化日志与告警;必要时落盘审计(见
data/log/)。有时AI调用失败,可以打日志,debug模式排查。
结语
通过将微信公众号通知能力以 MCP 工具的方式对外暴露,我们把“传统通知渠道”无缝接入到 AI 工作流中。DDD 分层保证了良好的可维护性与扩展性,MCP 则提供了模型可调用的标准协议。你可以按相同模式扩展到企业微信、钉钉、短信/邮件等渠道,逐步沉淀“通知中台”并赋能智能体。

510

被折叠的 条评论
为什么被折叠?



