关注公众号后推送小程序卡片

关注公众号后推送小程序卡片

前几天,有个不太熟悉的朋友联系到我,问我:“用户关注公众号后,如何给关注的用户推送一个小程序”。我确实也看到过这种场景,但是也不知道是怎么实现的,以为就在公众号后台设置自动回复就可以。然而,事实并非如此,我去查了一下公众号开发文档。发现事情稍许有点麻烦,并不没有想象中那么简单。那么要做到这种效果,要么就是使用第三方平台,要么就自己写代码

要实现该功能,我们需要调用公众号的发送客服消息接口,在编码之前,我们需要先做一下几项准备工作:
1.公众号认证
2.小程序认证且已发布
3.公众号和小程序关联(在公众号后台,依次进入 「广告与服务」 -> 「小程序」 -> 「小程序管理」 -> 「添加」,1个账号可关联10个“同主体或关联主体”小程序,3个非同主体小程序。)
4.域名
5.客服消息的接口权限

做好了以上几步,现在,我们就来开始编码吧

我这里用的RuoYi-Vue-Plus, 在引入一个微信公众号的SDK。我用的是WxJava库。准备就绪,直接开码

引入Jar包:

1
2
3
4
5
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>4.7.6-20250609.143003</version>
</dependency>

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
wx:
mp:
# 公众号配置
appid: wx26c607xxxxx
secret: xxxxxxxxxxx
token: okwechat
aes-key: xxxxxx
config-storage:
type: RedisTemplate
redis:
host: xx.xx.xx.x
port: 6379
password: xxxx
database: 2
key-prefix: wxmp
# 小程序卡片配置
miniprogram:
app-id: wx13f88d95502xxxx # 小程序的AppId
page-path: pages/index/index # 小程序页面路径
title: 欢迎使用xxxx售后 # 小程序卡片标题
thumb-media-id: # 小程序卡片缩略图的media_id,需要通过上传接口获取
# 消息模板配置
message-template:
subscribe:
instant-reply: "🎉 您好,欢迎关注 xxxx "
detailed-welcome: |
🌟 欢迎加入我们的大家庭!

感谢您关注我们的公众号!我们很高兴为您提供:

✨ 最新资讯和动态
📱 便捷的小程序服务
🎁 专属优惠和活动
💬 贴心的客服支持

如需了解更多功能,请回复"帮助"或点击菜单查看。

我们将竭诚为您服务!
miniprogram-guide: "👆 点击上方小程序卡片,体验更多精彩功能!"
keyword:
help: |
📋 功能菜单:

🔍 回复"小程序" - 获取小程序卡片
📱 回复"服务" - 查看我们的服务
🎁 回复"活动" - 查看最新活动
💬 回复"客服" - 联系人工客服

更多功能请查看底部菜单!
service: |
🏢 我们的服务包括:

🔧 技术支持与咨询
📊 数据分析服务
🎨 界面设计优化
🚀 系统升级维护

