first commit

This commit is contained in:
dev
2026-02-22 12:12:02 +08:00
commit b236048a24
1467 changed files with 160403 additions and 0 deletions
+79
View File
@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
</parent>
<artifactId>ruoyi-mp</artifactId>
<description>微信公众号模块</description>
<dependencies>
<!-- 核心模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<!-- Web模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-satoken</artifactId>
</dependency>
<!-- 微信公众号 SDK -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
</dependency>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Boot Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,16 @@
package org.dromara.mp.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 微信公众号切换配置注解
*
* @author Peng Les
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SwitchoverCheck {
}
@@ -0,0 +1,46 @@
package org.dromara.mp.aspect;
import me.chanjar.weixin.mp.api.WxMpService;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 微信公众号切面 用于切换公众号配置
*
* @author Peng Les
*/
@Aspect
@Component
public class SwitchoverCheckAspect {
@Autowired
private WxMpService wxMpService;
@Pointcut("@annotation(org.dromara.mp.annotation.SwitchoverCheck)")
public void switchoverCheckPointcut() {
}
@Before("switchoverCheckPointcut()")
public void beforeSwitchover(JoinPoint joinPoint) {
// 获取方法参数
Object[] args = joinPoint.getArgs();
if (args.length > 0 && args[0] instanceof String) {
String appid = (String) args[0];
System.out.println("前置切面:准备切换公众号配置,appid: " + appid);
// 检查 appid 是否为空
if (StringUtils.isEmpty(appid)) {
throw new IllegalArgumentException("appid 不能为空!");
}
if (!wxMpService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
}
}
}
@@ -0,0 +1,17 @@
package org.dromara.mp.builder;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Peng Les
*/
public abstract class WxMpAbstractBuilder {
protected final Logger logger = LoggerFactory.getLogger(getClass());
public abstract WxMpXmlOutMessage build(String content,
WxMpXmlMessage wxMessage, WxMpService service);
}
@@ -0,0 +1,23 @@
package org.dromara.mp.builder;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutImageMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
/**
* @author Peng Les
*/
public class WxMpImageBuilderWxMp extends WxMpAbstractBuilder {
@Override
public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage,
WxMpService service) {
logger.info("build 图片消息,内容:{}", content);
WxMpXmlOutImageMessage m = WxMpXmlOutMessage.IMAGE().mediaId(content)
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.build();
return m;
}
}
@@ -0,0 +1,23 @@
package org.dromara.mp.builder;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
/**
* @author Peng Les
*/
public class WxMpTextBuilderWxMp extends WxMpAbstractBuilder {
@Override
public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage,
WxMpService service) {
logger.info("build 文本消息,内容:{}", content);
WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.build();
return m;
}
}
@@ -0,0 +1,81 @@
package org.dromara.mp.config;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.redis.JedisWxRedisOps;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import java.util.List;
import java.util.stream.Collectors;
/**
* 微信公众号配置
*
* @author pengles
*
* 微信公众号服务端装配:
* - 读取 wx.mp.* 配置,支持多公众号与可选 Redis 存储
* - 注入 WxMpService 与消息路由器,用于处理消息、二维码、模板消息等能力
*/
@Configuration
@EnableConfigurationProperties(WxMpProperties.class)
@ConditionalOnClass(WxMpService.class)
@RequiredArgsConstructor
public class WxMpConfiguration {
private final WxMpProperties properties;
/**
* 构建 WxMpService,支持多公众号配置与 Redis 存储
*/
@Bean
@ConditionalOnMissingBean
public WxMpService wxMpService() {
final List<WxMpProperties.MpConfig> configs = this.properties.getConfigs();
if (configs == null || configs.isEmpty()) {
throw new RuntimeException("请先配置微信公众号相关信息!");
}
WxMpService service = new WxMpServiceImpl();
service.setMultiConfigStorages(configs
.stream().map(a -> {
WxMpDefaultConfigImpl configStorage;
if (this.properties.isUseRedis()) {
final WxMpProperties.RedisConfig redisConfig = this.properties.getRedisConfig();
JedisPoolConfig poolConfig = new JedisPoolConfig();
JedisPool jedisPool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
redisConfig.getTimeout(), redisConfig.getPassword());
configStorage = new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), a.getAppId());
} else {
configStorage = new WxMpDefaultConfigImpl();
}
configStorage.setAppId(a.getAppId());
configStorage.setSecret(a.getSecret());
configStorage.setToken(a.getToken());
configStorage.setAesKey(a.getAesKey());
configStorage.setOauth2RedirectUrl(a.getOauth2RedirectUrl());
configStorage.setQrConnectRedirectUrl(a.getQrConnectRedirectUrl());
return configStorage;
}).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o)));
return service;
}
/**
* 构建消息路由器,按规则分发处理入口消息
*/
@Bean
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
return new WxMpMessageRouter(wxMpService);
}
}
@@ -0,0 +1,87 @@
package org.dromara.mp.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
/**
* 微信公众号属性绑定(wx.mp.*)
* - useRedis:是否使用 Redis 存储 access token
* - redisConfigRedis 连接参数
* - configs:多公众号配置(appid、secret、token、aesKey 以及各类回调地址)
*/
@Data
@ConfigurationProperties(prefix = "wx.mp")
public class WxMpProperties {
/**
* 是否使用redis存储access token
*/
private boolean useRedis;
/**
* redis 配置
*/
private RedisConfig redisConfig;
/**
* 多个公众号配置信息
*/
private List<MpConfig> configs;
@Data
public static class RedisConfig {
/**
* redis服务器 主机地址
*/
private String host;
/**
* redis服务器 端口号
*/
private Integer port;
/**
* redis服务器 数据库索引
*/
private Integer database;
/**
* redis服务器 密码
*/
private String password;
/**
* redis服务器 连接超时时间
*/
private Integer timeout;
}
@Data
public static class MpConfig {
/**
* 设置微信公众号的appid
*/
private String appId;
/**
* 设置微信公众号的app secret
*/
private String secret;
/**
* 设置微信公众号的token
*/
private String token;
/**
* 设置微信公众号的EncodingAESKey
*/
private String aesKey;
/**
* 设置微信公众号网页授权获取用户信息回调地址
*/
private String oauth2RedirectUrl;
/**
* 设置微信公众号网页应用授权登陆回调地址回调地址
*/
private String qrConnectRedirectUrl;
}
}
@@ -0,0 +1,107 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxJsapiSignature;
import me.chanjar.weixin.mp.api.WxMpService;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 微信公众号JSAPI控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/jsapi/{appid}")
public class WxMpJsapiController extends BaseController {
private final WxMpService wxMpService;
/**
* 获取JSAPI票据
*
* @param appid 公众号appid
* @param forceRefresh 是否强制刷新
* @return JSAPI票据
*/
@GetMapping("/ticket")
@SwitchoverCheck
public R<String> getJsapiTicket(@PathVariable String appid,
@RequestParam(defaultValue = "false") boolean forceRefresh) {
try {
String ticket = wxMpService.getJsapiTicket(forceRefresh);
return R.ok(ticket);
} catch (Exception e) {
log.error("获取JSAPI票据失败: {}", e.getMessage(), e);
return R.fail("获取JSAPI票据失败: " + e.getMessage());
}
}
/**
* 创建JSAPI签名
*
* @param appid 公众号appid
* @param url 当前网页的URL,不包含#及其后面部分
* @return JSAPI签名信息
*/
@GetMapping("/signature")
@SwitchoverCheck
public R<WxJsapiSignature> createJsapiSignature(@PathVariable String appid,
@RequestParam String url) {
try {
WxJsapiSignature signature = wxMpService.createJsapiSignature(url);
return R.ok(signature);
} catch (Exception e) {
log.error("创建JSAPI签名失败: {}", e.getMessage(), e);
return R.fail("创建JSAPI签名失败: " + e.getMessage());
}
}
/**
* 获取微信卡券签名
*
* @param appid 公众号appid
* @param url 当前网页的URL
* @return 卡券签名信息
*/
@GetMapping("/card/signature")
@SwitchoverCheck
public R<Object> createCardSignature(@PathVariable String appid,
@RequestParam String url) {
try {
// 这里可以添加卡券签名的逻辑
log.info("创建卡券签名: appid={}, url={}", appid, url);
return R.ok("卡券签名功能待实现");
} catch (Exception e) {
log.error("创建卡券签名失败: {}", e.getMessage(), e);
return R.fail("创建卡券签名失败: " + e.getMessage());
}
}
/**
* 获取微信JS接口的临时票据
*
* @param appid 公众号appid
* @param type 票据类型
* @return 临时票据
*/
@GetMapping("/ticket/{type}")
@SwitchoverCheck
public R<String> getTicket(@PathVariable String appid,
@PathVariable String type) {
try {
String ticket = wxMpService.getTicket(me.chanjar.weixin.common.enums.TicketType.valueOf(type.toUpperCase()));
return R.ok(ticket);
} catch (Exception e) {
log.error("获取票据失败: {}", e.getMessage(), e);
return R.fail("获取票据失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,205 @@
package org.dromara.mp.controller;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialFileBatchGetResult;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.dromara.mp.service.WxMpMaterialService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 微信公众号素材管理控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/media/{appid}")
public class WxMpMediaController extends BaseController {
private final WxMpService wxMpService;
private final WxMpMaterialService wxMpMaterialService;
/**
* 上传临时素材
*
* @param appid 公众号appid
* @param type 素材类型(image、voice、video、thumb
* @param file 文件
* @return 上传结果
*/
@PostMapping("/upload/temp")
@SwitchoverCheck
public R<WxMediaUploadResult> uploadTempMedia(@PathVariable String appid,
@RequestParam String type,
@RequestParam("file") File file) {
try {
WxMediaUploadResult result = wxMpMaterialService.uploadTempMedia(type, file);
return R.ok(result);
} catch (Exception e) {
log.error("上传临时素材失败: {}", e.getMessage(), e);
return R.fail("上传临时素材失败: " + e.getMessage());
}
}
/**
* 下载临时素材
*
* @param appid 公众号appid
* @param mediaId 素材ID
* @param response HTTP响应
*/
@GetMapping("/download/temp/{mediaId}")
@SwitchoverCheck
public void downloadTempMedia(@PathVariable String appid,
@PathVariable String mediaId,
HttpServletResponse response) {
try {
InputStream inputStream = wxMpMaterialService.downloadTempMedia(mediaId);
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment; filename=" + mediaId);
try (OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.flush();
}
} catch (Exception e) {
log.error("下载临时素材失败: {}", e.getMessage(), e);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 上传永久素材
*
* @param appid 公众号appid
* @param type 素材类型(image、voice、video、thumb
* @param file 文件
* @return 上传结果
*/
@PostMapping("/upload/forever")
@SwitchoverCheck
public R<WxMpMaterialUploadResult> uploadForeverMedia(@PathVariable String appid,
@RequestParam String type,
@RequestParam("file") File file) {
try {
WxMpMaterialUploadResult result = wxMpMaterialService.uploadMaterial(type, file);
return R.ok(result);
} catch (Exception e) {
log.error("上传永久素材失败: {}", e.getMessage(), e);
return R.fail("上传永久素材失败: " + e.getMessage());
}
}
/**
* 删除永久素材
*
* @param appid 公众号appid
* @param mediaId 素材ID
* @return 删除结果
*/
@DeleteMapping("/delete/forever/{mediaId}")
@SwitchoverCheck
public R<Void> deleteForeverMedia(@PathVariable String appid, @PathVariable String mediaId) {
try {
boolean result = wxMpMaterialService.deleteMaterial(mediaId);
return result ? R.ok() : R.fail("删除永久素材失败");
} catch (Exception e) {
log.error("删除永久素材失败: {}", e.getMessage(), e);
return R.fail("删除永久素材失败: " + e.getMessage());
}
}
/**
* 获取永久素材
*
* @param appid 公众号appid
* @param mediaId 素材ID
*/
@GetMapping("/get/forever/{mediaId}")
@SwitchoverCheck
public R<WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem> getForeverMedia(@PathVariable String appid,
@PathVariable String mediaId) {
WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem wxMpMaterialServiceMaterial = wxMpMaterialService.getMaterial(mediaId);
return R.ok(wxMpMaterialServiceMaterial);
}
/**
* 获取素材总数
*
* @param appid 公众号appid
* @return 素材总数
*/
@GetMapping("/count")
@SwitchoverCheck
public R<Object> getMaterialCount(@PathVariable String appid) {
try {
Object count = wxMpMaterialService.getMaterialCount();
return R.ok(count);
} catch (Exception e) {
log.error("获取素材总数失败: {}", e.getMessage(), e);
return R.fail("获取素材总数失败: " + e.getMessage());
}
}
/**
* 获取素材列表
*
* @param appid 公众号appid
* @param type 素材类型
* @param offset 偏移量
* @param count 数量
* @return 素材列表
*/
@GetMapping("/list")
@SwitchoverCheck
public R<Object> getMaterialList(@PathVariable String appid,
@RequestParam String type,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "20") int count) {
try {
Object materialList = wxMpMaterialService.getMaterialList(type, offset, count);
return R.ok(materialList);
} catch (Exception e) {
log.error("获取素材列表失败: {}", e.getMessage(), e);
return R.fail("获取素材列表失败: " + e.getMessage());
}
}
/**
* 上传图文消息内的图片
*
* @param appid 公众号appid
* @param file 图片文件
* @return 上传结果
*/
@PostMapping("/upload/img")
@SwitchoverCheck
public R<String> uploadImg(@PathVariable String appid,
@RequestParam("file") File file) {
try {
String url = wxMpMaterialService.uploadImg(file);
return R.ok(url);
} catch (Exception e) {
log.error("上传图文消息图片失败: {}", e.getMessage(), e);
return R.fail("上传图文消息图片失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,124 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.menu.WxMpMenu;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.dromara.mp.service.WxMpMenuService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 微信公众号菜单管理控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/menu/{appid}")
public class WxMpMenuController extends BaseController {
private final WxMpService wxMpService;
private final WxMpMenuService wxMpMenuService;
/**
* 创建菜单
*
* @param appid 公众号appid
* @param wxMenu 菜单信息
* @return 创建结果
*/
@PostMapping("/create")
@SwitchoverCheck
public R<Void> createMenu(@PathVariable String appid, @RequestBody WxMenu wxMenu) {
try {
boolean result = wxMpMenuService.createMenu(wxMenu);
return result ? R.ok() : R.fail("创建菜单失败");
} catch (Exception e) {
log.error("创建菜单失败: {}", e.getMessage(), e);
return R.fail("创建菜单失败: " + e.getMessage());
}
}
/**
* 删除菜单
*
* @param appid 公众号appid
* @return 删除结果
*/
@DeleteMapping("/delete")
@SwitchoverCheck
public R<Void> deleteMenu(@PathVariable String appid) {
try {
boolean result = wxMpMenuService.deleteMenu();
return result ? R.ok() : R.fail("删除菜单失败");
} catch (Exception e) {
log.error("删除菜单失败: {}", e.getMessage(), e);
return R.fail("删除菜单失败: " + e.getMessage());
}
}
/**
* 查询菜单
*
* @param appid 公众号appid
* @return 菜单信息
*/
@GetMapping("/get")
@SwitchoverCheck
public R<WxMpMenu> getMenu(@PathVariable String appid) {
try {
WxMpMenu menu = wxMpMenuService.getMenu();
return R.ok(menu);
} catch (Exception e) {
log.error("查询菜单失败: {}", e.getMessage(), e);
return R.fail("查询菜单失败: " + e.getMessage());
}
}
/**
* 创建默认菜单
*
* @param appid 公众号appid
* @return 创建结果
*/
@PostMapping("/create-default")
@SwitchoverCheck
public R<Void> createDefaultMenu(@PathVariable String appid) {
try {
boolean result = wxMpMenuService.createDefaultMenu();
return result ? R.ok() : R.fail("创建默认菜单失败");
} catch (Exception e) {
log.error("创建默认菜单失败: {}", e.getMessage(), e);
return R.fail("创建默认菜单失败: " + e.getMessage());
}
}
/**
* 处理菜单点击事件
*
* @param appid 公众号appid
* @param eventKey 事件key
* @param openid 用户openid
* @return 处理结果
*/
@PostMapping("/handle-click")
@SwitchoverCheck
public R<String> handleMenuClick(@PathVariable String appid,
@RequestParam String eventKey,
@RequestParam String openid) {
try {
String response = wxMpMenuService.handleMenuClick(eventKey);
return R.ok(response);
} catch (Exception e) {
log.error("处理菜单点击事件失败: {}", e.getMessage(), e);
return R.fail("处理菜单点击事件失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,235 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.mp.api.WxMpService;
import org.apache.commons.codec.digest.DigestUtils;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
/**
* 微信公众号OAuth2控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/oauth2/{appid}")
public class WxMpOAuth2Controller extends BaseController {
private final WxMpService wxMpService;
/**
* 构建授权URL
*
* @param appid 公众号appid
* @param redirectUrl 重定向URL
* @param scope 授权作用域(snsapi_base静默授权,snsapi_userinfo用户信息授权)
* @param state 防CSRF攻击的随机字符串
* @return 授权URL
*/
@GetMapping("/buildAuthorizationUrl")
@SwitchoverCheck
public R<String> buildAuthorizationUrl(@PathVariable String appid,
@RequestParam String redirectUrl,
@RequestParam(defaultValue = "snsapi_userinfo") String scope,
@RequestParam String state) {
try {
String url = wxMpService.getOAuth2Service().buildAuthorizationUrl(redirectUrl, scope, state);
return R.ok(url);
} catch (Exception e) {
log.error("构建授权URL失败: {}", e.getMessage(), e);
return R.fail("构建授权URL失败: " + e.getMessage());
}
}
/**
* 获取访问令牌
*
* @param appid 公众号appid
* @param code 授权码
* @return 访问令牌信息
*/
@GetMapping("/getAccessToken")
@SwitchoverCheck
public R<WxOAuth2AccessToken> getAccessToken(@PathVariable String appid,
@RequestParam String code) {
try {
WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().getAccessToken(code);
return R.ok(accessToken);
} catch (Exception e) {
log.error("获取访问令牌失败: {}", e.getMessage(), e);
return R.fail("获取访问令牌失败: " + e.getMessage());
}
}
/**
* 刷新访问令牌
*
* @param appid 公众号appid
* @param refreshToken 刷新令牌
* @return 新的访问令牌信息
*/
@GetMapping("/refreshAccessToken")
@SwitchoverCheck
public R<WxOAuth2AccessToken> refreshAccessToken(@PathVariable String appid,
@RequestParam String refreshToken) {
try {
WxOAuth2AccessToken accessToken = wxMpService.getOAuth2Service().refreshAccessToken(refreshToken);
return R.ok(accessToken);
} catch (Exception e) {
log.error("刷新访问令牌失败: {}", e.getMessage(), e);
return R.fail("刷新访问令牌失败: " + e.getMessage());
}
}
/**
* 验证访问令牌
*
* @param appid 公众号appid
* @param accessToken 访问令牌
* @param openId 用户openid
* @return 验证结果
*/
@PostMapping("/validateAccessToken")
@SwitchoverCheck
public R<Boolean> validateAccessToken(@PathVariable String appid,
@RequestParam String accessToken,
@RequestParam String openId) {
try {
WxOAuth2AccessToken wxOAuth2AccessToken = new WxOAuth2AccessToken();
wxOAuth2AccessToken.setAccessToken(accessToken);
wxOAuth2AccessToken.setOpenId(openId);
boolean isValid = wxMpService.getOAuth2Service().validateAccessToken(wxOAuth2AccessToken);
return R.ok(isValid);
} catch (Exception e) {
log.error("验证访问令牌失败: {}", e.getMessage(), e);
return R.fail("验证访问令牌失败: " + e.getMessage());
}
}
/**
* 获取JSAPI签名
*
* @param appid 公众号appid
* @param url 当前网页URL
* @return JSAPI签名信息
*/
@GetMapping("/getSignature")
@SwitchoverCheck
public R<Object> getSignature(@PathVariable String appid,
@RequestParam String url) {
try {
String jsapiTicket = wxMpService.getTicket(TicketType.JSAPI);
String nonceStr = UUID.randomUUID().toString();
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String signature = DigestUtils.sha1Hex(
"jsapi_ticket=" + jsapiTicket +
"&noncestr=" + nonceStr +
"&timestamp=" + timestamp +
"&url=" + url
);
WxSignatureVo signatureVo = new WxSignatureVo();
signatureVo.setAppId(appid);
signatureVo.setNonceStr(nonceStr);
signatureVo.setTimestamp(timestamp);
signatureVo.setSignature(signature);
signatureVo.setUrl(url);
return R.ok(signatureVo);
} catch (Exception e) {
log.error("获取JSAPI签名失败: {}", e.getMessage(), e);
return R.fail("获取JSAPI签名失败: " + e.getMessage());
}
}
/**
* 获取用户信息
*
* @param appid 公众号appid
* @param accessToken 访问令牌
* @param openId 用户openid
* @param lang 语言
* @return 用户信息
*/
@GetMapping("/getUserInfo")
@SwitchoverCheck
public R<Object> getUserInfo(@PathVariable String appid,
@RequestParam String accessToken,
@RequestParam String openId,
@RequestParam(defaultValue = "zh_CN") String lang) {
try {
WxOAuth2AccessToken token = new WxOAuth2AccessToken();
token.setAccessToken(accessToken);
token.setOpenId(openId);
Object userInfo = wxMpService.getOAuth2Service().getUserInfo(token, lang);
return R.ok(userInfo);
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
return R.fail("获取用户信息失败: " + e.getMessage());
}
}
/**
* 签名VO类
*/
public static class WxSignatureVo {
private String appId;
private String nonceStr;
private String timestamp;
private String signature;
private String url;
// Getters and Setters
public String getAppId() {
return appId;
}
public void setAppId(String appId) {
this.appId = appId;
}
public String getNonceStr() {
return nonceStr;
}
public void setNonceStr(String nonceStr) {
this.nonceStr = nonceStr;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(String timestamp) {
this.timestamp = timestamp;
}
public String getSignature() {
return signature;
}
public void setSignature(String signature) {
this.signature = signature;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
}
}
@@ -0,0 +1,177 @@
package org.dromara.mp.controller;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxOAuth2UserInfo;
import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.config.WxMpConfigStorage;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
/**
* 微信公众号OAuth2授权回调控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/oauth2/redirect/{appid}")
public class WxMpOauth2RedirectController extends BaseController {
private final WxMpService wxMpService;
/**
* 微信授权回调处理
*
* @param appid 公众号appid
* @param code 授权码
* @param state 状态参数
* @param response HTTP响应
*/
@RequestMapping("/greet")
@SwitchoverCheck
public void greetUser(@PathVariable String appid,
@RequestParam String code,
@RequestParam String state,
HttpServletResponse response) {
try {
// 获取访问令牌
WxOAuth2AccessToken wxOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(code);
// 获取用户信息
WxOAuth2UserInfo user = wxMpService.getOAuth2Service().getUserInfo(wxOAuth2AccessToken, null);
log.info("OAuth2授权用户信息: {}", user);
// 获取配置信息
WxMpConfigStorage mpConfigStorage = wxMpService.getWxMpConfigStorage();
String redirectUrl = mpConfigStorage.getOauth2RedirectUrl();
// 构建重定向URL,携带用户信息
String redirectWithToken = redirectUrl + "?unionId=" + user.getUnionId() +
"&openId=" + user.getOpenid() +
"&mpAccessToken=" + wxOAuth2AccessToken.getAccessToken() +
"&mpRefreshToken=" + wxOAuth2AccessToken.getRefreshToken() +
"&nickname=" + user.getNickname() +
"&headImgUrl=" + user.getHeadImgUrl();
// 执行重定向
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", redirectWithToken);
} catch (Exception e) {
log.error("OAuth2授权回调处理失败: {}", e.getMessage(), e);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "授权处理失败");
} catch (IOException ioException) {
log.error("发送错误响应失败: {}", ioException.getMessage(), ioException);
}
}
}
/**
* 获取授权用户信息(API接口)
*
* @param appid 公众号appid
* @param code 授权码
* @param state 状态参数
* @return 用户信息
*/
@GetMapping("/userinfo")
@SwitchoverCheck
public R<WxOAuth2UserInfo> getUserInfo(@PathVariable String appid,
@RequestParam String code,
@RequestParam(required = false) String state) {
try {
// 获取访问令牌
WxOAuth2AccessToken wxOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(code);
// 获取用户信息
WxOAuth2UserInfo user = wxMpService.getOAuth2Service().getUserInfo(wxOAuth2AccessToken, null);
log.info("获取OAuth2用户信息: {}", user);
return R.ok(user);
} catch (Exception e) {
log.error("获取OAuth2用户信息失败: {}", e.getMessage(), e);
return R.fail("获取用户信息失败: " + e.getMessage());
}
}
/**
* 静默授权回调处理
*
* @param appid 公众号appid
* @param code 授权码
* @param state 状态参数
* @return 访问令牌信息
*/
@GetMapping("/silent")
@SwitchoverCheck
public R<WxOAuth2AccessToken> silentAuth(@PathVariable String appid,
@RequestParam String code,
@RequestParam(required = false) String state) {
try {
// 静默授权只获取访问令牌,不获取用户详细信息
WxOAuth2AccessToken wxOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(code);
log.info("静默授权成功,openId: {}", wxOAuth2AccessToken.getOpenId());
return R.ok(wxOAuth2AccessToken);
} catch (Exception e) {
log.error("静默授权失败: {}", e.getMessage(), e);
return R.fail("静默授权失败: " + e.getMessage());
}
}
/**
* 自定义授权回调处理
*
* @param appid 公众号appid
* @param code 授权码
* @param state 状态参数
* @param redirectUrl 自定义重定向URL
* @param response HTTP响应
*/
@RequestMapping("/custom")
@SwitchoverCheck
public void customRedirect(@PathVariable String appid,
@RequestParam String code,
@RequestParam(required = false) String state,
@RequestParam String redirectUrl,
HttpServletResponse response) {
try {
// 获取访问令牌
WxOAuth2AccessToken wxOAuth2AccessToken = wxMpService.getOAuth2Service().getAccessToken(code);
// 获取用户信息
WxOAuth2UserInfo user = wxMpService.getOAuth2Service().getUserInfo(wxOAuth2AccessToken, null);
log.info("自定义授权回调用户信息: {}", user);
// 构建自定义重定向URL
String customRedirectUrl = redirectUrl +
(redirectUrl.contains("?") ? "&" : "?") +
"openId=" + user.getOpenid() +
"&unionId=" + user.getUnionId() +
"&accessToken=" + wxOAuth2AccessToken.getAccessToken();
// 执行重定向
response.setStatus(HttpServletResponse.SC_FOUND);
response.setHeader("Location", customRedirectUrl);
} catch (Exception e) {
log.error("自定义授权回调处理失败: {}", e.getMessage(), e);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "授权处理失败");
} catch (IOException ioException) {
log.error("发送错误响应失败: {}", ioException.getMessage(), ioException);
}
}
}
}
@@ -0,0 +1,181 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpMessageRouter;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.apache.commons.lang3.StringUtils;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 微信公众号消息处理控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/portal/{appid}")
public class WxMpPortalController extends BaseController {
private final WxMpService wxMpService;
private final WxMpMessageRouter messageRouter;
/**
* 微信公众号服务器配置验证
*
* @param appid 公众号appid
* @param signature 微信加密签名
* @param timestamp 时间戳
* @param nonce 随机数
* @param echostr 随机字符串
* @return 验证结果
*/
@GetMapping(produces = "text/plain;charset=utf-8")
@SwitchoverCheck
public String authGet(@PathVariable String appid,
@RequestParam(name = "signature", required = false) String signature,
@RequestParam(name = "timestamp", required = false) String timestamp,
@RequestParam(name = "nonce", required = false) String nonce,
@RequestParam(name = "echostr", required = false) String echostr) {
log.info("接收到来自微信服务器的认证消息:[{}, {}, {}, {}]", signature, timestamp, nonce, echostr);
if (StringUtils.isAnyBlank(signature, timestamp, nonce, echostr)) {
log.error("请求参数非法,请核实!");
return "非法请求";
}
try {
if (wxMpService.checkSignature(timestamp, nonce, signature)) {
log.info("微信公众号验证成功");
return echostr;
} else {
log.error("微信公众号验证失败");
return "非法请求";
}
} catch (Exception e) {
log.error("微信公众号验证异常: {}", e.getMessage(), e);
return "验证异常";
}
}
/**
* 微信公众号消息处理
*
* @param appid 公众号appid
* @param requestBody 请求体
* @param signature 微信加密签名
* @param timestamp 时间戳
* @param nonce 随机数
* @param openid 用户openid
* @param encType 加密类型
* @param msgSignature 消息签名
* @param request HTTP请求
* @return 处理结果
*/
@PostMapping(produces = "application/xml; charset=UTF-8")
@SwitchoverCheck
public String post(@PathVariable String appid,
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam("openid") String openid,
@RequestParam(name = "encrypt_type", required = false) String encType,
@RequestParam(name = "msg_signature", required = false) String msgSignature) {
log.info("接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}], timestamp=[{}], nonce=[{}], requestBody=[\n{}\n] ]",
openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
try {
if (!wxMpService.checkSignature(timestamp, nonce, signature)) {
log.error("非法请求,可能属于伪造的请求!");
return "非法请求";
}
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;
} catch (Exception e) {
log.error("微信消息处理异常: {}", e.getMessage(), e);
return "";
}
}
/**
* 消息路由处理
*
* @param message 微信消息
* @return 回复消息
*/
private WxMpXmlOutMessage route(WxMpXmlMessage message) {
try {
return this.messageRouter.route(message);
} catch (Exception e) {
log.error("路由消息时出现异常!", e);
return null;
}
}
/**
* 获取微信公众号配置信息
*
* @param appid 公众号appid
* @return 配置信息
*/
@GetMapping("/config")
@SwitchoverCheck
public R<Object> getConfig(@PathVariable String appid) {
try {
Object config = wxMpService.getWxMpConfigStorage();
return R.ok(config);
} catch (Exception e) {
log.error("获取微信公众号配置失败: {}", e.getMessage(), e);
return R.fail("获取配置失败: " + e.getMessage());
}
}
/**
* 清理微信公众号配置缓存
*
* @param appid 公众号appid
* @return 清理结果
*/
@DeleteMapping("/config/cache")
@SwitchoverCheck
public R<Void> clearConfigCache(@PathVariable String appid) {
try {
// 这里可以添加清理缓存的逻辑
log.info("清理微信公众号配置缓存: {}", appid);
return R.ok();
} catch (Exception e) {
log.error("清理配置缓存失败: {}", e.getMessage(), e);
return R.fail("清理缓存失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,129 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.dromara.mp.service.WxMpQrCodeService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 微信公众号二维码管理控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/qrcode/{appid}")
public class WxMpQrCodeController extends BaseController {
private final WxMpService wxMpService;
private final WxMpQrCodeService wxMpQrCodeService;
/**
* 生成临时二维码
*
* @param appid 公众号appid
* @param expireSeconds 过期时间(秒)
* @param sceneStr 场景字符串
* @return 二维码ticket
*/
@PostMapping("/temp")
@SwitchoverCheck
public R<WxMpQrCodeTicket> createTempQrCode(@PathVariable String appid,
@RequestParam(defaultValue = "2592000") int expireSeconds,
@RequestParam int sceneStr) {
try {
WxMpQrCodeTicket ticket = wxMpQrCodeService.createTempQrCode(expireSeconds, sceneStr);
return R.ok(ticket);
} catch (Exception e) {
log.error("生成临时二维码失败: {}", e.getMessage(), e);
return R.fail("生成临时二维码失败: " + e.getMessage());
}
}
/**
* 生成永久二维码
*
* @param appid 公众号appid
* @param sceneStr 场景字符串
* @return 二维码ticket
*/
@PostMapping("/forever")
@SwitchoverCheck
public R<WxMpQrCodeTicket> createForeverQrCode(@PathVariable String appid,
@RequestParam String sceneStr) {
try {
WxMpQrCodeTicket ticket = wxMpQrCodeService.createForeverQrCode(sceneStr);
return R.ok(ticket);
} catch (Exception e) {
log.error("生成永久二维码失败: {}", e.getMessage(), e);
return R.fail("生成永久二维码失败: " + e.getMessage());
}
}
/**
* 获取二维码URL
*
* @param appid 公众号appid
* @param ticket 二维码ticket
* @return 二维码URL
*/
@GetMapping("/url")
@SwitchoverCheck
public R<String> getQrCodeUrl(@PathVariable String appid, @RequestParam String ticket) {
try {
String url = wxMpQrCodeService.getQrCodeUrl(ticket);
return R.ok(url);
} catch (Exception e) {
log.error("获取二维码URL失败: {}", e.getMessage(), e);
return R.fail("获取二维码URL失败: " + e.getMessage());
}
}
/**
* 生成活动二维码
*
* @param appid 公众号appid
* @param activityId 活动ID
* @return 二维码ticket
*/
@PostMapping("/activity")
@SwitchoverCheck
public R<WxMpQrCodeTicket> createActivityQrCode(@PathVariable String appid,
@RequestParam String activityId) {
try {
WxMpQrCodeTicket ticket = wxMpQrCodeService.createActivityQrCode(activityId);
return R.ok(ticket);
} catch (Exception e) {
log.error("生成活动二维码失败: {}", e.getMessage(), e);
return R.fail("生成活动二维码失败: " + e.getMessage());
}
}
/**
* 生成渠道二维码
*
* @param appid 公众号appid
* @param channelId 渠道ID
* @return 二维码ticket
*/
@PostMapping("/channel")
@SwitchoverCheck
public R<WxMpQrCodeTicket> createChannelQrCode(@PathVariable String appid,
@RequestParam String channelId) {
try {
WxMpQrCodeTicket ticket = wxMpQrCodeService.createChannelQrCode(channelId);
return R.ok(ticket);
} catch (Exception e) {
log.error("生成渠道二维码失败: {}", e.getMessage(), e);
return R.fail("生成渠道二维码失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,226 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.template.WxMpTemplate;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateIndustry;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateIndustryEnum;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.dromara.mp.service.WxMpTemplateService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 微信公众号模板消息控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/template/{appid}")
public class WxMpTemplateController extends BaseController {
private final WxMpService wxMpService;
private final WxMpTemplateService wxMpTemplateService;
/**
* 发送模板消息
*
* @param appid 公众号appid
* @param templateMessage 模板消息
* @return 发送结果
*/
@PostMapping("/send")
@SwitchoverCheck
public R<String> sendTemplateMessage(@PathVariable String appid,
@RequestBody WxMpTemplateMessage templateMessage) {
try {
String msgId = wxMpTemplateService.sendTemplateMessage(templateMessage);
return R.ok(msgId);
} catch (Exception e) {
log.error("发送模板消息失败: {}", e.getMessage(), e);
return R.fail("发送模板消息失败: " + e.getMessage());
}
}
/**
* 获取所有模板列表
*
* @param appid 公众号appid
* @return 模板列表
*/
@GetMapping("/list")
@SwitchoverCheck
public R<List<WxMpTemplate>> getAllTemplates(@PathVariable String appid) {
try {
List<WxMpTemplate> templates = wxMpTemplateService.getAllTemplates();
return R.ok(templates);
} catch (Exception e) {
log.error("获取模板列表失败: {}", e.getMessage(), e);
return R.fail("获取模板列表失败: " + e.getMessage());
}
}
/**
* 删除模板
*
* @param appid 公众号appid
* @param templateId 模板ID
* @return 删除结果
*/
@DeleteMapping("/delete/{templateId}")
@SwitchoverCheck
public R<Void> deleteTemplate(@PathVariable String appid, @PathVariable String templateId) {
try {
boolean result = wxMpTemplateService.deleteTemplate(templateId);
return result ? R.ok() : R.fail("删除模板失败");
} catch (Exception e) {
log.error("删除模板失败: {}", e.getMessage(), e);
return R.fail("删除模板失败: " + e.getMessage());
}
}
/**
* 获取行业信息
*
* @param appid 公众号appid
* @return 行业信息
*/
@GetMapping("/industry")
@SwitchoverCheck
public R<WxMpTemplateIndustry> getIndustry(@PathVariable String appid) {
try {
WxMpTemplateIndustry industry = wxMpService.getTemplateMsgService().getIndustry();
return R.ok(industry);
} catch (Exception e) {
log.error("获取行业信息失败: {}", e.getMessage(), e);
return R.fail("获取行业信息失败: " + e.getMessage());
}
}
/**
* 设置行业信息
*
* @param appid 公众号appid
* @param industryId1 行业代码1
* @param industryId2 行业代码2
* @return 设置结果
*/
@PostMapping("/industry")
@SwitchoverCheck
public R<Void> setIndustry(@PathVariable String appid,
@RequestParam String industryId1,
@RequestParam String industryId2) {
try {
WxMpTemplateIndustry wxMpTemplateIndustry = new WxMpTemplateIndustry();
wxMpTemplateIndustry.setPrimaryIndustry(WxMpTemplateIndustryEnum.valueOf(industryId1));
wxMpTemplateIndustry.setSecondIndustry(WxMpTemplateIndustryEnum.valueOf(industryId2));
wxMpService.getTemplateMsgService().setIndustry(wxMpTemplateIndustry);
return R.ok();
} catch (Exception e) {
log.error("设置行业信息失败: {}", e.getMessage(), e);
return R.fail("设置行业信息失败: " + e.getMessage());
}
}
/**
* 发送通知消息
*
* @param appid 公众号appid
* @param openid 用户openid
* @param templateId 模板ID
* @param url 跳转URL
* @param data 消息数据
* @return 发送结果
*/
@PostMapping("/send-notification")
@SwitchoverCheck
public R<String> sendNotification(@PathVariable String appid,
@RequestParam String openid,
@RequestParam String templateId,
@RequestParam String url,
@RequestBody Map<String, String> data) {
try {
String msgId = wxMpTemplateService.sendNotificationMessage(openid, templateId, url, data);
return R.ok(msgId);
} catch (Exception e) {
log.error("发送通知消息失败: {}", e.getMessage(), e);
return R.fail("发送通知消息失败: " + e.getMessage());
}
}
/**
* 发送订单消息
*
* @param appid 公众号appid
* @param openid 用户openid
* @param data 订单数据
* @return 发送结果
*/
@PostMapping("/send-order")
@SwitchoverCheck
public R<String> sendOrderMessage(@PathVariable String appid,
@RequestParam String openid,
@RequestBody Map<String, Object> data) {
try {
String msgId = wxMpTemplateService.sendOrderMessage(openid, null, null, null);
return R.ok(msgId);
} catch (Exception e) {
log.error("发送订单消息失败: {}", e.getMessage(), e);
return R.fail("发送订单消息失败: " + e.getMessage());
}
}
/**
* 发送审核结果消息
*
* @param appid 公众号appid
* @param openid 用户openid
* @param data 审核数据
* @return 发送结果
*/
@PostMapping("/send-audit-result")
@SwitchoverCheck
public R<String> sendAuditResult(@PathVariable String appid,
@RequestParam String openid,
@RequestBody Map<String, Object> data) {
try {
String msgId = wxMpTemplateService.sendAuditResultMessage(openid, null, null);
return R.ok(msgId);
} catch (Exception e) {
log.error("发送审核结果消息失败: {}", e.getMessage(), e);
return R.fail("发送审核结果消息失败: " + e.getMessage());
}
}
/**
* 发送活动通知
*
* @param appid 公众号appid
* @param openid 用户openid
* @param data 活动数据
* @return 发送结果
*/
@PostMapping("/send-activity")
@SwitchoverCheck
public R<String> sendActivityNotification(@PathVariable String appid,
@RequestParam String openid,
@RequestBody Map<String, Object> data) {
try {
String msgId = wxMpTemplateService.sendActivityNotificationMessage(openid, null, null, null);
return R.ok(msgId);
} catch (Exception e) {
log.error("发送活动通知失败: {}", e.getMessage(), e);
return R.fail("发送活动通知失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,251 @@
package org.dromara.mp.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import me.chanjar.weixin.mp.bean.result.WxMpUserList;
import me.chanjar.weixin.mp.bean.tag.WxUserTag;
import org.dromara.common.core.domain.R;
import org.dromara.common.web.core.BaseController;
import org.dromara.mp.annotation.SwitchoverCheck;
import org.dromara.mp.service.WxMpUserService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 微信公众号用户管理控制器
*
* @author pengles
*/
@Slf4j
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/wx/mp/user/{appid}")
public class WxMpUserController extends BaseController {
private final WxMpService wxMpService;
private final WxMpUserService wxMpUserService;
/**
* 获取用户信息
*
* @param appid 公众号appid
* @param openid 用户openid
* @return 用户信息
*/
@GetMapping("/info/{openid}")
@SwitchoverCheck
public R<WxMpUser> getUserInfo(@PathVariable String appid, @PathVariable String openid) {
try {
WxMpUser user = wxMpUserService.getUserInfo(openid);
return R.ok(user);
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage(), e);
return R.fail("获取用户信息失败: " + e.getMessage());
}
}
/**
* 获取用户列表
*
* @param appid 公众号appid
* @param nextOpenid 下一个openid
* @return 用户列表
*/
@GetMapping("/list")
@SwitchoverCheck
public R<WxMpUserList> getUserList(@PathVariable String appid,
@RequestParam(required = false) String nextOpenid) {
try {
return R.ok(wxMpUserService.getUserList(nextOpenid));
} catch (Exception e) {
log.error("获取用户列表失败: {}", e.getMessage(), e);
return R.fail("获取用户列表失败: " + e.getMessage());
}
}
/**
* 获取所有用户
*
* @param appid 公众号appid
* @return 所有用户openid列表
*/
@GetMapping("/all")
@SwitchoverCheck
public R<List<String>> getAllUsers(@PathVariable String appid) {
try {
List<String> allUsers = wxMpUserService.getAllUsers();
return R.ok(allUsers);
} catch (Exception e) {
log.error("获取所有用户失败: {}", e.getMessage(), e);
return R.fail("获取所有用户失败: " + e.getMessage());
}
}
/**
* 创建标签
*
* @param appid 公众号appid
* @param tagName 标签名称
* @return 创建结果
*/
@PostMapping("/tag/create")
@SwitchoverCheck
public R<WxUserTag> createTag(@PathVariable String appid, @RequestParam String tagName) {
try {
WxUserTag tag = wxMpUserService.createTag(tagName);
return R.ok(tag);
} catch (Exception e) {
log.error("创建标签失败: {}", e.getMessage(), e);
return R.fail("创建标签失败: " + e.getMessage());
}
}
/**
* 获取所有标签
*
* @param appid 公众号appid
* @return 标签列表
*/
@GetMapping("/tag/list")
@SwitchoverCheck
public R<List<WxUserTag>> getTags(@PathVariable String appid) {
try {
List<WxUserTag> tags = wxMpUserService.getTags();
return R.ok(tags);
} catch (Exception e) {
log.error("获取标签列表失败: {}", e.getMessage(), e);
return R.fail("获取标签列表失败: " + e.getMessage());
}
}
/**
* 更新标签
*
* @param appid 公众号appid
* @param tagId 标签ID
* @param tagName 新标签名称
* @return 更新结果
*/
@PutMapping("/tag/{tagId}")
@SwitchoverCheck
public R<Void> updateTag(@PathVariable String appid,
@PathVariable Long tagId,
@RequestParam String tagName) {
try {
boolean result = wxMpUserService.updateTag(tagId, tagName);
return result ? R.ok() : R.fail("更新标签失败");
} catch (Exception e) {
log.error("更新标签失败: {}", e.getMessage(), e);
return R.fail("更新标签失败: " + e.getMessage());
}
}
/**
* 删除标签
*
* @param appid 公众号appid
* @param tagId 标签ID
* @return 删除结果
*/
@DeleteMapping("/tag/{tagId}")
@SwitchoverCheck
public R<Void> deleteTag(@PathVariable String appid, @PathVariable Long tagId) {
try {
boolean result = wxMpUserService.deleteTag(tagId);
return result ? R.ok() : R.fail("删除标签失败");
} catch (Exception e) {
log.error("删除标签失败: {}", e.getMessage(), e);
return R.fail("删除标签失败: " + e.getMessage());
}
}
/**
* 批量为用户打标签
*
* @param appid 公众号appid
* @param tagId 标签ID
* @param openidList 用户openid列表
* @return 操作结果
*/
@PostMapping("/tag/{tagId}/users")
@SwitchoverCheck
public R<Void> batchTagging(@PathVariable String appid,
@PathVariable Long tagId,
@RequestBody List<String> openidList) {
try {
boolean result = wxMpUserService.batchTagging(tagId, openidList);
return result ? R.ok() : R.fail("批量打标签失败");
} catch (Exception e) {
log.error("批量打标签失败: {}", e.getMessage(), e);
return R.fail("批量打标签失败: " + e.getMessage());
}
}
/**
* 批量为用户取消标签
*
* @param appid 公众号appid
* @param tagId 标签ID
* @param openidList 用户openid列表
* @return 操作结果
*/
@DeleteMapping("/tag/{tagId}/users")
@SwitchoverCheck
public R<Void> batchUntagging(@PathVariable String appid,
@PathVariable Long tagId,
@RequestBody List<String> openidList) {
try {
boolean result = wxMpUserService.batchUntagging(tagId, openidList);
return result ? R.ok() : R.fail("批量取消标签失败");
} catch (Exception e) {
log.error("批量取消标签失败: {}", e.getMessage(), e);
return R.fail("批量取消标签失败: " + e.getMessage());
}
}
/**
* 获取用户身上的标签列表
*
* @param appid 公众号appid
* @param openid 用户openid
* @return 用户标签列表
*/
@GetMapping("/tag/user/{openid}")
@SwitchoverCheck
public R<List<Long>> getUserTags(@PathVariable String appid, @PathVariable String openid) {
try {
List<Long> userTags = wxMpUserService.getUserTags(openid);
return R.ok(userTags);
} catch (Exception e) {
log.error("获取用户标签失败: {}", e.getMessage(), e);
return R.fail("获取用户标签失败: " + e.getMessage());
}
}
/**
* 获取标签下的用户列表
*
* @param appid 公众号appid
* @param tagId 标签ID
* @param nextOpenid 下一个openid
* @return 用户列表
*/
@GetMapping("/tag/{tagId}/users")
@SwitchoverCheck
public R<List<String>> getTagUsers(@PathVariable String appid,
@PathVariable Long tagId,
@RequestParam(required = false) String nextOpenid) {
try {
List<String> tagUsers = wxMpUserService.getTagUsers(tagId, nextOpenid);
return R.ok(tagUsers);
} catch (Exception e) {
log.error("获取标签用户列表失败: {}", e.getMessage(), e);
return R.fail("获取标签用户列表失败: " + e.getMessage());
}
}
}
@@ -0,0 +1,67 @@
package org.dromara.mp.handler;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpMessageHandler;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
import java.util.Map;
/**
* 微信公众号消息处理器抽象类
*
* @author pengles
*/
@Slf4j
public abstract class AbstractHandler implements WxMpMessageHandler {
/**
* 构建文本消息
*
* @param content 消息内容
* @param wxMessage 微信消息
* @param service 微信服务
* @return 文本消息
*/
protected WxMpXmlOutTextMessage buildTextMessage(String content, WxMpXmlMessage wxMessage, WxMpService service) {
return WxMpXmlOutTextMessage.TEXT().content(content)
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
/**
* 记录日志
*
* @param wxMessage 微信消息
*/
protected void logMessage(WxMpXmlMessage wxMessage) {
log.info("接收到微信消息:{}", wxMessage.toString());
}
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) {
logMessage(wxMessage);
return doHandle(wxMessage, context, wxMpService, sessionManager);
}
/**
* 具体的消息处理逻辑
*
* @param wxMessage 微信消息
* @param context 上下文
* @param wxMpService 微信服务
* @param sessionManager 会话管理器
* @return 回复消息
*/
protected abstract WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager);
}
@@ -0,0 +1,30 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author Peng Les
*/
@Component
public class KfSessionHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
//TODO 对会话做处理
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,52 @@
package org.dromara.mp.handler;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.dromara.mp.builder.WxMpTextBuilderWxMp;
import org.springframework.stereotype.Component;
import java.util.Map;
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
/**
* 地理位消息处理
*
* @author Peng Les
*/
@Slf4j
@Component
public class LocationHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
if (wxMessage.getMsgType().equals(XmlMsgType.LOCATION)) {
// TODO 接收处理用户发送的地理位置消息
try {
String content = "感谢反馈,您的的地理位置已收到!";
return new WxMpTextBuilderWxMp().build(content, wxMessage, null);
} catch (Exception e) {
return null;
}
}
//上报地理位置事件
log.info("上报地理位置,纬度 : {},经度 : {},精度 : {}",
wxMessage.getLatitude(), wxMessage.getLongitude(), wxMessage.getPrecision());
// TODO 可以将用户地理位置信息保存到本地数据库,以便以后使用
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,28 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author Peng Les
*/
@Component
public class LogHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,42 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
import static me.chanjar.weixin.common.api.WxConsts.EventType;
/**
* 自定义菜单事件
*
* @author Peng Les
*/
@Component
public class MenuHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService weixinService,
WxSessionManager sessionManager) {
String msg = String.format("type:%s, event:%s, key:%s",
wxMessage.getMsgType(), wxMessage.getEvent(),
wxMessage.getEventKey());
if (EventType.VIEW.equals(wxMessage.getEvent())) {
return null;
}
return WxMpXmlOutMessage.TEXT().content(msg)
.fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.build();
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,44 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 文本消息处理器
*
* @author pengles
*/
@Component
public class MsgHandler extends AbstractHandler {
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) {
String content = wxMessage.getContent();
// 简单的自动回复逻辑
String replyContent;
if ("你好".equals(content) || "hello".equalsIgnoreCase(content)) {
replyContent = "您好!欢迎关注我们的公众号!";
} else if ("帮助".equals(content) || "help".equalsIgnoreCase(content)) {
replyContent = "您可以发送以下关键词获取帮助:\n" +
"1. 你好 - 获取欢迎信息\n" +
"2. 帮助 - 获取帮助信息\n" +
"3. 联系我们 - 获取联系方式";
} else if ("联系我们".equals(content)) {
replyContent = "如需帮助,请联系客服:400-123-4567";
} else {
replyContent = "感谢您的消息,我们已收到。如需帮助,请回复'帮助'。";
}
return buildTextMessage(replyContent, wxMessage, wxMpService);
}
}
@@ -0,0 +1,32 @@
package org.dromara.mp.handler;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* @author Peng Les
*/
@Slf4j
@Component
public class NullHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
log.info("\n接收到请求消息,内容:{}", wxMessage.toString());
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,62 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 扫描事件处理
*
* @author Peng Les
*/
@Component
public class ScanHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> map,
WxMpService wxMpService, WxSessionManager wxSessionManager) {
return doHandle(wxMessage, map, wxMpService, wxSessionManager);
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
String eventKey = wxMessage.getEventKey();
String openId = wxMessage.getFromUser();
if (StringUtils.isBlank(eventKey) || StringUtils.isBlank(openId)) {
return buildTextMessage("参数缺失,绑定失败。", wxMessage, wxMpService);
}
// 处理二维码场景值
String scene = eventKey;
if (scene.startsWith("qrscene_")) {
scene = scene.substring("qrscene_".length());
}
if (!scene.startsWith("sys_user:")) {
return buildTextMessage("二维码场景不匹配,未执行绑定。", wxMessage, wxMpService);
}
String userIdStr = scene.substring("sys_user:".length());
Long userId = null;
try {
userId = Long.valueOf(userIdStr);
} catch (Exception e) {
return buildTextMessage("用户标识解析失败,绑定终止。", wxMessage, wxMpService);
}
String unionId = null;
try {
WxMpUser user = wxMpService.getUserService().userInfo(openId);
unionId = user.getUnionId();
} catch (Exception ignore) {
}
String msg = "绑定请求已识别。\nopenid" + openId +
(StringUtils.isNotBlank(unionId) ? ("\nunionId" + unionId) : "") +
"\n请在后台完成管理员微信绑定。";
return buildTextMessage(msg, wxMessage, wxMpService);
}
}
@@ -0,0 +1,32 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 门店审核事件处理
*
* @author Peng Les
*/
@Component
public class StoreCheckNotifyHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
// TODO 处理门店审核事件
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,34 @@
package org.dromara.mp.handler;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 关注事件处理器
*
* @author pengles
*/
@Component
public class SubscribeHandler extends AbstractHandler {
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage,
Map<String, Object> context,
WxMpService wxMpService,
WxSessionManager sessionManager) {
String welcomeMsg = "感谢您关注我们的公众号!\n\n" +
"您可以发送以下关键词获取帮助:\n" +
"• 你好 - 获取欢迎信息\n" +
"• 帮助 - 获取帮助信息\n" +
"• 联系我们 - 获取联系方式\n\n" +
"我们将为您提供优质的服务!";
return buildTextMessage(welcomeMsg, wxMessage, wxMpService);
}
}
@@ -0,0 +1,35 @@
package org.dromara.mp.handler;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 取消关注事件处理
*
* @author pengles
*/
@Slf4j
@Component
public class UnsubscribeHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
log.info("取消关注用户 openId: {}, unionId: {}", wxMessage.getFromUser(), wxMessage.getUnionId());
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,34 @@
package org.dromara.mp.handler;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 用户撤回授权处理
*
* @author: Peng Les
*/
@Slf4j
@Component
public class UserAuthorizationRevokeHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
log.info("用户撤回授权 openId: {}, unionId: {}", wxMessage.getFromUser(), wxMessage.getUnionId());
return null;
}
@Override
protected WxMpXmlOutMessage doHandle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService, WxSessionManager sessionManager) {
return null;
}
}
@@ -0,0 +1,84 @@
package org.dromara.mp.service;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialCountResult;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialFileBatchGetResult;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult;
import java.io.File;
import java.io.InputStream;
/**
* 微信公众号素材管理服务接口
*
* @author pengles
*/
public interface WxMpMaterialService {
/**
* 上传临时素材
*
* @param mediaType 媒体类型
* @param file 文件
* @return 上传结果
*/
WxMediaUploadResult uploadTempMedia(String mediaType, File file);
/**
* 下载临时素材
*
* @param mediaId 媒体ID
* @return 文件输入流
*/
InputStream downloadTempMedia(String mediaId);
/**
* 上传永久素材
*
* @param mediaType 媒体类型
* @param file 文件
* @return 上传结果
*/
WxMpMaterialUploadResult uploadMaterial(String mediaType, File file);
/**
* 删除永久素材
*
* @param mediaId 媒体ID
* @return 是否删除成功
*/
boolean deleteMaterial(String mediaId);
/**
* 获取永久素材
*
* @param mediaId 媒体ID
* @return 素材信息
*/
WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem getMaterial(String mediaId);
/**
* 获取素材列表
*
* @param type 素材类型
* @param offset 偏移量
* @param count 数量
* @return 素材列表
*/
WxMpMaterialFileBatchGetResult getMaterialList(String type, int offset, int count);
/**
* 获取素材总数
*
* @return 素材总数
*/
WxMpMaterialCountResult getMaterialCount();
/**
* 上传图文消息内的图片
*
* @param file 图片文件
* @return 图片URL
*/
String uploadImg(File file);
}
@@ -0,0 +1,50 @@
package org.dromara.mp.service;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.mp.bean.menu.WxMpMenu;
/**
* 微信公众号菜单管理服务接口
*
* @author pengles
*/
public interface WxMpMenuService {
/**
* 创建菜单
*
* @param menu 菜单对象
* @return 是否成功
*/
boolean createMenu(WxMenu menu);
/**
* 删除菜单
*
* @return 是否成功
*/
boolean deleteMenu();
/**
* 查询菜单
*
* @return 菜单对象
*/
WxMpMenu getMenu();
/**
* 创建默认菜单
*
* @return 是否成功
*/
boolean createDefaultMenu();
/**
* 处理菜单点击事件
*
* @param eventKey 事件key
* @return 响应消息
*/
String handleMenuClick(String eventKey);
}
@@ -0,0 +1,123 @@
package org.dromara.mp.service;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
/**
* 微信公众号消息处理服务接口
*
* @author pengles
*/
public interface WxMpMessageService {
/**
* 处理文本消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleTextMessage(WxMpXmlMessage inMessage);
/**
* 处理图片消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleImageMessage(WxMpXmlMessage inMessage);
/**
* 处理语音消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleVoiceMessage(WxMpXmlMessage inMessage);
/**
* 处理视频消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleVideoMessage(WxMpXmlMessage inMessage);
/**
* 处理位置消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleLocationMessage(WxMpXmlMessage inMessage);
/**
* 处理链接消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleLinkMessage(WxMpXmlMessage inMessage);
/**
* 处理事件消息
*
* @param inMessage 接收到的消息
* @return 回复消息
*/
WxMpXmlOutMessage handleEventMessage(WxMpXmlMessage inMessage);
/**
* 自动回复消息
*
* @param keyword 关键词
* @param openId 用户openId
* @return 回复内容
*/
String getAutoReplyContent(String keyword, String openId);
/**
* 保存用户消息记录
*
* @param inMessage 接收到的消息
*/
void saveUserMessage(WxMpXmlMessage inMessage);
/**
* 保存回复消息记录
*
* @param outMessage 回复的消息
* @param openId 用户openId
*/
void saveReplyMessage(WxMpXmlOutMessage outMessage, String openId);
/**
* 获取默认回复内容
*
* @return 默认回复内容
*/
String getDefaultReplyContent();
/**
* 处理关注事件
*
* @param openId 用户openId
* @return 欢迎消息
*/
String handleSubscribeEvent(String openId);
/**
* 处理取消关注事件
*
* @param openId 用户openId
*/
void handleUnsubscribeEvent(String openId);
/**
* 处理扫码事件
*
* @param openId 用户openId
* @param sceneStr 场景值
* @return 回复内容
*/
String handleScanEvent(String openId, String sceneStr);
}
@@ -0,0 +1,76 @@
package org.dromara.mp.service;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
/**
* 微信公众号二维码生成服务接口
*
* @author pengles
*/
public interface WxMpQrCodeService {
/**
* 创建临时二维码
*
* @param sceneId 场景值ID
* @param expireSeconds 过期时间(秒)
* @return 二维码ticket
*/
WxMpQrCodeTicket createTempQrCode(int sceneId, int expireSeconds);
/**
* 创建永久二维码
*
* @param sceneStr 场景值字符串
* @return 二维码ticket
*/
WxMpQrCodeTicket createPermanentQrCode(String sceneStr);
/**
* 创建永久二维码(别名方法)
*
* @param sceneStr 场景值字符串
* @return 二维码ticket
*/
WxMpQrCodeTicket createForeverQrCode(String sceneStr);
/**
* 创建永久二维码
*
* @param sceneId 场景值ID
* @return 二维码ticket
*/
WxMpQrCodeTicket createPermanentQrCode(int sceneId);
/**
* 通过ticket换取二维码图片URL
*
* @param ticket 二维码ticket
* @return 二维码图片URL
*/
String getQrCodeUrl(String ticket);
/**
* 创建用户邀请二维码
*
* @param userId 用户ID
* @return 二维码ticket
*/
WxMpQrCodeTicket createUserInviteQrCode(String userId);
/**
* 创建活动推广二维码
*
* @param activityId 活动ID
* @return 二维码ticket
*/
WxMpQrCodeTicket createActivityQrCode(String activityId);
/**
* 创建渠道推广二维码
*
* @param channelId 渠道ID
* @return 二维码ticket
*/
WxMpQrCodeTicket createChannelQrCode(String channelId);
}
@@ -0,0 +1,158 @@
package org.dromara.mp.service;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
* 微信公众号数据统计服务接口
*
* @author pengles
*/
public interface WxMpStatisticsService {
/**
* 获取用户增减数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 用户增减数据
*/
List<Map<String, Object>> getUserSummary(LocalDate beginDate, LocalDate endDate);
/**
* 获取用户累计数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 用户累计数据
*/
List<Map<String, Object>> getUserCumulate(LocalDate beginDate, LocalDate endDate);
/**
* 获取图文群发每日数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 图文群发数据
*/
List<Map<String, Object>> getArticleSummary(LocalDate beginDate, LocalDate endDate);
/**
* 获取图文群发总数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 图文群发总数据
*/
List<Map<String, Object>> getArticleTotal(LocalDate beginDate, LocalDate endDate);
/**
* 获取图文统计数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 图文统计数据
*/
List<Map<String, Object>> getUserRead(LocalDate beginDate, LocalDate endDate);
/**
* 获取图文统计分时数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 图文统计分时数据
*/
List<Map<String, Object>> getUserReadHour(LocalDate beginDate, LocalDate endDate);
/**
* 获取图文分享转发数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 图文分享转发数据
*/
List<Map<String, Object>> getUserShare(LocalDate beginDate, LocalDate endDate);
/**
* 获取图文分享转发分时数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 图文分享转发分时数据
*/
List<Map<String, Object>> getUserShareHour(LocalDate beginDate, LocalDate endDate);
/**
* 获取消息发送概况数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 消息发送概况数据
*/
List<Map<String, Object>> getUpstreamMsg(LocalDate beginDate, LocalDate endDate);
/**
* 获取消息分送分时数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 消息分送分时数据
*/
List<Map<String, Object>> getUpstreamMsgHour(LocalDate beginDate, LocalDate endDate);
/**
* 获取消息关键词发送概况
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 消息关键词发送概况
*/
List<Map<String, Object>> getUpstreamMsgWeek(LocalDate beginDate, LocalDate endDate);
/**
* 获取菜单分析数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 菜单分析数据
*/
List<Map<String, Object>> getMenuSummary(LocalDate beginDate, LocalDate endDate);
/**
* 获取统计概览数据
*
* @return 统计概览
*/
Map<String, Object> getStatisticsOverview();
/**
* 获取用户画像数据
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @return 用户画像数据
*/
Map<String, Object> getUserPortrait(LocalDate beginDate, LocalDate endDate);
/**
* 获取热门内容排行
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @param limit 限制数量
* @return 热门内容排行
*/
List<Map<String, Object>> getPopularContent(LocalDate beginDate, LocalDate endDate, int limit);
/**
* 获取关键词统计
*
* @param beginDate 开始日期
* @param endDate 结束日期
* @param limit 限制数量
* @return 关键词统计
*/
List<Map<String, Object>> getKeywordStatistics(LocalDate beginDate, LocalDate endDate, int limit);
}
@@ -0,0 +1,81 @@
package org.dromara.mp.service;
import me.chanjar.weixin.mp.bean.template.WxMpTemplate;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import java.util.List;
import java.util.Map;
/**
* 微信公众号模板消息服务接口
*
* @author pengles
*/
public interface WxMpTemplateService {
/**
* 发送模板消息
*
* @param templateMessage 模板消息对象
* @return 消息ID
*/
String sendTemplateMessage(WxMpTemplateMessage templateMessage);
/**
* 获取所有模板列表
*
* @return 模板列表
*/
List<WxMpTemplate> getAllTemplates();
/**
* 删除模板
*
* @param templateId 模板ID
* @return 是否成功
*/
boolean deleteTemplate(String templateId);
/**
* 发送通知模板消息
*
* @param openId 用户openId
* @param templateId 模板ID
* @param url 跳转链接
* @param data 模板数据
* @return 消息ID
*/
String sendNotificationMessage(String openId, String templateId, String url, Map<String, String> data);
/**
* 发送订单模板消息
*
* @param openId 用户openId
* @param orderNo 订单号
* @param amount 金额
* @param status 状态
* @return 消息ID
*/
String sendOrderMessage(String openId, String orderNo, String amount, String status);
/**
* 发送审核结果模板消息
*
* @param openId 用户openId
* @param result 审核结果
* @param reason 审核原因
* @return 消息ID
*/
String sendAuditResultMessage(String openId, String result, String reason);
/**
* 发送活动通知模板消息
*
* @param openId 用户openId
* @param activityName 活动名称
* @param startTime 开始时间
* @param endTime 结束时间
* @return 消息ID
*/
String sendActivityNotificationMessage(String openId, String activityName, String startTime, String endTime);
}
@@ -0,0 +1,133 @@
package org.dromara.mp.service;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import me.chanjar.weixin.mp.bean.result.WxMpUserList;
import me.chanjar.weixin.mp.bean.tag.WxTagListUser;
import me.chanjar.weixin.mp.bean.tag.WxUserTag;
import java.util.List;
/**
* 微信公众号用户管理服务接口
*
* @author pengles
*/
public interface WxMpUserService {
/**
* 获取用户基本信息
*
* @param openId 用户openId
* @return 用户信息
*/
WxMpUser getUserInfo(String openId);
/**
* 获取用户列表
*
* @param nextOpenId 下一个openId,用于分页
* @return 用户列表
*/
WxMpUserList getUserList(String nextOpenId);
/**
* 获取所有用户列表
*
* @return 所有用户的openId列表
*/
List<String> getAllUsers();
/**
* 创建标签
*
* @param tagName 标签名称
* @return 标签对象
*/
WxUserTag createTag(String tagName);
/**
* 获取所有标签
*
* @return 标签列表
*/
List<WxUserTag> getTags();
/**
* 更新标签
*
* @param tagId 标签ID
* @param tagName 新标签名称
* @return 是否成功
*/
boolean updateTag(Long tagId, String tagName);
/**
* 删除标签
*
* @param tagId 标签ID
* @return 是否成功
*/
boolean deleteTag(Long tagId);
/**
* 给用户打标签
*
* @param tagId 标签ID
* @param openIds 用户openId列表
* @return 是否成功
*/
boolean tagUsers(Long tagId, String[] openIds);
/**
* 取消用户标签
*
* @param tagId 标签ID
* @param openIds 用户openId列表
* @return 是否成功
*/
boolean untagUsers(Long tagId, String[] openIds);
/**
* 获取用户的标签列表
*
* @param openId 用户openId
* @return 标签ID列表
*/
List<Long> getUserTags(String openId);
/**
* 获取标签下的用户列表
*
* @param tagId 标签ID
* @param nextOpenId 下一个openId,用于分页
* @return 用户列表
*/
WxTagListUser getUsersByTag(Long tagId, String nextOpenId);
/**
* 获取标签下的用户openId列表
*
* @param tagId 标签ID
* @param nextOpenid 下一个openid,用于分页
* @return 用户openId列表
*/
List<String> getTagUsers(Long tagId, String nextOpenid);
/**
* 批量为用户打标签
*
* @param tagId 标签ID
* @param openidList 用户openid列表
* @return 操作结果
*/
boolean batchTagging(Long tagId, List<String> openidList);
/**
* 批量为用户取消标签
*
* @param tagId 标签ID
* @param openidList 用户openid列表
* @return 操作结果
*/
boolean batchUntagging(Long tagId, List<String> openidList);
}
@@ -0,0 +1,139 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.material.*;
import org.dromara.mp.service.WxMpMaterialService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.InputStream;
/**
* 微信公众号素材管理服务实现类
*
* @author pengles
*/
@Service
public class WxMpMaterialServiceImpl implements WxMpMaterialService {
private static final Logger logger = LoggerFactory.getLogger(WxMpMaterialServiceImpl.class);
@Autowired
private WxMpService wxMpService;
@Override
public WxMediaUploadResult uploadTempMedia(String mediaType, File file) {
try {
WxMediaUploadResult result = wxMpService.getMaterialService().mediaUpload(mediaType, file);
logger.info("上传临时素材成功: {}", result.getMediaId());
return result;
} catch (WxErrorException e) {
logger.error("上传临时素材失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public InputStream downloadTempMedia(String mediaId) {
try {
File file = wxMpService.getMaterialService().mediaDownload(mediaId);
logger.info("下载临时素材成功: {}", mediaId);
return file.toURI().toURL().openStream();
} catch (Exception e) {
logger.error("下载临时素材失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpMaterialUploadResult uploadMaterial(String mediaType, File file) {
try {
WxMpMaterial material = new WxMpMaterial();
material.setFile(file);
material.setName(file.getName());
WxMpMaterialUploadResult result = wxMpService.getMaterialService().materialFileUpload(mediaType, material);
logger.info("上传永久素材成功: {}", result.getMediaId());
return result;
} catch (WxErrorException e) {
logger.error("上传永久素材失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public boolean deleteMaterial(String mediaId) {
try {
boolean result = wxMpService.getMaterialService().materialDelete(mediaId);
logger.info("删除永久素材成功: {}", mediaId);
return result;
} catch (WxErrorException e) {
logger.error("删除永久素材失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem getMaterial(String mediaId) {
try {
WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem item =
wxMpService.getMaterialService().materialFileBatchGet("news", 0, 1)
.getItems().stream()
.filter(i -> mediaId.equals(i.getMediaId()))
.findFirst()
.orElse(null);
if (item != null) {
logger.info("获取永久素材成功: {}", mediaId);
} else {
logger.warn("未找到永久素材: {}", mediaId);
}
return item;
} catch (WxErrorException e) {
logger.error("获取永久素材失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpMaterialFileBatchGetResult getMaterialList(String type, int offset, int count) {
try {
WxMpMaterialFileBatchGetResult result = wxMpService.getMaterialService().materialFileBatchGet(type, offset, count);
logger.info("获取素材列表成功: 类型={}, 数量={}", type, result.getItemCount());
return result;
} catch (WxErrorException e) {
logger.error("获取素材列表失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpMaterialCountResult getMaterialCount() {
try {
WxMpMaterialCountResult result = wxMpService.getMaterialService().materialCount();
logger.info("获取素材总数成功: 图片={}, 语音={}, 视频={}, 图文={}",
result.getImageCount(), result.getVoiceCount(), result.getVideoCount(), result.getNewsCount());
return result;
} catch (WxErrorException e) {
logger.error("获取素材总数失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public String uploadImg(File file) {
try {
WxMediaImgUploadResult result = wxMpService.getMaterialService().mediaImgUpload(file);
logger.info("上传图文消息图片成功: {}", result.getUrl());
return result.getUrl();
} catch (WxErrorException e) {
logger.error("上传图文消息图片失败: {}", e.getMessage(), e);
return null;
}
}
}
@@ -0,0 +1,120 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.common.bean.menu.WxMenuButton;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.menu.WxMpMenu;
import org.dromara.mp.service.WxMpMenuService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 微信公众号菜单管理服务实现类
*
* @author pengles
*/
@Service
public class WxMpMenuServiceImpl implements WxMpMenuService {
private static final Logger logger = LoggerFactory.getLogger(WxMpMenuServiceImpl.class);
@Autowired
private WxMpService wxMpService;
@Override
public boolean createMenu(WxMenu menu) {
try {
String result = wxMpService.getMenuService().menuCreate(menu);
logger.info("创建菜单成功: {}", result);
return true;
} catch (WxErrorException e) {
logger.error("创建菜单失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public boolean deleteMenu() {
try {
wxMpService.getMenuService().menuDelete();
logger.info("删除菜单成功");
return true;
} catch (WxErrorException e) {
logger.error("删除菜单失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public WxMpMenu getMenu() {
try {
WxMpMenu menu = wxMpService.getMenuService().menuGet();
logger.info("查询菜单成功");
return menu;
} catch (WxErrorException e) {
logger.error("查询菜单失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public boolean createDefaultMenu() {
WxMenu menu = new WxMenu();
// 一级菜单1
WxMenuButton button1 = new WxMenuButton();
button1.setName("关于我们");
button1.setType("click");
button1.setKey("ABOUT_US");
// 一级菜单2
WxMenuButton button2 = new WxMenuButton();
button2.setName("服务中心");
// 二级菜单2-1
WxMenuButton subButton21 = new WxMenuButton();
subButton21.setName("在线客服");
subButton21.setType("click");
subButton21.setKey("ONLINE_SERVICE");
// 二级菜单2-2
WxMenuButton subButton22 = new WxMenuButton();
subButton22.setName("常见问题");
subButton22.setType("click");
subButton22.setKey("FAQ");
button2.getSubButtons().add(subButton21);
button2.getSubButtons().add(subButton22);
// 一级菜单3
WxMenuButton button3 = new WxMenuButton();
button3.setName("联系我们");
button3.setType("click");
button3.setKey("CONTACT_US");
menu.getButtons().add(button1);
menu.getButtons().add(button2);
menu.getButtons().add(button3);
return createMenu(menu);
}
@Override
public String handleMenuClick(String eventKey) {
switch (eventKey) {
case "ABOUT_US":
return "欢迎了解我们!\n\n我们致力于为用户提供优质的服务和产品。";
case "ONLINE_SERVICE":
return "客服服务时间:\n周一至周五 9:00-18:00\n\n如需帮助,请直接回复您的问题。";
case "FAQ":
return "常见问题:\n\n1. 如何使用我们的服务?\n2. 如何联系客服?\n3. 服务时间是什么?\n\n更多问题请咨询在线客服。";
case "CONTACT_US":
return "联系方式:\n\n客服电话:400-123-4567\n客服邮箱:service@example.com\n工作时间:周一至周五 9:00-18:00";
default:
return "感谢您的关注!";
}
}
}
@@ -0,0 +1,293 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutTextMessage;
import org.dromara.mp.service.WxMpMessageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 微信公众号消息处理服务实现类
*
* @author pengles
*/
@Service
public class WxMpMessageServiceImpl implements WxMpMessageService {
private static final Logger logger = LoggerFactory.getLogger(WxMpMessageServiceImpl.class);
// 自动回复关键词映射
private static final Map<String, String> AUTO_REPLY_MAP = new HashMap<>();
static {
AUTO_REPLY_MAP.put("帮助", "您好!我是智能客服助手,可以为您提供以下服务:\n\n1. 输入\"联系客服\"获取人工客服\n2. 输入\"常见问题\"查看FAQ\n3. 输入\"产品介绍\"了解我们的产品\n4. 输入\"使用教程\"获取使用指南");
AUTO_REPLY_MAP.put("联系客服", "客服工作时间:周一至周五 9:00-18:00\n\n如需人工客服,请拨打:400-123-4567\n或发送邮件至:service@example.com");
AUTO_REPLY_MAP.put("常见问题", "常见问题:\n\n1. 如何注册账号?\n2. 忘记密码怎么办?\n3. 如何修改个人信息?\n4. 如何联系客服?\n\n更多问题请输入具体问题关键词。");
AUTO_REPLY_MAP.put("产品介绍", "我们的产品特色:\n\n✅ 功能强大,操作简单\n✅ 安全可靠,数据保护\n✅ 7x24小时技术支持\n✅ 持续更新,免费升级\n\n了解更多请访问官网或联系客服。");
AUTO_REPLY_MAP.put("使用教程", "使用教程:\n\n📖 新手入门指南\n📖 功能操作说明\n📖 常见问题解答\n📖 视频教程\n\n详细教程请访问帮助中心或联系客服获取。");
}
@Override
public WxMpXmlOutMessage handleTextMessage(WxMpXmlMessage inMessage) {
String content = inMessage.getContent();
String openId = inMessage.getFromUser();
logger.info("收到文本消息: openId={}, content={}", openId, content);
// 保存用户消息
saveUserMessage(inMessage);
// 获取自动回复内容
String replyContent = getAutoReplyContent(content, openId);
// 构建回复消息
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
// 保存回复消息
saveReplyMessage(outMessage, openId);
return outMessage;
}
@Override
public WxMpXmlOutMessage handleImageMessage(WxMpXmlMessage inMessage) {
String openId = inMessage.getFromUser();
logger.info("收到图片消息: openId={}, mediaId={}", openId, inMessage.getMediaId());
saveUserMessage(inMessage);
String replyContent = "感谢您发送的图片!如需帮助,请输入\"帮助\"查看可用功能。";
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
saveReplyMessage(outMessage, openId);
return outMessage;
}
@Override
public WxMpXmlOutMessage handleVoiceMessage(WxMpXmlMessage inMessage) {
String openId = inMessage.getFromUser();
logger.info("收到语音消息: openId={}, mediaId={}", openId, inMessage.getMediaId());
saveUserMessage(inMessage);
String replyContent = "收到您的语音消息!目前暂不支持语音识别,请发送文字消息或输入\"帮助\"查看可用功能。";
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
saveReplyMessage(outMessage, openId);
return outMessage;
}
@Override
public WxMpXmlOutMessage handleVideoMessage(WxMpXmlMessage inMessage) {
String openId = inMessage.getFromUser();
logger.info("收到视频消息: openId={}, mediaId={}", openId, inMessage.getMediaId());
saveUserMessage(inMessage);
String replyContent = "感谢您发送的视频!如需帮助,请输入\"帮助\"查看可用功能。";
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
saveReplyMessage(outMessage, openId);
return outMessage;
}
@Override
public WxMpXmlOutMessage handleLocationMessage(WxMpXmlMessage inMessage) {
String openId = inMessage.getFromUser();
logger.info("收到位置消息: openId={}, location=({}, {})",
openId, inMessage.getLocationX(), inMessage.getLocationY());
saveUserMessage(inMessage);
String replyContent = String.format("收到您的位置信息:\n纬度:%s\n经度:%s\n\n如需基于位置的服务,请联系客服。",
inMessage.getLocationX(), inMessage.getLocationY());
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
saveReplyMessage(outMessage, openId);
return outMessage;
}
@Override
public WxMpXmlOutMessage handleLinkMessage(WxMpXmlMessage inMessage) {
String openId = inMessage.getFromUser();
logger.info("收到链接消息: openId={}, url={}", openId, inMessage.getUrl());
saveUserMessage(inMessage);
String replyContent = "感谢您分享的链接!如需帮助,请输入\"帮助\"查看可用功能。";
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
saveReplyMessage(outMessage, openId);
return outMessage;
}
@Override
public WxMpXmlOutMessage handleEventMessage(WxMpXmlMessage inMessage) {
String openId = inMessage.getFromUser();
String event = inMessage.getEvent();
logger.info("收到事件消息: openId={}, event={}", openId, event);
String replyContent;
switch (event) {
case "subscribe":
replyContent = handleSubscribeEvent(openId);
break;
case "unsubscribe":
handleUnsubscribeEvent(openId);
return null; // 取消关注不回复消息
case "SCAN":
replyContent = handleScanEvent(openId, inMessage.getEventKey());
break;
case "CLICK":
replyContent = handleMenuClickEvent(inMessage.getEventKey());
break;
default:
replyContent = getDefaultReplyContent();
break;
}
if (replyContent != null) {
WxMpXmlOutTextMessage outMessage = WxMpXmlOutMessage.TEXT()
.content(replyContent)
.fromUser(inMessage.getToUser())
.toUser(openId)
.build();
saveReplyMessage(outMessage, openId);
return outMessage;
}
return null;
}
@Override
public String getAutoReplyContent(String keyword, String openId) {
// 精确匹配
if (AUTO_REPLY_MAP.containsKey(keyword)) {
return AUTO_REPLY_MAP.get(keyword);
}
// 模糊匹配
for (Map.Entry<String, String> entry : AUTO_REPLY_MAP.entrySet()) {
if (keyword.contains(entry.getKey()) || entry.getKey().contains(keyword)) {
return entry.getValue();
}
}
// 默认回复
return getDefaultReplyContent();
}
@Override
public void saveUserMessage(WxMpXmlMessage inMessage) {
// TODO: 保存用户消息到数据库
logger.info("保存用户消息: openId={}, msgType={}, content={}",
inMessage.getFromUser(), inMessage.getMsgType(), inMessage.getContent());
}
@Override
public void saveReplyMessage(WxMpXmlOutMessage outMessage, String openId) {
// TODO: 保存回复消息到数据库
logger.info("保存回复消息: openId={}, content={}", openId, outMessage.getMsgSignature());
}
@Override
public String getDefaultReplyContent() {
return "您好!欢迎关注我们!\n\n输入\"帮助\"查看可用功能,或直接描述您的问题,我们会尽快为您解答。";
}
@Override
public String handleSubscribeEvent(String openId) {
logger.info("用户关注: openId={}", openId);
String welcomeMessage = "🎉 欢迎关注我们!\n\n" +
"感谢您的关注,我们将为您提供优质的服务。\n\n" +
"💡 输入\"帮助\"查看可用功能\n" +
"📞 输入\"联系客服\"获取人工服务\n" +
"❓ 输入\"常见问题\"查看FAQ\n\n" +
"有任何问题随时联系我们!";
return welcomeMessage;
}
@Override
public void handleUnsubscribeEvent(String openId) {
logger.info("用户取消关注: openId={}", openId);
// TODO: 更新用户关注状态
}
@Override
public String handleScanEvent(String openId, String sceneStr) {
logger.info("用户扫码: openId={}, sceneStr={}", openId, sceneStr);
if (sceneStr.startsWith("invite_")) {
String inviteUserId = sceneStr.substring(7);
return String.format("感谢通过邀请码关注我们!\n邀请人ID%s\n\n您已获得新用户专属福利,详情请联系客服。", inviteUserId);
} else if (sceneStr.startsWith("activity_")) {
String activityId = sceneStr.substring(9);
return String.format("欢迎参与活动!\n活动ID%s\n\n活动详情和参与方式请联系客服了解。", activityId);
} else if (sceneStr.startsWith("channel_")) {
String channelId = sceneStr.substring(8);
return String.format("欢迎通过渠道关注我们!\n渠道ID%s\n\n感谢您的关注,有任何问题请随时联系。", channelId);
}
return "感谢扫码关注我们!如需帮助,请输入\"帮助\"查看可用功能。";
}
/**
* 处理菜单点击事件
*
* @param eventKey 事件key
* @return 回复内容
*/
private String handleMenuClickEvent(String eventKey) {
switch (eventKey) {
case "ABOUT_US":
return "关于我们\n\n我们是一家专注于为用户提供优质服务的公司,致力于通过技术创新改善用户体验。";
case "ONLINE_SERVICE":
return "在线客服\n\n客服工作时间:周一至周五 9:00-18:00\n\n请直接描述您的问题,我们会尽快回复。";
case "FAQ":
return AUTO_REPLY_MAP.get("常见问题");
case "CONTACT_US":
return AUTO_REPLY_MAP.get("联系客服");
default:
return getDefaultReplyContent();
}
}
}
@@ -0,0 +1,96 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.dromara.mp.service.WxMpQrCodeService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 微信公众号二维码生成服务实现类
*
* @author pengles
*/
@Service
public class WxMpQrCodeServiceImpl implements WxMpQrCodeService {
private static final Logger logger = LoggerFactory.getLogger(WxMpQrCodeServiceImpl.class);
@Autowired
private WxMpService wxMpService;
@Override
public WxMpQrCodeTicket createTempQrCode(int sceneId, int expireSeconds) {
try {
WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(sceneId, expireSeconds);
logger.info("创建临时二维码成功: sceneId={}, ticket={}", sceneId, ticket.getTicket());
return ticket;
} catch (WxErrorException e) {
logger.error("创建临时二维码失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpQrCodeTicket createPermanentQrCode(String sceneStr) {
try {
WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateLastTicket(sceneStr);
logger.info("创建永久二维码成功: sceneStr={}, ticket={}", sceneStr, ticket.getTicket());
return ticket;
} catch (WxErrorException e) {
logger.error("创建永久二维码失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpQrCodeTicket createPermanentQrCode(int sceneId) {
try {
WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateLastTicket(sceneId);
logger.info("创建永久二维码成功: sceneId={}, ticket={}", sceneId, ticket.getTicket());
return ticket;
} catch (WxErrorException e) {
logger.error("创建永久二维码失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpQrCodeTicket createForeverQrCode(String sceneStr) {
// 别名方法,直接调用createPermanentQrCode
return createPermanentQrCode(sceneStr);
}
@Override
public String getQrCodeUrl(String ticket) {
String url = null;
try {
url = wxMpService.getQrcodeService().qrCodePictureUrl(ticket);
} catch (WxErrorException e) {
throw new RuntimeException(e);
}
logger.info("获取二维码URL成功: ticket={}, url={}", ticket, url);
return url;
}
@Override
public WxMpQrCodeTicket createUserInviteQrCode(String userId) {
String sceneStr = "invite_" + userId;
return createPermanentQrCode(sceneStr);
}
@Override
public WxMpQrCodeTicket createActivityQrCode(String activityId) {
String sceneStr = "activity_" + activityId;
return createPermanentQrCode(sceneStr);
}
@Override
public WxMpQrCodeTicket createChannelQrCode(String channelId) {
String sceneStr = "channel_" + channelId;
return createPermanentQrCode(sceneStr);
}
}
@@ -0,0 +1,493 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.datacube.*;
import org.dromara.mp.service.WxMpStatisticsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
/**
* 微信公众号数据统计服务实现类
*
* @author pengles
*/
@Service
public class WxMpStatisticsServiceImpl implements WxMpStatisticsService {
private static final Logger logger = LoggerFactory.getLogger(WxMpStatisticsServiceImpl.class);
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Autowired
private WxMpService wxMpService;
@Override
public List<Map<String, Object>> getUserSummary(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeUserSummary> summaries = wxMpService.getDataCubeService()
.getUserSummary(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeUserSummary summary : summaries) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", summary.getRefDate());
data.put("userSource", summary.getUserSource());
data.put("newUser", summary.getNewUser());
data.put("cancelUser", summary.getCancelUser());
result.add(data);
}
logger.info("获取用户增减数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取用户增减数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUserCumulate(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeUserCumulate> cumulates = wxMpService.getDataCubeService()
.getUserCumulate(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeUserCumulate cumulate : cumulates) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", cumulate.getRefDate());
data.put("cumulateUser", cumulate.getCumulateUser());
result.add(data);
}
logger.info("获取用户累计数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取用户累计数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getArticleSummary(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeArticleResult> summaries = wxMpService.getDataCubeService()
.getArticleSummary(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeArticleResult summary : summaries) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", summary.getRefDate());
data.put("msgid", summary.getMsgId());
data.put("title", summary.getTitle());
data.put("intPageReadUser", summary.getIntPageReadUser());
data.put("intPageReadCount", summary.getIntPageReadCount());
data.put("oriPageReadUser", summary.getOriPageReadUser());
data.put("oriPageReadCount", summary.getOriPageReadCount());
data.put("shareUser", summary.getShareUser());
data.put("shareCount", summary.getShareCount());
data.put("addToFavUser", summary.getAddToFavUser());
data.put("addToFavCount", summary.getAddToFavCount());
result.add(data);
}
logger.info("获取图文群发每日数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取图文群发每日数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getArticleTotal(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeArticleTotal> totals = wxMpService.getDataCubeService()
.getArticleTotal(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeArticleTotal total : totals) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", total.getRefDate());
data.put("msgid", total.getMsgId());
data.put("title", total.getTitle());
data.put("details", total.getDetails());
result.add(data);
}
logger.info("获取图文群发总数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取图文群发总数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUserRead(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeArticleResult> reads = wxMpService.getDataCubeService()
.getUserRead(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeArticleResult read : reads) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", read.getRefDate());
data.put("userSource", read.getUserSource());
data.put("intPageReadUser", read.getIntPageReadUser());
data.put("intPageReadCount", read.getIntPageReadCount());
data.put("oriPageReadUser", read.getOriPageReadUser());
data.put("oriPageReadCount", read.getOriPageReadCount());
data.put("shareUser", read.getShareUser());
data.put("shareCount", read.getShareCount());
data.put("addToFavUser", read.getAddToFavUser());
data.put("addToFavCount", read.getAddToFavCount());
result.add(data);
}
logger.info("获取图文统计数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取图文统计数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUserReadHour(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeArticleResult> reads = wxMpService.getDataCubeService()
.getUserReadHour(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeArticleResult read : reads) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", read.getRefDate());
data.put("refHour", read.getRefHour());
data.put("userSource", read.getUserSource());
data.put("intPageReadUser", read.getIntPageReadUser());
data.put("intPageReadCount", read.getIntPageReadCount());
data.put("oriPageReadUser", read.getOriPageReadUser());
data.put("oriPageReadCount", read.getOriPageReadCount());
data.put("shareUser", read.getShareUser());
data.put("shareCount", read.getShareCount());
data.put("addToFavUser", read.getAddToFavUser());
data.put("addToFavCount", read.getAddToFavCount());
result.add(data);
}
logger.info("获取图文统计分时数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取图文统计分时数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUserShare(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeArticleResult> shares = wxMpService.getDataCubeService()
.getUserShare(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeArticleResult share : shares) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", share.getRefDate());
data.put("shareScene", share.getShareScene());
data.put("shareUser", share.getShareUser());
data.put("shareCount", share.getShareCount());
result.add(data);
}
logger.info("获取图文分享转发数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取图文分享转发数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUserShareHour(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeArticleResult> shares = wxMpService.getDataCubeService()
.getUserShareHour(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeArticleResult share : shares) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", share.getRefDate());
data.put("refHour", share.getRefHour());
data.put("shareScene", share.getShareScene());
data.put("shareUser", share.getShareUser());
data.put("shareCount", share.getShareCount());
result.add(data);
}
logger.info("获取图文分享转发分时数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取图文分享转发分时数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUpstreamMsg(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeMsgResult> msgs = wxMpService.getDataCubeService()
.getUpstreamMsg(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeMsgResult msg : msgs) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", msg.getRefDate());
data.put("msgType", msg.getMsgType());
data.put("msgUser", msg.getMsgUser());
data.put("msgCount", msg.getMsgCount());
result.add(data);
}
logger.info("获取消息发送概况数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取消息发送概况数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUpstreamMsgHour(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeMsgResult> msgs = wxMpService.getDataCubeService()
.getUpstreamMsgHour(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeMsgResult msg : msgs) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", msg.getRefDate());
data.put("refHour", msg.getRefHour());
data.put("msgType", msg.getMsgType());
data.put("msgUser", msg.getMsgUser());
data.put("msgCount", msg.getMsgCount());
result.add(data);
}
logger.info("获取消息分送分时数据成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取消息分送分时数据失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getUpstreamMsgWeek(LocalDate beginDate, LocalDate endDate) {
try {
List<WxDataCubeMsgResult> msgs = wxMpService.getDataCubeService()
.getUpstreamMsgWeek(Date.from(beginDate.atStartOfDay(ZoneId.systemDefault()).toInstant()), Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
List<Map<String, Object>> result = new ArrayList<>();
for (WxDataCubeMsgResult msg : msgs) {
Map<String, Object> data = new HashMap<>();
data.put("refDate", msg.getRefDate());
data.put("msgType", msg.getMsgType());
data.put("msgUser", msg.getMsgUser());
data.put("msgCount", msg.getMsgCount());
result.add(data);
}
logger.info("获取消息关键词发送概况(周)成功: {} - {}", beginDate, endDate);
return result;
} catch (WxErrorException e) {
logger.error("获取消息关键词发送概况(周)失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getMenuSummary(LocalDate beginDate, LocalDate endDate) {
// 微信API暂不支持菜单分析数据接口,这里提供模拟数据结构
List<Map<String, Object>> result = new ArrayList<>();
// 模拟菜单点击数据
Map<String, Object> menuData = new HashMap<>();
menuData.put("refDate", beginDate.format(DATE_FORMATTER));
menuData.put("menuId", "ABOUT_US");
menuData.put("menuName", "关于我们");
menuData.put("clickCount", 0);
menuData.put("clickUser", 0);
result.add(menuData);
logger.info("获取菜单分析数据(模拟): {} - {}", beginDate, endDate);
return result;
}
@Override
public Map<String, Object> getStatisticsOverview() {
Map<String, Object> overview = new HashMap<>();
try {
LocalDate today = LocalDate.now();
LocalDate yesterday = today.minusDays(1);
LocalDate weekAgo = today.minusDays(7);
// 获取用户数据
List<Map<String, Object>> userSummary = getUserSummary(yesterday, yesterday);
List<Map<String, Object>> userCumulate = getUserCumulate(yesterday, yesterday);
int newUsers = userSummary.stream()
.mapToInt(data -> (Integer) data.getOrDefault("newUser", 0))
.sum();
int cancelUsers = userSummary.stream()
.mapToInt(data -> (Integer) data.getOrDefault("cancelUser", 0))
.sum();
int totalUsers = userCumulate.stream()
.mapToInt(data -> (Integer) data.getOrDefault("cumulateUser", 0))
.max().orElse(0);
overview.put("newUsers", newUsers);
overview.put("cancelUsers", cancelUsers);
overview.put("totalUsers", totalUsers);
overview.put("netGrowth", newUsers - cancelUsers);
// 获取消息数据
List<Map<String, Object>> msgData = getUpstreamMsg(yesterday, yesterday);
int totalMessages = msgData.stream()
.mapToInt(data -> (Integer) data.getOrDefault("msgCount", 0))
.sum();
int activeUsers = msgData.stream()
.mapToInt(data -> (Integer) data.getOrDefault("msgUser", 0))
.sum();
overview.put("totalMessages", totalMessages);
overview.put("activeUsers", activeUsers);
// 计算活跃度
double activityRate = totalUsers > 0 ? (double) activeUsers / totalUsers * 100 : 0;
overview.put("activityRate", Math.round(activityRate * 100.0) / 100.0);
overview.put("updateTime", LocalDate.now().format(DATE_FORMATTER));
logger.info("获取统计概览数据成功");
} catch (Exception e) {
logger.error("获取统计概览数据失败: {}", e.getMessage(), e);
// 返回默认数据
overview.put("newUsers", 0);
overview.put("cancelUsers", 0);
overview.put("totalUsers", 0);
overview.put("netGrowth", 0);
overview.put("totalMessages", 0);
overview.put("activeUsers", 0);
overview.put("activityRate", 0.0);
overview.put("updateTime", LocalDate.now().format(DATE_FORMATTER));
}
return overview;
}
@Override
public Map<String, Object> getUserPortrait(LocalDate beginDate, LocalDate endDate) {
Map<String, Object> portrait = new HashMap<>();
try {
// 获取用户来源分析
List<Map<String, Object>> userSummary = getUserSummary(beginDate, endDate);
Map<String, Integer> sourceAnalysis = new HashMap<>();
for (Map<String, Object> data : userSummary) {
String source = (String) data.get("userSource");
int newUser = (Integer) data.getOrDefault("newUser", 0);
sourceAnalysis.put(source, sourceAnalysis.getOrDefault(source, 0) + newUser);
}
portrait.put("sourceAnalysis", sourceAnalysis);
// 获取消息类型分析
List<Map<String, Object>> msgData = getUpstreamMsg(beginDate, endDate);
Map<String, Integer> msgTypeAnalysis = new HashMap<>();
for (Map<String, Object> data : msgData) {
String msgType = (String) data.get("msgType");
int msgCount = (Integer) data.getOrDefault("msgCount", 0);
msgTypeAnalysis.put(msgType, msgTypeAnalysis.getOrDefault(msgType, 0) + msgCount);
}
portrait.put("msgTypeAnalysis", msgTypeAnalysis);
logger.info("获取用户画像数据成功: {} - {}", beginDate, endDate);
} catch (Exception e) {
logger.error("获取用户画像数据失败: {}", e.getMessage(), e);
portrait.put("sourceAnalysis", new HashMap<>());
portrait.put("msgTypeAnalysis", new HashMap<>());
}
return portrait;
}
@Override
public List<Map<String, Object>> getPopularContent(LocalDate beginDate, LocalDate endDate, int limit) {
try {
List<Map<String, Object>> articleData = getArticleSummary(beginDate, endDate);
// 按阅读量排序
articleData.sort((a, b) -> {
int readCountA = (Integer) a.getOrDefault("intPageReadCount", 0);
int readCountB = (Integer) b.getOrDefault("intPageReadCount", 0);
return Integer.compare(readCountB, readCountA);
});
// 限制返回数量
List<Map<String, Object>> result = articleData.stream()
.limit(limit)
.collect(ArrayList::new, (list, item) -> list.add(item), ArrayList::addAll);
logger.info("获取热门内容排行成功: {} - {}, limit={}", beginDate, endDate, limit);
return result;
} catch (Exception e) {
logger.error("获取热门内容排行失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public List<Map<String, Object>> getKeywordStatistics(LocalDate beginDate, LocalDate endDate, int limit) {
// 这里需要结合实际的消息记录数据库来统计关键词
// 目前提供模拟数据结构
List<Map<String, Object>> result = new ArrayList<>();
// 模拟关键词统计数据
String[] keywords = {"帮助", "客服", "产品", "价格", "使用", "问题", "联系", "服务"};
Random random = new Random();
for (int i = 0; i < Math.min(keywords.length, limit); i++) {
Map<String, Object> data = new HashMap<>();
data.put("keyword", keywords[i]);
data.put("count", random.nextInt(100) + 1);
data.put("userCount", random.nextInt(50) + 1);
result.add(data);
}
// 按使用次数排序
result.sort((a, b) -> {
int countA = (Integer) a.get("count");
int countB = (Integer) b.get("count");
return Integer.compare(countB, countA);
});
logger.info("获取关键词统计(模拟)成功: {} - {}, limit={}", beginDate, endDate, limit);
return result;
}
}
@@ -0,0 +1,134 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.template.WxMpTemplate;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateData;
import me.chanjar.weixin.mp.bean.template.WxMpTemplateMessage;
import org.dromara.mp.service.WxMpTemplateService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 微信公众号模板消息服务实现类
*
* @author pengles
*/
@Service
public class WxMpTemplateServiceImpl implements WxMpTemplateService {
private static final Logger logger = LoggerFactory.getLogger(WxMpTemplateServiceImpl.class);
@Autowired
private WxMpService wxMpService;
@Override
public String sendTemplateMessage(WxMpTemplateMessage templateMessage) {
try {
String msgId = wxMpService.getTemplateMsgService().sendTemplateMsg(templateMessage);
logger.info("发送模板消息成功: {}", msgId);
return msgId;
} catch (WxErrorException e) {
logger.error("发送模板消息失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public List<WxMpTemplate> getAllTemplates() {
try {
List<WxMpTemplate> templates = wxMpService.getTemplateMsgService().getAllPrivateTemplate();
logger.info("获取模板列表成功,模板数量: {}", templates.size());
return templates;
} catch (WxErrorException e) {
logger.error("获取模板列表失败: {}", e.getMessage(), e);
return new ArrayList<>();
}
}
@Override
public boolean deleteTemplate(String templateId) {
try {
boolean result = wxMpService.getTemplateMsgService().delPrivateTemplate(templateId);
logger.info("删除模板成功: {}", templateId);
return result;
} catch (WxErrorException e) {
logger.error("删除模板失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public String sendNotificationMessage(String openId, String templateId, String url, Map<String, String> data) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId)
.templateId(templateId)
.url(url)
.build();
// 添加模板数据
for (Map.Entry<String, String> entry : data.entrySet()) {
templateMessage.addData(new WxMpTemplateData(entry.getKey(), entry.getValue(), "#173177"));
}
return sendTemplateMessage(templateMessage);
}
@Override
public String sendOrderMessage(String openId, String orderNo, String amount, String status) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId)
.templateId("ORDER_TEMPLATE_ID") // 需要配置实际的模板ID
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "订单状态通知", "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword1", orderNo, "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword2", amount, "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword3", status, "#173177"));
templateMessage.addData(new WxMpTemplateData("remark", "感谢您的支持!", "#173177"));
return sendTemplateMessage(templateMessage);
}
@Override
public String sendAuditResultMessage(String openId, String result, String reason) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId)
.templateId("AUDIT_TEMPLATE_ID") // 需要配置实际的模板ID
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "审核结果通知", "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword1", result, "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword2", reason, "#173177"));
templateMessage.addData(new WxMpTemplateData("remark", "如有疑问,请联系客服。", "#173177"));
return sendTemplateMessage(templateMessage);
}
@Override
public String sendActivityNotificationMessage(String openId, String activityName, String startTime, String endTime) {
WxMpTemplateMessage templateMessage = WxMpTemplateMessage.builder()
.toUser(openId)
.templateId("ACTIVITY_TEMPLATE_ID") // 需要配置实际的模板ID
.build();
// 添加模板数据
templateMessage.addData(new WxMpTemplateData("first", "活动通知", "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword1", activityName, "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword2", startTime, "#173177"));
templateMessage.addData(new WxMpTemplateData("keyword3", endTime, "#173177"));
templateMessage.addData(new WxMpTemplateData("remark", "点击查看活动详情", "#173177"));
return sendTemplateMessage(templateMessage);
}
}
@@ -0,0 +1,199 @@
package org.dromara.mp.service.impl;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import me.chanjar.weixin.mp.bean.result.WxMpUserList;
import me.chanjar.weixin.mp.bean.tag.WxTagListUser;
import me.chanjar.weixin.mp.bean.tag.WxUserTag;
import org.dromara.mp.service.WxMpUserService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 微信公众号用户管理服务实现类
*
* @author pengles
*/
@Service
public class WxMpUserServiceImpl implements WxMpUserService {
private static final Logger logger = LoggerFactory.getLogger(WxMpUserServiceImpl.class);
@Autowired
private WxMpService wxMpService;
@Override
public WxMpUser getUserInfo(String openId) {
try {
WxMpUser user = wxMpService.getUserService().userInfo(openId);
logger.info("获取用户信息成功: {}", openId);
return user;
} catch (WxErrorException e) {
logger.error("获取用户信息失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxMpUserList getUserList(String nextOpenId) {
try {
WxMpUserList userList = wxMpService.getUserService().userList(nextOpenId);
logger.info("获取用户列表成功,数量: {}", userList.getCount());
return userList;
} catch (WxErrorException e) {
logger.error("获取用户列表失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public List<String> getAllUsers() {
try {
List<String> allUsers = wxMpService.getUserService().userList().getOpenids();
logger.info("获取所有用户成功,数量: {}", allUsers.size());
return allUsers;
} catch (WxErrorException e) {
logger.error("获取所有用户失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxUserTag createTag(String tagName) {
try {
WxUserTag tag = wxMpService.getUserTagService().tagCreate(tagName);
logger.info("创建标签成功: {}", tagName);
return tag;
} catch (WxErrorException e) {
logger.error("创建标签失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public List<WxUserTag> getTags() {
try {
List<WxUserTag> tags = wxMpService.getUserTagService().tagGet();
logger.info("获取标签列表成功,数量: {}", tags.size());
return tags;
} catch (WxErrorException e) {
logger.error("获取标签列表失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public boolean updateTag(Long tagId, String tagName) {
try {
boolean result = wxMpService.getUserTagService().tagUpdate(tagId, tagName);
logger.info("更新标签成功: {} -> {}", tagId, tagName);
return result;
} catch (WxErrorException e) {
logger.error("更新标签失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public boolean deleteTag(Long tagId) {
try {
boolean result = wxMpService.getUserTagService().tagDelete(tagId);
logger.info("删除标签成功: {}", tagId);
return result;
} catch (WxErrorException e) {
logger.error("删除标签失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public boolean tagUsers(Long tagId, String[] openIds) {
try {
boolean result = wxMpService.getUserTagService().batchTagging(tagId, openIds);
logger.info("批量打标签成功: {} 用户数: {}", tagId, openIds.length);
return result;
} catch (WxErrorException e) {
logger.error("批量打标签失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public boolean untagUsers(Long tagId, String[] openIds) {
try {
boolean result = wxMpService.getUserTagService().batchUntagging(tagId, openIds);
logger.info("批量取消标签成功: {} 用户数: {}", tagId, openIds.length);
return result;
} catch (WxErrorException e) {
logger.error("批量取消标签失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public List<Long> getUserTags(String openId) {
try {
List<Long> tags = wxMpService.getUserTagService().userTagList(openId);
logger.info("获取用户标签成功: {} 标签数: {}", openId, tags.size());
return tags;
} catch (WxErrorException e) {
logger.error("获取用户标签失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public WxTagListUser getUsersByTag(Long tagId, String nextOpenId) {
try {
WxTagListUser userList = wxMpService.getUserTagService().tagListUser(tagId, nextOpenId);
logger.info("获取标签下用户列表成功: {} 用户数: {}", tagId, userList.getCount());
return userList;
} catch (WxErrorException e) {
logger.error("获取标签下用户列表失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public List<String> getTagUsers(Long tagId, String nextOpenId) {
try {
WxTagListUser userList = wxMpService.getUserTagService().tagListUser(tagId, nextOpenId);
logger.info("获取标签下用户openId列表成功: {} 用户数: {}", tagId, userList.getCount());
return userList.getData().getOpenidList();
} catch (WxErrorException e) {
logger.error("获取标签下用户openId列表失败: {}", e.getMessage(), e);
return null;
}
}
@Override
public boolean batchTagging(Long tagId, List<String> openidList) {
try {
String[] openIds = openidList.toArray(new String[0]);
boolean result = wxMpService.getUserTagService().batchTagging(tagId, openIds);
logger.info("批量打标签成功: {} 用户数: {}", tagId, openIds.length);
return result;
} catch (WxErrorException e) {
logger.error("批量打标签失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public boolean batchUntagging(Long tagId, List<String> openidList) {
try {
String[] openIds = openidList.toArray(new String[0]);
boolean result = wxMpService.getUserTagService().batchUntagging(tagId, openIds);
logger.info("批量取消标签成功: {} 用户数: {}", tagId, openIds.length);
return result;
} catch (WxErrorException e) {
logger.error("批量取消标签失败: {}", e.getMessage(), e);
return false;
}
}
}
@@ -0,0 +1,365 @@
package org.dromara.mp.utils;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.regex.Pattern;
/**
* 微信公众号工具类
*
* @author pengles
*/
public class WxMpUtils {
private static final Logger logger = LoggerFactory.getLogger(WxMpUtils.class);
/**
* 手机号正则表达式
*/
private static final Pattern MOBILE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$");
/**
* 邮箱正则表达式
*/
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
/**
* 身份证号正则表达式
*/
private static final Pattern ID_CARD_PATTERN = Pattern.compile("^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");
/**
* 验证微信签名
*
* @param token 微信公众号token
* @param signature 微信签名
* @param timestamp 时间戳
* @param nonce 随机数
* @return 验证结果
*/
public static boolean checkSignature(String token, String signature, String timestamp, String nonce) {
String[] arr = new String[]{token, timestamp, nonce};
Arrays.sort(arr);
StringBuilder content = new StringBuilder();
for (String s : arr) {
content.append(s);
}
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] digest = md.digest(content.toString().getBytes());
StringBuilder hexStr = new StringBuilder();
for (byte b : digest) {
String shaHex = Integer.toHexString(b & 0xFF);
if (shaHex.length() < 2) {
hexStr.append(0);
}
hexStr.append(shaHex);
}
return hexStr.toString().equals(signature);
} catch (NoSuchAlgorithmException e) {
logger.error("SHA-1算法不存在", e);
return false;
}
}
/**
* 构建文本回复消息
*
* @param content 回复内容
* @param wxMessage 接收到的消息
* @return 回复消息
*/
public static WxMpXmlOutMessage buildTextMessage(String content, WxMpXmlMessage wxMessage) {
return WxMpXmlOutMessage.TEXT()
.content(content)
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
/**
* 构建图片回复消息
*
* @param mediaId 图片媒体ID
* @param wxMessage 接收到的消息
* @return 回复消息
*/
public static WxMpXmlOutMessage buildImageMessage(String mediaId, WxMpXmlMessage wxMessage) {
return WxMpXmlOutMessage.IMAGE()
.mediaId(mediaId)
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
/**
* 构建语音回复消息
*
* @param mediaId 语音媒体ID
* @param wxMessage 接收到的消息
* @return 回复消息
*/
public static WxMpXmlOutMessage buildVoiceMessage(String mediaId, WxMpXmlMessage wxMessage) {
return WxMpXmlOutMessage.VOICE()
.mediaId(mediaId)
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
/**
* 构建视频回复消息
*
* @param mediaId 视频媒体ID
* @param title 视频标题
* @param description 视频描述
* @param wxMessage 接收到的消息
* @return 回复消息
*/
public static WxMpXmlOutMessage buildVideoMessage(String mediaId, String title, String description, WxMpXmlMessage wxMessage) {
return WxMpXmlOutMessage.VIDEO()
.mediaId(mediaId)
.title(title)
.description(description)
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
/**
* 构建音乐回复消息
*
* @param title 音乐标题
* @param description 音乐描述
* @param musicUrl 音乐链接
* @param hqMusicUrl 高质量音乐链接
* @param thumbMediaId 缩略图媒体ID
* @param wxMessage 接收到的消息
* @return 回复消息
*/
public static WxMpXmlOutMessage buildMusicMessage(String title, String description, String musicUrl,
String hqMusicUrl, String thumbMediaId, WxMpXmlMessage wxMessage) {
return WxMpXmlOutMessage.MUSIC()
.title(title)
.description(description)
.musicUrl(musicUrl)
.hqMusicUrl(hqMusicUrl)
.thumbMediaId(thumbMediaId)
.fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser())
.build();
}
/**
* 判断是否为文本消息
*
* @param wxMessage 微信消息
* @return 是否为文本消息
*/
public static boolean isTextMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.TEXT.equals(wxMessage.getMsgType());
}
/**
* 判断是否为图片消息
*
* @param wxMessage 微信消息
* @return 是否为图片消息
*/
public static boolean isImageMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.IMAGE.equals(wxMessage.getMsgType());
}
/**
* 判断是否为语音消息
*
* @param wxMessage 微信消息
* @return 是否为语音消息
*/
public static boolean isVoiceMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.VOICE.equals(wxMessage.getMsgType());
}
/**
* 判断是否为视频消息
*
* @param wxMessage 微信消息
* @return 是否为视频消息
*/
public static boolean isVideoMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.VIDEO.equals(wxMessage.getMsgType()) ||
WxConsts.XmlMsgType.SHORTVIDEO.equals(wxMessage.getMsgType());
}
/**
* 判断是否为地理位置消息
*
* @param wxMessage 微信消息
* @return 是否为地理位置消息
*/
public static boolean isLocationMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.LOCATION.equals(wxMessage.getMsgType());
}
/**
* 判断是否为链接消息
*
* @param wxMessage 微信消息
* @return 是否为链接消息
*/
public static boolean isLinkMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.LINK.equals(wxMessage.getMsgType());
}
/**
* 判断是否为事件消息
*
* @param wxMessage 微信消息
* @return 是否为事件消息
*/
public static boolean isEventMessage(WxMpXmlMessage wxMessage) {
return WxConsts.XmlMsgType.EVENT.equals(wxMessage.getMsgType());
}
/**
* 判断是否为关注事件
*
* @param wxMessage 微信消息
* @return 是否为关注事件
*/
public static boolean isSubscribeEvent(WxMpXmlMessage wxMessage) {
return isEventMessage(wxMessage) && WxConsts.EventType.SUBSCRIBE.equals(wxMessage.getEvent());
}
/**
* 判断是否为取消关注事件
*
* @param wxMessage 微信消息
* @return 是否为取消关注事件
*/
public static boolean isUnsubscribeEvent(WxMpXmlMessage wxMessage) {
return isEventMessage(wxMessage) && WxConsts.EventType.UNSUBSCRIBE.equals(wxMessage.getEvent());
}
/**
* 判断是否为菜单点击事件
*
* @param wxMessage 微信消息
* @return 是否为菜单点击事件
*/
public static boolean isMenuClickEvent(WxMpXmlMessage wxMessage) {
return isEventMessage(wxMessage) && WxConsts.EventType.CLICK.equals(wxMessage.getEvent());
}
/**
* 判断是否为扫码事件
*
* @param wxMessage 微信消息
* @return 是否为扫码事件
*/
public static boolean isScanEvent(WxMpXmlMessage wxMessage) {
return isEventMessage(wxMessage) &&
(WxConsts.EventType.SCAN.equals(wxMessage.getEvent()) ||
(WxConsts.EventType.SUBSCRIBE.equals(wxMessage.getEvent()) && wxMessage.getEventKey() != null && wxMessage.getEventKey().startsWith("qrscene_")));
}
/**
* 验证手机号格式
*
* @param mobile 手机号
* @return 是否有效
*/
public static boolean isValidMobile(String mobile) {
return mobile != null && MOBILE_PATTERN.matcher(mobile).matches();
}
/**
* 验证邮箱格式
*
* @param email 邮箱
* @return 是否有效
*/
public static boolean isValidEmail(String email) {
return email != null && EMAIL_PATTERN.matcher(email).matches();
}
/**
* 验证身份证号格式
*
* @param idCard 身份证号
* @return 是否有效
*/
public static boolean isValidIdCard(String idCard) {
return idCard != null && ID_CARD_PATTERN.matcher(idCard).matches();
}
/**
* 获取扫码事件的场景值
*
* @param wxMessage 微信消息
* @return 场景值
*/
public static String getScanSceneValue(WxMpXmlMessage wxMessage) {
if (!isScanEvent(wxMessage)) {
return null;
}
String eventKey = wxMessage.getEventKey();
if (eventKey == null) {
return null;
}
// 如果是关注时扫码,eventKey格式为qrscene_场景值
if (eventKey.startsWith("qrscene_")) {
return eventKey.substring(8);
}
// 如果是已关注用户扫码,eventKey就是场景值
return eventKey;
}
/**
* 过滤敏感词
*
* @param content 原始内容
* @return 过滤后的内容
*/
public static String filterSensitiveWords(String content) {
if (content == null || content.trim().isEmpty()) {
return content;
}
// 这里可以接入敏感词过滤服务
// 简单示例:替换一些常见敏感词
String filtered = content;
String[] sensitiveWords = {"fuck", "shit", "damn"};
for (String word : sensitiveWords) {
filtered = filtered.replaceAll("(?i)" + word, "***");
}
return filtered;
}
/**
* 截取字符串,超出长度用省略号表示
*
* @param str 原始字符串
* @param maxLength 最大长度
* @return 截取后的字符串
*/
public static String truncateString(String str, int maxLength) {
if (str == null || str.length() <= maxLength) {
return str;
}
return str.substring(0, maxLength - 3) + "...";
}
}