如需详细了解,请联系客服!
miniprogram-promo: "已为您发送小程序卡片,请点击体验更多功能!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
@Data
@Component
@ConfigurationProperties(prefix = "wx.mp.message-template")
public class WxMpMessageTemplate {

/**
* 关注欢迎消息模板
*/
private SubscribeTemplate subscribe = new SubscribeTemplate();

/**
* 关键词回复模板
*/
private KeywordTemplate keyword = new KeywordTemplate();

@Data
public static class SubscribeTemplate {
/**
* 即时回复消息(防止超时)
*/
private String instantReply = """
🎉 您好,欢迎关注 xxxx!



""";

/**
* 详细欢迎消息
*/
private String detailedWelcome = """
🌟 欢迎加入我们的大家庭!

感谢您关注我们的公众号!我们很高兴为您提供:

✨ 最新资讯和动态
📱 便捷的小程序服务
🎁 专属优惠和活动
💬 贴心的客服支持

如需了解更多功能,请回复"帮助"或点击菜单查看。

我们将竭诚为您服务!
""";

/**
* 小程序引导消息
*/
private String miniprogramGuide = "👆 点击上方小程序卡片,体验更多精彩功能!";
}

@Data
public static class KeywordTemplate {
/**
* 帮助消息
*/
private String help = """
📋 功能菜单:

🔍 回复"小程序" - 获取小程序卡片
📱 回复"服务" - 查看我们的服务
🎁 回复"活动" - 查看最新活动
💬 回复"客服" - 联系人工客服

更多功能请查看底部菜单!
""";

/**
* 服务介绍
*/
private String service = """
🏢 我们的服务包括:

🔧 技术支持与咨询
📊 数据分析服务
🎨 界面设计优化
🚀 系统升级维护

如需详细了解,请联系客服!
""";

/**
* 小程序推广消息
*/
private String miniprogramPromo = "已为您发送小程序卡片,请点击体验更多功能!";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
@Slf4j
@Service
@RequiredArgsConstructor
public class WxMpEventHandler {

private final IWxMpUserService wxMpUserService;
private final WxMpMessageTemplate messageTemplate;

@Value("${wx.mp.miniprogram.app-id:}")
private String miniprogramAppId;

@Value("${wx.mp.miniprogram.page-path:pages/index/index}")
private String miniprogramPagePath;

@Value("${wx.mp.miniprogram.title:欢迎使用我们的小程序}")
private String miniprogramTitle;

@Value("${wx.mp.miniprogram.thumb-media-id:}")
private String miniprogramThumbMediaId;

/**
* 记录所有事件的日志
*/
public WxMpXmlOutMessage logHandler(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService weixinService,
WxSessionManager sessionManager) {
log.info("接收到请求消息,内容:{}", wxMessage);
return null;
}

/**
* 关注事件处理
*/
public WxMpXmlOutMessage subscribeHandler(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService weixinService,
WxSessionManager sessionManager)
throws WxErrorException {

String openId = wxMessage.getFromUser();
log.info("新关注用户 OPENID: {}", openId);

// 获取用户信息并保存
try {
WxMpUser userInfo = weixinService.getUserService().userInfo(openId);
log.info("已保存用户信息: {}", userInfo.getNickname());
} catch (Exception e) {
log.error("获取用户信息失败,仅保存openId", e);
}

// 异步发送完整的欢迎消息(文字 + 小程序卡片)
sendWelcomeMessages(openId, weixinService);

// 立即回复简单的文字消息(避免微信超时)
return WxMpXmlOutTextMessage.TEXT()
.content(messageTemplate.getSubscribe().getInstantReply())
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}

/**
* 取消关注事件处理
*/
public WxMpXmlOutMessage unsubscribeHandler(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService weixinService,
WxSessionManager sessionManager) {
String openId = wxMessage.getFromUser();
log.info("取消关注用户 OPENID: {}", openId);

// 更新用户取消关注状态
wxMpUserService.handleUnsubscribe(openId);

return null;
}

/**
* 默认事件处理
*/
public WxMpXmlOutMessage defaultHandler(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService weixinService,
WxSessionManager sessionManager) {

// 如果是文字消息,进行智能回复
if ("text".equals(wxMessage.getMsgType())) {
String content = wxMessage.getContent();
log.info("收到文字消息:{}", content);

return handleTextMessage(content, wxMessage, weixinService);
}

return null;
}

/**
* 处理文字消息
*/
private WxMpXmlOutMessage handleTextMessage(String content, WxMpXmlMessage wxMessage, WxMpService weixinService) {
if (content == null) {
return null;
}

String lowerContent = content.toLowerCase().trim();
String fromUser = wxMessage.getFromUser();
String toUser = wxMessage.getToUser();

// 关键词匹配回复
if (lowerContent.contains("小程序") || lowerContent.contains("程序")) {
return handleMiniprogramRequest(fromUser, toUser, weixinService);
} else if (lowerContent.contains("帮助") || lowerContent.equals("?") || lowerContent.equals("?")) {
return buildTextReply(messageTemplate.getKeyword().getHelp(), fromUser, toUser);
} else if (lowerContent.contains("服务")) {
return buildTextReply(messageTemplate.getKeyword().getService(), fromUser, toUser);
} else if (lowerContent.contains("你好") || lowerContent.contains("hi") || lowerContent.contains("hello")) {
return buildTextReply("您好!很高兴为您服务!如需帮助,请回复\"帮助\"查看功能菜单。", fromUser, toUser);
} else if (lowerContent.contains("客服") || lowerContent.contains("人工")) {
return buildTextReply("正在为您转接人工客服,请稍候...\n\n工作时间:9:00-18:00", fromUser, toUser);
}

// 默认回复
return buildTextReply("感谢您的消息!如需帮助,请回复\"帮助\"查看功能菜单。", fromUser, toUser);
}

/**
* 处理小程序请求
*/
private WxMpXmlOutMessage handleMiniprogramRequest(String fromUser, String toUser, WxMpService weixinService) {
if (miniprogramAppId != null && !miniprogramAppId.trim().isEmpty()
&& !miniprogramAppId.equals("your_mp_appid")) {
try {
// 异步发送小程序卡片
java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 稍微延迟
sendMiniprogramCard(fromUser, weixinService);

// 发送引导消息
Thread.sleep(1000);
me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage guideMessage =
me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage.TEXT()
.toUser(fromUser)
.content(messageTemplate.getSubscribe().getMiniprogramGuide())
.build();
weixinService.getKefuService().sendKefuMessage(guideMessage);
} catch (Exception e) {
log.error("异步发送小程序卡片失败", e);
}
});

return buildTextReply(messageTemplate.getKeyword().getMiniprogramPromo(), fromUser, toUser);
} catch (Exception e) {
log.error("发送小程序卡片失败", e);
return buildTextReply("抱歉,小程序卡片发送失败,请稍后重试。", fromUser, toUser);
}
} else {
return buildTextReply("抱歉,小程序功能暂未配置,请联系管理员。", fromUser, toUser);
}
}

/**
* 构建文字回复消息
*/
private WxMpXmlOutTextMessage buildTextReply(String content, String fromUser, String toUser) {
return WxMpXmlOutTextMessage.TEXT()
.content(content)
.fromUser(toUser)
.toUser(fromUser)
.build();
}

/**
* 发送完整的欢迎消息(文字 + 小程序卡片)
*/
private void sendWelcomeMessages(String openId, WxMpService weixinService) {
// 使用线程池异步发送,避免阻塞主流程
java.util.concurrent.CompletableFuture.runAsync(() -> {
try {
// 等待一秒,确保用户已关注成功
Thread.sleep(1000);

// 发送详细的欢迎文字消息
sendDetailedWelcomeText(openId, weixinService);

// 如果配置了小程序信息,发送小程序卡片
if (miniprogramAppId != null && !miniprogramAppId.trim().isEmpty()
&& !miniprogramAppId.equals("your_mp_appid")) {
// 稍微延迟,避免消息发送过快
Thread.sleep(1000);
sendMiniprogramCard(openId, weixinService);
}

} catch (Exception e) {
log.error("发送欢迎消息失败,openId: {}", openId, e);
}
});
}

/**
* 发送详细的欢迎文字消息
*/
private void sendDetailedWelcomeText(String openId, WxMpService weixinService) throws WxErrorException {
try {
me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage textMessage =
me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage.TEXT()
.toUser(openId)
.content(messageTemplate.getSubscribe().getDetailedWelcome())
.build();

weixinService.getKefuService().sendKefuMessage(textMessage);
log.info("成功发送详细欢迎文字给用户:{}", openId);
} catch (Exception e) {
log.error("发送详细欢迎文字失败,openId: {}", openId, e);
throw e;
}
}

/**
* 发送小程序卡片
*/
private void sendMiniprogramCard(String openId, WxMpService weixinService) throws WxErrorException {
try {
// 检查是否配置了缩略图媒体ID
if (miniprogramThumbMediaId == null || miniprogramThumbMediaId.trim().isEmpty()) {
log.warn("未配置小程序卡片缩略图媒体ID,无法发送小程序卡片,openId: {}", openId);
// 发送文字提示替代小程序卡片
WxMpKefuMessage textMessage = WxMpKefuMessage.TEXT()
.toUser(openId)
.content("点击这里打开小程序:" + miniprogramTitle + "\n\n注意:管理员需要先上传小程序卡片缩略图")
.build();
weixinService.getKefuService().sendKefuMessage(textMessage);
return;
}

WxMpKefuMessage build = WxMpKefuMessage.MINIPROGRAMPAGE()
.appId(miniprogramAppId)
.title(miniprogramTitle)
.pagePath(miniprogramPagePath)
.thumbMediaId(miniprogramThumbMediaId)
.toUser(openId)
.build();

weixinService.getKefuService().sendKefuMessage(build);
log.info("成功发送小程序卡片给用户:{}", openId);
} catch (Exception e) {
log.error("发送小程序卡片失败,openId: {}", openId, e);
throw e;
}
}

/**
* 上传临时媒体文件获取media_id
* 用于小程序卡片的缩略图(有效期3天)
*
* @param weixinService 微信服务
* @param filePath 图片文件路径
* @param mediaType 媒体类型 (image, voice, video, thumb)
* @return media_id
*/
public String uploadTempMedia(WxMpService weixinService, String filePath, String mediaType) throws WxErrorException {
try {
java.io.File file = new java.io.File(filePath);
if (!file.exists()) {
throw new IllegalArgumentException("文件不存在: " + filePath);
}

// 使用临时媒体文件上传接口
// me.chanjar.weixin.mp.bean.result.WxMpMediaUploadResult result =
// weixinService.getMediaService().upload(mediaType, file);
//
// String mediaId = result.getMediaId();
// log.info("成功上传临时媒体文件,media_id: {}, 文件路径: {}", mediaId, filePath);
return "";
} catch (Exception e) {
log.error("上传临时媒体文件失败,文件路径: {}", filePath, e);
throw e;
}
}

/**
* 上传永久素材获取media_id
* 用于小程序卡片的缩略图(永久有效)
*
* @param weixinService 微信服务
* @param filePath 图片文件路径
* @param title 素材标题(可选)
* @param introduction 素材介绍(可选)
* @return media_id
*/
public String uploadPermanentMedia(WxMpService weixinService, String filePath, String title, String introduction) throws WxErrorException {
try {
java.io.File file = new java.io.File(filePath);
if (!file.exists()) {
throw new IllegalArgumentException("文件不存在: " + filePath);
}

// 创建永久素材对象
me.chanjar.weixin.mp.bean.material.WxMpMaterial material =
new me.chanjar.weixin.mp.bean.material.WxMpMaterial();
material.setFile(file);
material.setName(file.getName());

if (title != null && !title.trim().isEmpty()) {
material.setVideoTitle(title);
}
if (introduction != null && !introduction.trim().isEmpty()) {
material.setVideoIntroduction(introduction);
}

// 上传永久素材
me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult result =
weixinService.getMaterialService().materialFileUpload("image", material);

String mediaId = result.getMediaId();
log.info("成功上传永久素材,media_id: {}, 文件路径: {}", mediaId, filePath);
return mediaId;
} catch (Exception e) {
log.error("上传永久素材失败,文件路径: {}", filePath, e);
throw e;
}
}

/**
* 上传图片文件作为小程序卡片缩略图(临时媒体文件,3天有效期)
* 建议图片尺寸:200x200px,格式:jpg/png,大小不超过2MB
*
* @param weixinService 微信服务
* @param imagePath 图片文件路径
* @return thumb_media_id
*/
public String uploadMiniprogramThumb(WxMpService weixinService, String imagePath) throws WxErrorException {
return uploadTempMedia(weixinService, imagePath, "thumb");
}

/**
* 上传图片文件作为小程序卡片缩略图(永久素材,无时间限制)
* 建议图片尺寸:200x200px,格式:jpg/png,大小不超过2MB
*
* @param weixinService 微信服务
* @param imagePath 图片文件路径
* @param title 素材标题
* @return media_id
*/
public String uploadPermanentMiniprogramThumb(WxMpService weixinService, String imagePath, String title) throws WxErrorException {
return uploadPermanentMedia(weixinService, imagePath, title, "小程序卡片缩略图");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp")
public class WxMpController {

private final WxMpService wxMpService;
private final WxMpMessageRouter messageRouter;
private final WxMpEventHandler wxMpEventHandler;

/**
* 验证消息的确来自微信服务器
*/
@SaIgnore
@GetMapping("/portal")
public String authGet(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String echostr) {
log.info("接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr);
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法请求";
}

/**
* 接收并处理微信公众号消息
*/
@SaIgnore
@PostMapping("/portal")
public String post(@RequestBody String requestBody,
@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam(required = false) String openid,
@RequestParam(required = false) String encType,
@RequestParam(required = false) String msgSignature,
HttpServletRequest request) {
log.info("接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}], timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ",
openid, signature, encType, msgSignature, timestamp, nonce, requestBody);

if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}

String out = null;
if (encType == null) {
// 明文传输的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}

out = outMessage.toXml();
} else if ("aes".equalsIgnoreCase(encType)) {
// aes加密的消息
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxMpService.getWxMpConfigStorage(), timestamp, nonce, msgSignature);
log.debug("消息解密后内容为:\n{} ", inMessage.toString());
WxMpXmlOutMessage outMessage = this.route(inMessage);
if (outMessage == null) {
return "";
}

out = outMessage.toEncryptedXml(wxMpService.getWxMpConfigStorage());
}

log.debug("组装回复信息:{}", out);
return out;
}

private WxMpXmlOutMessage route(WxMpXmlMessage message) {
try {
return this.messageRouter.route(message);
} catch (Exception e) {
log.error("路由消息时出现异常!", e);
}

return null;
}

/**
* 上传小程序卡片缩略图(临时媒体文件,3天有效期)
* 建议图片尺寸:200x200px,格式:jpg/png,大小不超过3MB
*/
@SaCheckPermission("system:wxmp:upload")
@PostMapping("/uploadThumb")
public R<Map<String, String>> uploadMiniprogramThumb(@RequestParam("file") MultipartFile file) {
try {
if (file.isEmpty()) {
return R.fail("上传文件不能为空");
}

// 检查文件类型
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || (!originalFilename.toLowerCase().endsWith(".jpg")
&& !originalFilename.toLowerCase().endsWith(".jpeg")
&& !originalFilename.toLowerCase().endsWith(".png"))) {
return R.fail("仅支持 JPG、JPEG、PNG 格式的图片");
}

// 检查文件大小(3MB)
if (file.getSize() > 3 * 1024 * 1024) {
return R.fail("图片大小不能超过 3MB");
}

// 创建临时文件
String tempFileName = System.currentTimeMillis() + "_" + originalFilename;
String tempDir = System.getProperty("java.io.tmpdir");
File tempFile = new File(tempDir, tempFileName);

// 确保临时目录存在
tempFile.getParentFile().mkdirs();

try {
// 直接使用输入流写入文件,避免Undertow临时文件问题
Files.copy(file.getInputStream(), tempFile.toPath(),
StandardCopyOption.REPLACE_EXISTING);

// 上传到微信服务器
String mediaId = wxMpEventHandler.uploadMiniprogramThumb(wxMpService, tempFile.getAbsolutePath());

Map<String, String> result = new HashMap<>();
result.put("mediaId", mediaId);
result.put("fileName", originalFilename);
result.put("fileSize", String.valueOf(file.getSize()));
result.put("type", "temporary");
result.put("validDays", "3");

log.info("成功上传小程序卡片缩略图,media_id: {}, 文件名: {}", mediaId, originalFilename);
return R.ok("临时素材上传成功(3天有效期)", result);

} finally {
// 清理临时文件
if (tempFile.exists()) {
tempFile.delete();
}
}

} catch (IOException e) {
log.error("文件操作失败", e);
return R.fail("文件上传失败:" + e.getMessage());
} catch (Exception e) {
log.error("上传小程序卡片缩略图失败", e);
return R.fail("上传失败:" + e.getMessage());
}
}

/**
* 上传小程序卡片缩略图(永久素材,无时间限制)
* 建议图片尺寸:200x200px,格式:jpg/png,大小不超过3MB
*/
// @SaCheckPermission("system:wxmp:upload")
@SaIgnore
@PostMapping("/uploadPermanentThumb")
public R<Map<String, String>> uploadPermanentMiniprogramThumb(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "title", defaultValue = "小程序卡片缩略图") String title) {
try {
if (file.isEmpty()) {
return R.fail("上传文件不能为空");
}

// 检查文件类型
String originalFilename = file.getOriginalFilename();
if (originalFilename == null || (!originalFilename.toLowerCase().endsWith(".jpg")
&& !originalFilename.toLowerCase().endsWith(".jpeg")
&& !originalFilename.toLowerCase().endsWith(".png"))) {
return R.fail("仅支持 JPG、JPEG、PNG 格式的图片");
}

// 检查文件大小(3MB)
if (file.getSize() > 3 * 1024 * 1024) {
return R.fail("图片大小不能超过 3MB");
}

// 创建临时文件
String tempFileName = System.currentTimeMillis() + "_" + originalFilename;
String tempDir = System.getProperty("java.io.tmpdir");
File tempFile = new File(tempDir, tempFileName);

// 确保临时目录存在
tempFile.getParentFile().mkdirs();

try {
// 直接使用输入流写入文件,避免Undertow临时文件问题
java.nio.file.Files.copy(file.getInputStream(), tempFile.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);

// 上传到微信服务器(永久素材)
String mediaId = wxMpEventHandler.uploadPermanentMiniprogramThumb(wxMpService, tempFile.getAbsolutePath(), title);

Map<String, String> result = new HashMap<>();
result.put("mediaId", mediaId);
result.put("fileName", originalFilename);
result.put("fileSize", String.valueOf(file.getSize()));
result.put("title", title);
result.put("type", "permanent");

log.info("成功上传永久小程序卡片缩略图,media_id: {}, 文件名: {}", mediaId, originalFilename);
return R.ok("永久素材上传成功(无时间限制)", result);

} finally {
// 清理临时文件
if (tempFile.exists()) {
tempFile.delete();
}
}

} catch (IOException e) {
log.error("文件操作失败", e);
return R.fail("文件上传失败:" + e.getMessage());
} catch (Exception e) {
log.error("上传永久小程序卡片缩略图失败", e);
return R.fail("上传失败:" + e.getMessage());
}
}
}

程序开发好后,我们需要取微信公众号后面配置一下地址,【设置与开发】->【开发接口管理】->【基本配置】->【服务器配置】->【修改配置】

设置好后,还需要点击【启用】

后续,如果有新用户关注或者取消关注、后端都能接收到事件消息,当关注时,就会发送小程序卡片了

还有一个地方需要注意,小程序卡片缩略图的media_id,需要通过上传接口获取。这个可以直接使用程序中的uploadThumb(临时媒体文件,3天有效期)接口或者uploadPermanentThumb(永久素材,无时间限制)接口

关注公众号后推送小程序卡片

https://blogs.52fx.biz/posts/170493083.html

作者

eyiadmin

发布于

2025-08-21

更新于

2025-08-21

许可协议

评论