feat: initial iShare project code

This commit is contained in:
purovps
2026-02-16 23:20:59 +08:00
parent 8c83a6fd46
commit 6f270a972e
1910 changed files with 218015 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
FROM pig4cloud/java:8-jre
MAINTAINER wangiegie@gmail.com
ENV TZ=Asia/Shanghai
ENV JAVA_OPTS="-Xms128m -Xmx256m -Djava.security.egd=file:/dev/./urandom"
RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN mkdir -p /pigx-mp-platform
WORKDIR /pigx-mp-platform
EXPOSE 6000
ADD ./target/pigx-mp-platform.jar ./
CMD sleep 180;java $JAVA_OPTS -jar pigx-mp-platform.jar

View File

@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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>com.pig4cloud</groupId>
<artifactId>pigx-visual</artifactId>
<version>5.2.0</version>
</parent>
<artifactId>pigx-mp-platform</artifactId>
<packaging>jar</packaging>
<description>微信公众号管理模块</description>
<properties>
<mock.version>1.1.2</mock.version>
</properties>
<dependencies>
<!--注册中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--配置中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--数据操作-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-data</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- mysql8 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<!-- ojdbc8 -->
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
</dependency>
<!--PG-->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<!-- druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!--common-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-core</artifactId>
</dependency>
<!--安全模块-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-xss</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-security</artifactId>
</dependency>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-log</artifactId>
</dependency>
<!-- sentinel-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-sentinel</artifactId>
</dependency>
<!--灰度支持-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-gray</artifactId>
</dependency>
<!--微信依赖-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
</dependency>
<!--随机昵称生成-->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>java-testdata-generator</artifactId>
<version>${mock.version}</version>
<exclusions>
<exclusion>
<artifactId>guava</artifactId>
<groupId>com.google.guava</groupId>
</exclusion>
</exclusions>
</dependency>
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--undertow容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp;
import com.pig4cloud.pigx.common.feign.annotation.EnablePigxFeignClients;
import com.pig4cloud.pigx.common.security.annotation.EnablePigxResourceServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @author lengleng
* @date 2019/03/25 微信公众号管理模块
*/
@EnablePigxFeignClients
@EnablePigxResourceServer
@EnableDiscoveryClient
@SpringBootApplication
public class PigxMpPlatformApplication {
public static void main(String[] args) {
SpringApplication.run(PigxMpPlatformApplication.class, args);
}
}

View File

@@ -0,0 +1,21 @@
package com.pig4cloud.pigx.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;
/**
* @author Binary Wang(https://github.com/binarywang)
*/
public abstract class AbstractBuilder {
/**
* 构建返回微信消息报文
* @param content
* @param wxMessage
* @param service
* @return
*/
public abstract WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage, WxMpService service);
}

View File

@@ -0,0 +1,22 @@
package com.pig4cloud.pigx.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 Binary Wang(https://github.com/binarywang)
*/
public class ImageBuilder extends AbstractBuilder {
@Override
public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage, WxMpService service) {
WxMpXmlOutImageMessage m = WxMpXmlOutMessage.IMAGE().mediaId(content).fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser()).build();
return m;
}
}

View File

@@ -0,0 +1,20 @@
package com.pig4cloud.pigx.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 Binary Wang(https://github.com/binarywang)
*/
public class TextBuilder extends AbstractBuilder {
@Override
public WxMpXmlOutMessage build(String content, WxMpXmlMessage wxMessage, WxMpService service) {
WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content(content).fromUser(wxMessage.getToUser())
.toUser(wxMessage.getFromUser()).build();
return m;
}
}

View File

@@ -0,0 +1,52 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.config;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.experimental.UtilityClass;
/**
* @author lengleng
* @date 2019/03/27 微信上下文工具类
*/
@UtilityClass
public class WxMpContextHolder {
private final ThreadLocal<String> THREAD_LOCAL_APPID = new TransmittableThreadLocal<>();
/**
* TTL 设置appId
* @param appId
*/
public void setAppId(String appId) {
THREAD_LOCAL_APPID.set(appId);
}
/**
* 获取TTL中的appId
* @return
*/
public String getAppId() {
return THREAD_LOCAL_APPID.get();
}
public void clear() {
THREAD_LOCAL_APPID.remove();
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright (c) 2018-2025, 云集汇通 All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the yunjihuitong.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*/
package com.pig4cloud.pigx.mp.config;
import me.chanjar.weixin.common.enums.TicketType;
import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author lengleng
* @date 2019/03/26
* <p>
* 微信SDK redis扩展
*/
public class WxMpInRedisConfigStorage extends WxMpDefaultConfigImpl {
private final static String ACCESS_TOKEN_KEY = "wechat:access_token_";
private final static String JSAPI_TICKET_KEY = "wechat:jsapi_ticket_";
private final static String CARDAPI_TICKET_KEY = "wechat:cardapi_ticket_";
private final RedisTemplate<String, String> redisTemplate;
private String accessTokenKey;
private String jsapiTicketKey;
private String cardapiTicketKey;
public WxMpInRedisConfigStorage(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 每个公众号生成独有的存储key
* @param appId
*/
@Override
public void setAppId(String appId) {
super.setAppId(appId);
this.accessTokenKey = ACCESS_TOKEN_KEY.concat(appId);
this.jsapiTicketKey = JSAPI_TICKET_KEY.concat(appId);
this.cardapiTicketKey = CARDAPI_TICKET_KEY.concat(appId);
}
@Override
public String getAccessToken() {
return redisTemplate.opsForValue().get(this.accessTokenKey);
}
@Override
public boolean isAccessTokenExpired() {
return redisTemplate.getExpire(accessTokenKey) < 2L;
}
@Override
public synchronized void updateAccessToken(String accessToken, int expiresInSeconds) {
redisTemplate.opsForValue().set(accessTokenKey, accessToken, expiresInSeconds - 200, TimeUnit.SECONDS);
}
@Override
public void expireAccessToken() {
redisTemplate.expire(this.accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public String getJsapiTicket() {
return redisTemplate.opsForValue().get(this.jsapiTicketKey);
}
@Override
public String getCardApiTicket() {
return redisTemplate.opsForValue().get(cardapiTicketKey);
}
@Override
public String getTicket(TicketType type) {
return redisTemplate.opsForValue().get(getTicketRedisKey(type));
}
@Override
public boolean isTicketExpired(TicketType type) {
return redisTemplate.getExpire(this.getTicketRedisKey(type)) < 2;
}
@Override
public synchronized void updateTicket(TicketType type, String jsapiTicket, int expiresInSeconds) {
redisTemplate.opsForValue().set(getTicketRedisKey(type), jsapiTicket, expiresInSeconds - 200, TimeUnit.SECONDS);
}
@Override
public void expireTicket(TicketType type) {
redisTemplate.expire(getTicketRedisKey(type), 0, TimeUnit.SECONDS);
}
private String getTicketRedisKey(TicketType type) {
return String.format("wechat:ticket:key:%s:%s", this.appId, type.getCode());
}
}

View File

@@ -0,0 +1,201 @@
package com.pig4cloud.pigx.mp.config;
import com.google.common.collect.Maps;
import com.pig4cloud.pigx.admin.api.feign.RemoteTenantService;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.core.util.RetOps;
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.handler.*;
import com.pig4cloud.pigx.mp.service.WxAccountService;
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.api.impl.WxMpServiceImpl;
import me.chanjar.weixin.mp.constant.WxMpEventConstants;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.annotation.Async;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static me.chanjar.weixin.common.api.WxConsts.*;
/**
* @author Binary Wang
* @author lengleng
* <p>
* 微信公众号基础配置,初始化配置
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class WxMpInitConfigRunner {
/**
* 保存 appid-router 的对应关系
*/
private static Map<String, WxMpMessageRouter> routers = Maps.newHashMap();
/**
* 保存 appid-mpservice 的对应关系
*/
private static Map<String, WxMpService> mpServices = Maps.newHashMap();
/**
* 保存 appid-tenantId 的对应关系
*/
private static final Map<String, Long> tenants = Maps.newHashMap();
private final RemoteTenantService tenantService;
private final WxAccountService accountService;
private final LogHandler logHandler;
private final NullHandler nullHandler;
private final KfSessionHandler kfSessionHandler;
private final StoreCheckNotifyHandler storeCheckNotifyHandler;
private final LocationHandler locationHandler;
private final MenuHandler menuHandler;
private final MsgHandler msgHandler;
private final UnsubscribeHandler unsubscribeHandler;
private final SubscribeHandler subscribeHandler;
private final ScanHandler scanHandler;
private final RedisTemplate redisTemplate;
@Async
@Order
@EventListener({ WebServerInitializedEvent.class })
public void WebServerInit() {
this.initServices();
}
public void initServices() {
// 获取全部租户 遍历所有租户对应的公众号列表
List<WxAccount> accountList = new ArrayList<>();
// @formatter:off
RetOps.of(tenantService.list(SecurityConstants.FROM_IN))
.getData()
.orElseGet(Collections::emptyList)
.forEach(
tenant -> TenantBroker.runAs(tenant.getId(), (id) -> accountList.addAll(accountService.list()))
);
// @formatter:on
mpServices = accountList.stream().map(a -> {
WxMpInRedisConfigStorage configStorage = new WxMpInRedisConfigStorage(redisTemplate);
configStorage.setAppId(a.getAppid());
configStorage.setSecret(a.getAppsecret());
configStorage.setToken(a.getToken());
configStorage.setAesKey(a.getAeskey());
WxMpService service = new WxMpServiceImpl();
service.setWxMpConfigStorage(configStorage);
routers.put(a.getAppid(), this.newRouter(service));
tenants.put(a.getAppid(), a.getTenantId());
return service;
}).collect(Collectors.toMap(s -> s.getWxMpConfigStorage().getAppId(), a -> a));
}
private WxMpMessageRouter newRouter(WxMpService wxMpService) {
final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
// 记录所有事件的日志 (异步执行)
newRouter.rule().handler(this.logHandler).next();
// 接收客服会话管理事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT)
.event(WxMpEventConstants.CustomerService.KF_CREATE_SESSION).handler(this.kfSessionHandler).end();
newRouter.rule().async(false).msgType(XmlMsgType.EVENT)
.event(WxMpEventConstants.CustomerService.KF_CLOSE_SESSION).handler(this.kfSessionHandler).end();
newRouter.rule().async(false).msgType(XmlMsgType.EVENT)
.event(WxMpEventConstants.CustomerService.KF_SWITCH_SESSION).handler(this.kfSessionHandler).end();
// 门店审核事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(WxMpEventConstants.POI_CHECK_NOTIFY)
.handler(this.storeCheckNotifyHandler).end();
// 自定义菜单事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(MenuButtonType.CLICK).handler(this.menuHandler)
.end();
// 点击菜单连接事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(MenuButtonType.VIEW).handler(this.nullHandler)
.end();
// 关注事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(EventType.SUBSCRIBE)
.handler(this.subscribeHandler).end();
// 取消关注事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(EventType.UNSUBSCRIBE)
.handler(this.unsubscribeHandler).end();
// 上报地理位置事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(EventType.LOCATION).handler(this.locationHandler)
.end();
// 接收地理位置消息
newRouter.rule().async(false).msgType(XmlMsgType.LOCATION).handler(this.locationHandler).end();
// 扫码事件
newRouter.rule().async(false).msgType(XmlMsgType.EVENT).event(EventType.SCAN).handler(this.scanHandler).end();
// 默认
newRouter.rule().async(false).handler(this.msgHandler).end();
return newRouter;
}
/**
* redis 监听配置,监听 mp_redis_reload_topic,重新加载配置
* @param redisConnectionFactory redis 配置
* @return
*/
@Bean
public RedisMessageListenerContainer redisContainer(RedisConnectionFactory redisConnectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
container.addMessageListener((message, bytes) -> {
log.warn("接收到重新加载公众号配置事件");
initServices();
}, new ChannelTopic(CacheConstants.MP_REDIS_RELOAD_TOPIC));
return container;
}
public static Map<String, Long> getTenants() {
return tenants;
}
public static Map<String, WxMpMessageRouter> getRouters() {
return routers;
}
public static Map<String, WxMpService> getMpServices() {
return mpServices;
}
}

View File

@@ -0,0 +1,36 @@
package com.pig4cloud.pigx.mp.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author lengleng
* @date 2020/4/25
* <p>
* 消息类型
*/
@Getter
@AllArgsConstructor
public enum MsgTypeEnum {
/**
* 用户发给公众号
*/
USER2MP("1", "用户发给公众号"),
/**
* 公众号发给用户
*/
MP2USER("2", "公众号发给用户");
/***
* 类型
*/
private String type;
/**
* 描述
*/
private String desc;
}

View File

@@ -0,0 +1,36 @@
package com.pig4cloud.pigx.mp.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author lengleng
* @date 2020/4/25
* <p>
* 匹配类型
*/
@Getter
@AllArgsConstructor
public enum ReplyMateEnum {
/**
* 完全匹配
*/
ALL("1", "完全匹配"),
/**
* 半匹配
*/
LIKE("2", "半匹配");
/***
* 类型
*/
private String type;
/**
* 描述
*/
private String desc;
}

View File

@@ -0,0 +1,55 @@
package com.pig4cloud.pigx.mp.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author lengleng
* @date 2020/4/25
* <p>
* 回复类型
*/
@Getter
@AllArgsConstructor
public enum ReplyTypeEnum {
/**
* 关注自动回复
*/
ATTENTION("1", "关注时回复"),
/**
* 用户消息回复
*/
MSG("2", "消息回复"),
/**
* 关键字回复
*/
KEYWORD("3", "关键词回复");
/***
* 类型
*/
private String type;
/**
* 描述
*/
private String desc;
/**
* 根据 type 查找枚举
* @param type
* @return
*/
public static ReplyTypeEnum getEnumByType(String type) {
for (ReplyTypeEnum value : ReplyTypeEnum.values()) {
if (value.getType().equals(type)) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,36 @@
package com.pig4cloud.pigx.mp.constant;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author lengleng
* @date 2020/4/26
* <p>
* 公众号订阅状态
*/
@Getter
@AllArgsConstructor
public enum SubStatusEnum {
/**
* 已关注
*/
SUBED("1", "已关注"),
/**
* 未关注
*/
UNSUB("0", "关注");
/***
* 类型
*/
private String type;
/**
* 描述
*/
private String desc;
}

View File

@@ -0,0 +1,160 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.constant.CacheConstants;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.service.WxAccountService;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 公众号账户
*
* @author lengleng
* @date 2019-03-26 22:07:53
*/
@RestController
@AllArgsConstructor
@RequestMapping("/wx-account")
public class WxAccountController {
private final WxAccountService wxAccountService;
private final RedisTemplate redisTemplate;
/**
* 分页查询
* @param page 分页对象
* @param wxAccount 公众号账户
* @return
*/
@GetMapping("/page")
public R getWxAccountPage(Page page, WxAccount wxAccount) {
LambdaQueryWrapper<WxAccount> wrapper = Wrappers.<WxAccount>lambdaQuery()
.like(StrUtil.isNotBlank(wxAccount.getName()), WxAccount::getName, wxAccount.getName())
.like(StrUtil.isNotBlank(wxAccount.getAccount()), WxAccount::getAccount, wxAccount.getAccount());
return R.ok(wxAccountService.page(page, wrapper));
}
/**
* 通过id查询公众号账户
* @param id id
* @return R
*/
@GetMapping("/{id}")
public R getById(@PathVariable("id") Long id) {
return R.ok(wxAccountService.getById(id));
}
/**
* 新增公众号账户
* @param wxAccount 公众号账户
* @return R
*/
@SysLog("新增公众号账户")
@PostMapping
@PreAuthorize("@pms.hasPermission('mp_wxaccount_add')")
public R save(@RequestBody WxAccount wxAccount) {
wxAccountService.save(wxAccount);
redisTemplate.convertAndSend(CacheConstants.MP_REDIS_RELOAD_TOPIC, "重新加载公众号配置");
return R.ok();
}
/**
* 修改公众号账户
* @param wxAccount 公众号账户
* @return R
*/
@SysLog("修改公众号账户")
@PutMapping
@PreAuthorize("@pms.hasPermission('mp_wxaccount_edit')")
public R updateById(@RequestBody WxAccount wxAccount) {
wxAccountService.updateById(wxAccount);
redisTemplate.convertAndSend(CacheConstants.MP_REDIS_RELOAD_TOPIC, "重新加载公众号配置");
return R.ok();
}
/**
* 通过id删除公众号账户
* @param id id
* @return R
*/
@SysLog("删除公众号账户")
@DeleteMapping("/{id}")
@PreAuthorize("@pms.hasPermission('mp_wxaccount_del')")
public R removeById(@PathVariable Long id) {
wxAccountService.removeById(id);
redisTemplate.convertAndSend(CacheConstants.MP_REDIS_RELOAD_TOPIC, "重新加载公众号配置");
return R.ok();
}
/**
* 生成公众号二维码
* @param appId
* @return
*/
@SysLog("生成公众号二维码")
@PostMapping("/qr/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wxaccount_add')")
public R qr(@PathVariable String appId) {
return wxAccountService.generateQr(appId);
}
/**
* 获取公众号列表
* @return
*/
@GetMapping("/list")
public R list(String name) {
LambdaQueryWrapper<WxAccount> wrapper = Wrappers.<WxAccount>lambdaQuery().like(StrUtil.isNotBlank(name),
WxAccount::getName, name);
return R.ok(wxAccountService.list(wrapper));
}
/**
* 获取公众号接口数据
* @param appId 公众号
* @param interval 时间间隔
* @return
*/
@GetMapping("/statistics")
public R statistics(String appId, String interval) {
return wxAccountService.statistics(appId, interval);
}
@SneakyThrows
@PostMapping("/clear-quota/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wxaccount_del')")
public R clearQuota(@PathVariable String appId) {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
wxMpService.clearQuota(appId);
return R.ok();
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.service.WxAccountFansService;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 微信公众号粉丝管理
*
* @author lengleng
* @date 2019-03-26 22:08:08
*/
@RestController
@AllArgsConstructor
@RequestMapping("/wx-account-fans")
public class WxAccountFansController {
private final WxAccountFansService wxAccountFansService;
/**
* 分页查询
* @param page 分页对象
* @param wxAccountFans 微信公众号粉丝
* @return
*/
@GetMapping("/page")
public R getWxAccountFansPage(Page page, WxAccountFans wxAccountFans) {
return R.ok(wxAccountFansService.getFansWithTagPage(page, wxAccountFans));
}
/**
* 通过id查询微信公众号粉丝
* @param id id
* @return R
*/
@GetMapping("/{id}")
public R getById(@PathVariable("id") Long id) {
return R.ok(wxAccountFansService.getById(id));
}
/**
* 修改微信公众号粉丝
* @param wxAccountFans 微信公众号粉丝
* @return R
*/
@SysLog("修改微信公众号粉丝")
@PutMapping
@PreAuthorize("@pms.hasPermission('mp_wxaccountfans_edit')")
public R updateById(@RequestBody WxAccountFans wxAccountFans) {
return R.ok(wxAccountFansService.updateFans(wxAccountFans));
}
/**
* 通过id删除微信公众号粉丝
* @param id id
* @return R
*/
@SysLog("删除微信公众号粉丝")
@DeleteMapping("/{id}")
@PreAuthorize("@pms.hasPermission('mp_wxaccountfans_del')")
public R removeById(@PathVariable Long id) {
return R.ok(wxAccountFansService.removeById(id));
}
/**
* 同步粉丝
* @param appId
* @return
*/
@PostMapping("/sync/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wxaccountfans_sync')")
public R sync(@PathVariable String appId) {
return R.ok(wxAccountFansService.syncAccountFans(appId));
}
/**
* 取消拉黑
* @param ids
* @param appId
* @return
*/
@PostMapping("/unblack/{appId}")
public R unblack(@RequestBody Long[] ids, @PathVariable String appId) {
return R.ok(wxAccountFansService.unblack(ids, appId));
}
@PostMapping("/black/{appId}")
public R black(@RequestBody Long[] ids, @PathVariable String appId) {
return R.ok(wxAccountFansService.black(ids, appId));
}
}

View File

@@ -0,0 +1,107 @@
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.entity.WxAccountTag;
import com.pig4cloud.pigx.mp.entity.dto.WxAccountTagDeleteDTO;
import com.pig4cloud.pigx.mp.service.WxAccountTagService;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;
/**
* 微信账户标签
*
* @author lishangbu
* @date 2021/12/31
*/
@RestController
@AllArgsConstructor
@RequestMapping("/wx-account-tag")
public class WxAccountTagController {
private final WxAccountTagService wxAccountTagService;
/**
* 分页查询
* @param page 分页对象
* @param wxAccountTag 账户标签查询条件
* @return
*/
@GetMapping("/page")
public R<Page<WxAccountTag>> getWxAccountTagPage(Page<WxAccountTag> page, WxAccountTag wxAccountTag) {
LambdaQueryWrapper<WxAccountTag> wrapper = Wrappers.<WxAccountTag>lambdaQuery()
.like(StrUtil.isNotBlank(wxAccountTag.getTag()), WxAccountTag::getTag, wxAccountTag.getTag())
.eq(StrUtil.isNotBlank(wxAccountTag.getWxAccountAppid()), WxAccountTag::getWxAccountAppid,
wxAccountTag.getWxAccountAppid());
return R.ok(wxAccountTagService.page(page, wrapper));
}
/**
* 列表查询
* @param wxAccountTag 账户标签查询条件
* @return
*/
@GetMapping("/list")
public R<List<WxAccountTag>> listWxAccountTags(WxAccountTag wxAccountTag) {
return R.ok(wxAccountTagService.list(Wrappers.query(wxAccountTag)));
}
/**
* 保存账户标签
* @param wxAccountTag 待保存的账户标签
* @return 包含保存成功后的账户标签的API调用结果
*/
@PostMapping
@PreAuthorize("@pms.hasPermission('mp_wx_account_tag_add')")
public R<WxAccountTag> saveAccountTag(@RequestBody @Valid WxAccountTag wxAccountTag) {
return R.ok(wxAccountTagService.saveAccountTag(wxAccountTag));
}
/**
* 修改账户标签
* @param wxAccountTag 待修改的账户标签
* @return 包含修改成功后的账户标签的API调用结果
*/
@PutMapping
@PreAuthorize("@pms.hasPermission('mp_wx_account_tag_edit')")
public R<WxAccountTag> updateAccountTag(@RequestBody @Valid WxAccountTag wxAccountTag) {
return R.ok(wxAccountTagService.updateAccountTag(wxAccountTag));
}
/**
* 删除
* @param wxAccountTag 标签
* @return R
*/
@DeleteMapping
@PreAuthorize("@pms.hasPermission('mp_wx_account_tag_del')")
public R<Boolean> removeAccountTagById(@RequestBody WxAccountTagDeleteDTO deleteDTO) {
deleteDTO.getIds().forEach(item -> {
WxAccountTag wxAccountTag = new WxAccountTag();
wxAccountTag.setId(item);
wxAccountTag.setWxAccountAppid(deleteDTO.getWxAccountAppid());
wxAccountTagService.removeAccountTagById(wxAccountTag);
});
return R.ok();
}
/**
* 同步粉丝
* @param appId
* @return
*/
@PostMapping("/sync/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wx_account_tag_sync')")
public R sync(@PathVariable String appId) {
return R.ok(wxAccountTagService.syncAccountTags(appId));
}
}

View File

@@ -0,0 +1,120 @@
package com.pig4cloud.pigx.mp.controller;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.constant.ReplyTypeEnum;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
import com.pig4cloud.pigx.mp.service.WxAutoReplyService;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 消息自动回复
*
* @author JL
* @date 2019-04-18 15:40:39
*/
@RestController
@AllArgsConstructor
@RequestMapping("/wx-auto-reply")
public class WxAutoReplyController {
private final WxAutoReplyService wxAutoReplyService;
/**
* 分页查询
* @param page 分页对象
* @param wxAutoReply 消息自动回复
* @return
*/
@GetMapping("/page")
public R getWxAutoReplyPage(Page page, WxAutoReply wxAutoReply) {
return R.ok(wxAutoReplyService.page(page, Wrappers.query(wxAutoReply)));
}
/**
* 通过id查询消息自动回复
* @param id id
* @return R
*/
@GetMapping("/{id}")
public R getById(@PathVariable("id") Long id) {
return R.ok(wxAutoReplyService.getById(id));
}
/**
* 新增消息自动回复
* @param wxAutoReply 消息自动回复
* @return R
*/
@PostMapping
@PreAuthorize("@pms.hasPermission('mp_wxautoreply_add')")
public R save(@RequestBody WxAutoReply wxAutoReply) {
this.jude(wxAutoReply);
return R.ok(wxAutoReplyService.save(wxAutoReply));
}
/**
* 修改消息自动回复
* @param wxAutoReply 消息自动回复
* @return R
*/
@PutMapping
@PreAuthorize("@pms.hasPermission('mp_wxautoreply_edit')")
public R updateById(@RequestBody WxAutoReply wxAutoReply) {
return R.ok(wxAutoReplyService.updateById(wxAutoReply));
}
/**
* 通过id删除消息自动回复
* @param id id
* @return R
*/
@DeleteMapping("/{id}")
@PreAuthorize("@pms.hasPermission('mp_wxautoreply_del')")
public R removeById(@PathVariable Long id) {
return R.ok(wxAutoReplyService.removeById(id));
}
/**
* //校验参数
* @param wxAutoReply
*/
public void jude(WxAutoReply wxAutoReply) {
if (ReplyTypeEnum.MSG.getType().equals(wxAutoReply.getType())) {
Wrapper<WxAutoReply> queryWrapper = Wrappers.<WxAutoReply>lambdaQuery().eq(WxAutoReply::getReqType,
wxAutoReply.getReqType());
List<WxAutoReply> list = wxAutoReplyService.list(queryWrapper);
if (wxAutoReply.getId() != null) {
if (list != null && list.size() == 1) {
if (!list.get(0).getId().equals(wxAutoReply.getId())) {
throw new RuntimeException("请求消息类型重复");
}
}
if (list != null && list.size() > 1) {
throw new RuntimeException("请求消息类型重复");
}
}
}
if (ReplyTypeEnum.KEYWORD.getType().equals(wxAutoReply.getType())) {
Wrapper<WxAutoReply> queryWrapper = Wrappers.<WxAutoReply>lambdaQuery()
.eq(WxAutoReply::getReqKey, wxAutoReply.getReqKey())
.eq(WxAutoReply::getRepType, wxAutoReply.getRepMate());
List<WxAutoReply> list = wxAutoReplyService.list(queryWrapper);
if (list != null && list.size() == 1) {
if (!list.get(0).getId().equals(wxAutoReply.getId())) {
throw new RuntimeException("关键词重复");
}
}
if (list != null && list.size() > 1) {
throw new RuntimeException("关键词重复");
}
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.entity.WxMsg;
import com.pig4cloud.pigx.mp.service.WxMsgService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 微信粉丝消息管理
*
* @author lengleng
* @date 2019-03-27 20:45:27
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/wx-fans-msg")
public class WxFansMsgController {
private final WxMsgService wxMsgService;
/**
* 分页查询
* @param page 分页对象
* @param msg 查询条件
*/
@GetMapping("/page")
public R getWxMsgPage(Page page, WxMsg msg) {
LambdaQueryWrapper<WxMsg> wrapper = Wrappers.<WxMsg>lambdaQuery()
.eq(StrUtil.isNotBlank(msg.getAppId()), WxMsg::getAppId, msg.getAppId())
.like(StrUtil.isNotBlank(msg.getNickName()), WxMsg::getNickName, msg.getNickName())
.eq(StrUtil.isNotBlank(msg.getType()), WxMsg::getType, msg.getType());
return R.ok(wxMsgService.page(page, wrapper));
}
/**
* 通过id查询微信消息
* @param id id
* @return R
*/
@GetMapping("/{id}")
public R getById(@PathVariable("id") Long id) {
return R.ok(wxMsgService.getById(id));
}
/**
* 新增微信消息
* @param wxMsg 微信消息
* @return R
*/
@PostMapping
@PreAuthorize("@pms.hasPermission('mp_wxmsg_add')")
public R save(@RequestBody WxMsg wxMsg) {
return wxMsgService.saveAndPushMsg(wxMsg);
}
/**
* 修改微信消息
* @param wxMsg 微信消息
* @return R
*/
@PutMapping
@PreAuthorize("@pms.hasPermission('mp_wxmsg_edit')")
public R updateById(@RequestBody WxMsg wxMsg) {
return R.ok(wxMsgService.updateById(wxMsg));
}
/**
* 通过id删除微信消息
* @param id id
* @return R
*/
@DeleteMapping("/{id}")
@PreAuthorize("@pms.hasPermission('mp_wxmsg_del')")
public R removeById(@PathVariable String id) {
return R.ok(wxMsgService.removeById(id));
}
}

View File

@@ -0,0 +1,311 @@
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpDraftService;
import me.chanjar.weixin.mp.api.WxMpMaterialService;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.draft.WxMpAddDraft;
import me.chanjar.weixin.mp.bean.draft.WxMpDraftArticles;
import me.chanjar.weixin.mp.bean.draft.WxMpUpdateDraft;
import me.chanjar.weixin.mp.bean.material.WxMediaImgUploadResult;
import me.chanjar.weixin.mp.bean.material.WxMpMaterial;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialFileBatchGetResult;
import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 微信素材
*
* @author JL
* @date 2019-03-23 21:26:35
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/wx-material")
public class WxMaterialController {
/**
* 上传非图文微信素材
* @param mulFile
* @param mediaType
* @return
*/
@PostMapping("/materialFileUpload")
@PreAuthorize("@pms.hasPermission('mp_wxmaterial_add')")
public R materialFileUpload(@RequestPart("file") MultipartFile mulFile, @RequestParam String appId,
@RequestParam("title") String title, @RequestParam("introduction") String introduction,
@RequestParam("mediaType") String mediaType) {
try {
String originalFilename = new String(
Objects.requireNonNull(mulFile.getOriginalFilename()).getBytes(StandardCharsets.ISO_8859_1),
StandardCharsets.UTF_8);
WxMpMaterial material = new WxMpMaterial();
material.setName(originalFilename);
if (WxConsts.MediaFileType.VIDEO.equals(mediaType)) {
material.setVideoTitle(title);
material.setVideoIntroduction(introduction);
}
File file = FileUtil.newFile(FileUtil.getTmpDirPath() + originalFilename);
mulFile.transferTo(file);
material.setFile(file);
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
WxMpMaterialUploadResult wxMpMaterialUploadResult = wxMpMaterialService.materialFileUpload(mediaType,
material);
WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem wxMpMaterialFileBatchGetResult = new WxMpMaterialFileBatchGetResult.WxMaterialFileBatchGetNewsItem();
wxMpMaterialFileBatchGetResult.setName(file.getName());
wxMpMaterialFileBatchGetResult.setMediaId(wxMpMaterialUploadResult.getMediaId());
wxMpMaterialFileBatchGetResult.setUrl(wxMpMaterialUploadResult.getUrl());
FileUtil.del(file);
return R.ok(wxMpMaterialFileBatchGetResult);
}
catch (WxErrorException e) {
log.warn("上传非图文微信素材失败: {}", e.getLocalizedMessage());
return R.failed(e.getLocalizedMessage());
}
catch (Exception e) {
log.error("上传失败", e);
return R.failed(e.getLocalizedMessage());
}
}
/**
* 新增图文消息
* @param data
* @return
*/
@PostMapping("/materialNews")
@PreAuthorize("@pms.hasPermission('mp_wxmaterial_add')")
public R materialNewsUpload(@RequestBody JSONObject data) {
try {
JSONArray jSONArray = data.getJSONArray("articles");
List<WxMpDraftArticles> articles = jSONArray.toList(WxMpDraftArticles.class);
WxMpAddDraft draft = new WxMpAddDraft();
draft.setArticles(articles);
String appId = data.getStr("appId");
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpDraftService draftService = wxMpService.getDraftService();
return R.ok(draftService.addDraft(draft));
}
catch (WxErrorException e) {
log.warn("新增图文失败: {}", e.getMessage());
return R.failed(e.getMessage());
}
catch (Exception e) {
log.error("新增图文失败", e);
return R.failed(e.getLocalizedMessage());
}
}
/**
* 修改图文消息
* @param data
* @return
*/
@PutMapping("/materialNews")
@PreAuthorize("@pms.hasPermission('mp_wxmaterial_add')")
public R materialNewsUpdate(@RequestBody JSONObject data) {
try {
String mediaId = data.getStr("mediaId");
JSONArray jSONArray = data.getJSONArray("articles");
List<WxMpDraftArticles> articles = jSONArray.toList(WxMpDraftArticles.class);
String appId = data.getStr("appId");
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpDraftService draftService = wxMpService.getDraftService();
WxMpUpdateDraft mpUpdateDraft = new WxMpUpdateDraft();
mpUpdateDraft.setMediaId(mediaId);
int index = 0;
for (WxMpDraftArticles article : articles) {
mpUpdateDraft.setIndex(index);
mpUpdateDraft.setArticles(article);
draftService.updateDraft(mpUpdateDraft);
index++;
}
return R.ok();
}
catch (WxErrorException e) {
log.warn("修改图文失败:{}", e.getLocalizedMessage());
return R.failed(e.getLocalizedMessage());
}
catch (Exception e) {
log.error("修改图文失败", e);
return R.failed(e.getLocalizedMessage());
}
}
/**
* 上传图文消息内的图片获取URL
* @param mulFile
* @return
*/
@PostMapping("/newsImgUpload")
@PreAuthorize("@pms.hasPermission('mp_wxmaterial_add')")
public String newsImgUpload(@RequestParam("file") MultipartFile mulFile, @RequestParam String appId)
throws Exception {
File file = FileUtil.createTempFile(FileUtil.getTmpDir());
mulFile.transferTo(file);
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
WxMediaImgUploadResult wxMediaImgUploadResult = wxMpMaterialService.mediaImgUpload(file);
Map<Object, Object> responseData = new HashMap<>(2);
responseData.put("link", wxMediaImgUploadResult.getUrl());
FileUtil.del(file);
return JSONUtil.toJsonStr(responseData);
}
/**
* 通过id删除微信素材
* @param
* @return R
*/
@DeleteMapping
@PreAuthorize("@pms.hasPermission('mp_wxmaterial_del')")
public R materialDel(String appId, String id) {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
try {
return R.ok(wxMpMaterialService.materialDelete(id));
}
catch (WxErrorException e) {
log.error("删除微信素材失败", e);
return R.failed(e.getMessage());
}
}
/**
* 分页查询
* @param page 分页对象
* @param type
* @return
*/
@GetMapping("/page")
public R getWxMaterialPage(Page page, String type, String appId) {
try {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
int count = (int) page.getSize();
int offset = (int) page.getCurrent() * count - count;
if (WxConsts.MaterialType.NEWS.equals(type)) {
WxMpDraftService draftService = wxMpService.getDraftService();
return R.ok(draftService.listDraft(offset, count));
}
else {
return R.ok(wxMpMaterialService.materialFileBatchGet(type, offset, count));
}
}
catch (WxErrorException e) {
log.error("查询素材失败", e);
return R.failed(e.getLocalizedMessage());
}
}
/**
* 获取微信视频素材
* @param
* @return R
*/
@GetMapping("/materialVideo")
public R getMaterialVideo(String mediaId, String appId) {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
try {
return R.ok(wxMpMaterialService.materialVideoInfo(mediaId));
}
catch (WxErrorException e) {
log.error("获取微信视频素材失败", e);
return R.failed(e.getMessage());
}
}
/**
* 获取微信素材直接文件
* @param
* @return R
*/
@GetMapping("/materialOther")
public ResponseEntity<byte[]> getMaterialOther(String appId, String mediaId, String fileName) throws Exception {
try {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
// 获取文件
InputStream is = wxMpMaterialService.materialImageOrVoiceDownload(mediaId);
byte[] body = new byte[is.available()];
is.read(body);
HttpHeaders headers = new HttpHeaders();
// 设置文件类型
headers.add("Content-Disposition", "attchement;filename=" + URLEncoder.encode(fileName, "UTF-8"));
headers.add("Content-Type", "application/octet-stream");
HttpStatus statusCode = HttpStatus.OK;
// 返回数据
ResponseEntity<byte[]> entity = new ResponseEntity<>(body, headers, statusCode);
return entity;
}
catch (WxErrorException e) {
e.printStackTrace();
log.error("获取微信素材直接文件失败", e);
return null;
}
}
/**
* 获取微信临时素材直接文件
* @param
* @return R
*/
@GetMapping("/tempMaterialOther")
public ResponseEntity<byte[]> getTempMaterialOther(String appId, String mediaId, String fileName) throws Exception {
try {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMaterialService wxMpMaterialService = wxMpService.getMaterialService();
// 获取文件
InputStream is = new FileInputStream(wxMpMaterialService.mediaDownload(mediaId));
byte[] body = new byte[is.available()];
is.read(body);
HttpHeaders headers = new HttpHeaders();
// 设置文件类型
headers.add("Content-Disposition", "attchement;filename=" + URLEncoder.encode(fileName, "UTF-8"));
headers.add("Content-Type", "application/octet-stream");
HttpStatus statusCode = HttpStatus.OK;
// 返回数据
ResponseEntity<byte[]> entity = new ResponseEntity<>(body, headers, statusCode);
return entity;
}
catch (WxErrorException e) {
e.printStackTrace();
log.error("获取微信素材直接文件失败", e);
return null;
}
}
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.json.JSONObject;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.mp.service.WxMenuService;
import lombok.AllArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 微信菜单管理
*
* @author lengleng
* @date 2019-03-27 20:45:18
*/
@RestController
@AllArgsConstructor
@RequestMapping("/wx-menu")
public class WxMenuController {
private final WxMenuService wxMenuService;
/**
* 通过appid查询微信菜单
* @param appId 公众号
* @return R
*/
@GetMapping("/{appId}")
public R getById(@PathVariable("appId") String appId) {
return wxMenuService.getByAppId(appId);
}
/**
* 新增微信菜单
* @param wxMenu 微信菜单列表
* @return R
*/
@SysLog("新增微信菜单")
@PostMapping("/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wxmenu_add')")
public R save(@RequestBody JSONObject wxMenu, @PathVariable String appId) {
return R.ok(wxMenuService.save(wxMenu, appId));
}
/**
* 发布微信菜单
* @param appId 公众号
* @return R
*/
@SysLog("发布微信菜单")
@PutMapping("/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wxmenu_push')")
public R updateById(@PathVariable String appId) {
return wxMenuService.push(appId);
}
@DeleteMapping("/{appId}")
@PreAuthorize("@pms.hasPermission('mp_wxmenu_del')")
public R delete(@PathVariable("appId") String appId) {
return wxMenuService.delete(appId);
}
}

View File

@@ -0,0 +1,144 @@
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pigx.common.core.constant.SecurityConstants;
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import com.pig4cloud.pigx.common.security.annotation.Inner;
import com.pig4cloud.pigx.common.xss.core.XssCleanIgnore;
import com.pig4cloud.pigx.mp.config.WxMpContextHolder;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.web.bind.annotation.*;
/**
* @author Binary Wang
* @author lengleng
* <p>
* 微信接入入口
*/
@Slf4j
@Inner(value = false)
@RestController
@AllArgsConstructor
@RequestMapping("/{appId}/portal")
public class WxPortalController {
/**
* 微信接入校验处理
* @param appId 多公众号标志位
* @param signature 微信签名
* @param timestamp 时间戳
* @param nonce 随机数
* @param echostr 随机字符串
* @return
*/
@XssCleanIgnore
@GetMapping(produces = "text/plain;charset=utf-8")
public String authGet(@PathVariable("appId") 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 (StrUtil.isAllBlank(signature, timestamp, nonce, echostr)) {
throw new IllegalArgumentException("请求参数非法,请核实!");
}
final WxMpService wxService = WxMpInitConfigRunner.getMpServices().get(appId);
if (wxService == null) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%d]的配置,请核实!", appId));
}
if (wxService.checkSignature(timestamp, nonce, signature)) {
return echostr;
}
return "非法请求";
}
/**
* 微信消息处理
* @param appId 多公众号标志位
* @param requestBody 请求报文体
* @param signature 微信签名
* @param encType 加签方式
* @param msgSignature 微信签名
* @param timestamp 时间戳
* @param nonce 随机数
* @return
*/
@XssCleanIgnore
@PostMapping(produces = "application/xml; charset=UTF-8")
public String post(@PathVariable("appId") 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) {
return TenantBroker.applyAs(() -> WxMpInitConfigRunner.getTenants().get(appId),
(id) -> handleMessage(appId, requestBody, signature, timestamp, nonce, openid, encType, msgSignature));
}
/**
* 微信消息处理
* @param appId 多公众号标志位
* @param requestBody 请求报文体
* @param signature 微信签名
* @param encType 加签方式
* @param msgSignature 微信签名
* @param timestamp 时间戳
* @param nonce 随机数
* @return
*/
public String handleMessage(String appId, String requestBody, String signature, String timestamp, String nonce,
String openid, String encType, String msgSignature) {
WxMpContextHolder.setAppId(appId);
final WxMpService wxService = WxMpInitConfigRunner.getMpServices().get(appId);
log.info(
"接收微信请求:[openid=[{}], [signature=[{}], encType=[{}], msgSignature=[{}],"
+ " timestamp=[{}], nonce=[{}], requestBody=[{}] ",
openid, signature, encType, msgSignature, timestamp, nonce, requestBody);
if (!wxService.checkSignature(timestamp, nonce, signature)) {
throw new IllegalArgumentException("非法请求,可能属于伪造的请求!");
}
String out = null;
// 明文模式
if (StrUtil.isBlank(encType)) {
WxMpXmlMessage inMessage = WxMpXmlMessage.fromXml(requestBody);
WxMpXmlOutMessage outMessage = WxMpInitConfigRunner.getRouters().get(appId).route(inMessage);
if (outMessage != null) {
out = outMessage.toXml();
}
}
// aes加密模式
if (SecurityConstants.AES.equalsIgnoreCase(encType)) {
WxMpXmlMessage inMessage = WxMpXmlMessage.fromEncryptedXml(requestBody, wxService.getWxMpConfigStorage(),
timestamp, nonce, msgSignature);
log.debug("消息解密后内容为:{} ", inMessage.toString());
WxMpXmlOutMessage outMessage = WxMpInitConfigRunner.getRouters().get(appId).route(inMessage);
if (outMessage != null) {
out = outMessage.toEncryptedXml(wxService.getWxMpConfigStorage());
}
}
log.debug("组装回复信息:{}", out);
WxMpContextHolder.clear();
return out;
}
}

View File

@@ -0,0 +1,111 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.pig4cloud.pigx.common.core.sensitive.Sensitive;
import com.pig4cloud.pigx.common.core.sensitive.SensitiveTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 公众号账户
*
* @author lengleng
* @date 2019-03-26 22:07:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxAccount extends Model<WxAccount> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 公众号名称
*/
private String name;
/**
* 公众号账户
*/
private String account;
/**
* 公众号appid
*/
private String appid;
/**
* 公众号密钥
*/
@Sensitive(type = SensitiveTypeEnum.KEY)
private String appsecret;
/**
* 公众号url
*/
private String url;
/**
* 公众号token
*/
private String token;
/**
* 加密密钥
*/
private String aeskey;
/**
* 二维码图片URL
*/
private String qrUrl;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除 -1已删除 0正常
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
/**
* 租户
*/
private Long tenantId;
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 微信公众号粉丝
*
* @author lengleng
* @date 2019-03-26 22:08:08
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxAccountFans extends Model<WxAccountFans> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 用户标识
*/
private String openid;
/**
* 订阅状态0未关注1已关注
*/
private String subscribeStatus;
/**
* 订阅时间
*/
private LocalDateTime subscribeTime;
/**
* 昵称
*/
private String nickname;
/**
* 性别1男2女0未知
*/
private String gender;
/**
* 语言
*/
private String language;
/**
* 国家
*/
private String country;
/**
* 省份
*/
private String province;
/**
* 城市
*/
private String city;
/**
* 头像地址
*/
private String headimgUrl;
/**
* 备注
*/
private String remark;
/**
* 微信公众号ID
*/
private Long wxAccountId;
/**
* 微信公众号appid
*/
private String wxAccountAppid;
/**
* 微信公众号名
*/
private String wxAccountName;
/**
* 标签ID列表
*/
private Long[] tagIds;
/**
* 是否被拉黑
*/
private Integer isBlack;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除 -1已删除 0正常
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}

View File

@@ -0,0 +1,86 @@
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import java.time.LocalDateTime;
/**
* 微信公众号账号标签
*
* @author lishangbu
* @date 2021/12/31
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxAccountTag extends Model<WxAccount> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 标签
*/
@NotBlank(message = "标签不能为空")
private String tag;
/**
* 标签ID 微信公众平台返回
*/
private Long tagId;
/**
* 微信公众号ID
*/
private Long wxAccountId;
/**
* 微信公众号appid
*/
private String wxAccountAppid;
/**
* 微信公众号名
*/
private String wxAccountName;
/**
* 创建者
*/
@TableField(fill = FieldFill.INSERT)
private String createBy;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新者
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateBy;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除 1已删除 0正常
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}

View File

@@ -0,0 +1,131 @@
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 消息自动回复
*
* @author JL
* @date 2019-04-18 15:40:39
*/
@Data
@TableName("wx_auto_reply")
@EqualsAndHashCode(callSuper = true)
public class WxAutoReply extends Model<WxAutoReply> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 公众号ID
*/
private String appId;
/**
* 备注
*/
private String remark;
/**
* 类型1、关注时回复2、消息回复3、关键词回复
*/
@NotNull(message = "类型不能为空")
private String type;
/**
* 关键词
*/
private String reqKey;
/**
* 请求消息类型text文本image图片voice语音video视频shortvideo小视频location地理位置
*/
private String reqType;
/**
* 回复消息类型text文本image图片voice语音video视频music音乐news图文
*/
@NotNull(message = "回复消息类型不能为空")
private String repType;
/**
* 回复类型文本匹配类型1、全匹配2、半匹配
*/
private String repMate;
/**
* 回复类型文本保存文字
*/
private String repContent;
/**
* 回复的素材名、视频和音乐的标题
*/
private String repName;
/**
* 回复类型imge、voice、news、video的mediaID或音乐缩略图的媒体id
*/
private String repMediaId;
/**
* 视频和音乐的描述
*/
private String repDesc;
/**
* 链接
*/
private String repUrl;
/**
* 高质量链接
*/
private String repHqUrl;
/**
* 缩略图的媒体id
*/
private String repThumbMediaId;
/**
* 缩略图url
*/
private String repThumbUrl;
/**
* 图文消息的内容
*/
private String content;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除 -1已删除 0正常
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 微信粉丝消息回复表
*
* @author lengleng
* @date 2019-03-27 20:45:48
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxFansMsgRes extends Model<WxFansMsgRes> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 粉丝消息ID
*/
private Long fansMsgId;
/**
* 回复内容
*/
private String resContent;
/**
* 用户ID
*/
private Long userId;
/**
* 用户名
*/
private String userName;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 微信菜单表
*
* @author lengleng
* @date 2019-03-27 20:45:18
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxMpMenu extends Model<WxMpMenu> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 菜单
*/
private String menu;
/**
* 微信公众号ID
*/
private Long wxAccountId;
/**
* 微信公众号appid
*/
private String wxAccountAppid;
/**
* 微信公众号名
*/
private String wxAccountName;
/**
* 是否发布 0 未发布 1 已发布
*/
private String pubFlag;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除 -1已删除 0正常
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}

View File

@@ -0,0 +1,170 @@
package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.time.LocalDateTime;
/**
* 微信消息
*
* @author JL
* @date 2019-05-28 16:12:10
*/
@Data
@TableName("wx_msg")
@EqualsAndHashCode(callSuper = true)
@Schema(description = "微信消息")
public class WxMsg extends Model<WxMsg> {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 备注
*/
private String remark;
/***
* 公众号ID
*/
private String appId;
/**
* 公众号名称
*/
private String appName;
/**
* 公众号logo
*/
private String appLogo;
/**
* 微信用户ID
*/
private Long wxUserId;
/**
* 昵称
*/
private String nickName;
/**
* 头像
*/
private String headimgUrl;
/**
* 微信openId
*/
private String openId;
/**
* 消息分类1、用户发给公众号2、公众号发给用户
*/
private String type;
/**
* 消息类型text文本image图片voice语音video视频shortvideo小视频location地理位置music音乐news图文event推送事件
*/
private String repType;
/**
* 事件类型subscribe关注unsubscribe取关CLICK、VIEW菜单事件
*/
private String repEvent;
/**
* 回复类型文本保存文字
*/
private String repContent;
/**
* 回复类型imge、voice、news、video的mediaID或音乐缩略图的媒体id
*/
private String repMediaId;
/**
* 回复的素材名、视频和音乐的标题
*/
private String repName;
/**
* 视频和音乐的描述
*/
private String repDesc;
/**
* 链接
*/
private String repUrl;
/**
* 高质量链接
*/
private String repHqUrl;
/**
* 图文消息的内容
*/
private String content;
/**
* 缩略图的媒体id
*/
private String repThumbMediaId;
/**
* 缩略图url
*/
private String repThumbUrl;
/**
* 地理位置维度
*/
private Double repLocationX;
/**
* 地理位置经度
*/
private Double repLocationY;
/**
* 地图缩放大小
*/
private Double repScale;
/**
* 已读标记01
*/
private String readFlag;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 是否删除 -1已删除 0正常
*/
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String delFlag;
}

View File

@@ -0,0 +1,14 @@
package com.pig4cloud.pigx.mp.entity.dto;
import lombok.Data;
import java.util.List;
@Data
public class WxAccountTagDeleteDTO {
private List<Long> ids;
private String wxAccountAppid;
}

View File

@@ -0,0 +1,23 @@
package com.pig4cloud.pigx.mp.entity.vo;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxAccountTag;
import lombok.Data;
import java.util.List;
/**
* 粉丝Vo 对象
*
* @author lengleng
* @date 2022/1/5
*/
@Data
public class WxAccountFansVo extends WxAccountFans {
/**
* 标签名称列表
*/
private List<WxAccountTag> tagList;
}

View File

@@ -0,0 +1,10 @@
package com.pig4cloud.pigx.mp.handler;
import me.chanjar.weixin.mp.api.WxMpMessageHandler;
/**
* @author Binary Wang(https://github.com/binarywang)
*/
public abstract class AbstractHandler implements WxMpMessageHandler {
}

View File

@@ -0,0 +1,23 @@
package com.pig4cloud.pigx.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 Binary Wang(https://github.com/binarywang)
*/
@Component
public class KfSessionHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
return null;
}
}

View File

@@ -0,0 +1,63 @@
package com.pig4cloud.pigx.mp.handler;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.mp.config.WxMpContextHolder;
import com.pig4cloud.pigx.mp.constant.ReplyTypeEnum;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxMsgMapper;
import com.pig4cloud.pigx.mp.service.WxAutoReplyService;
import lombok.AllArgsConstructor;
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.List;
import java.util.Map;
/**
* @author Binary Wang(https://github.com/binarywang)
*/
@Slf4j
@Component
@AllArgsConstructor
public class LocationHandler extends AbstractHandler {
private final WxAutoReplyService wxAutoReplyService;
private final WxAccountFansMapper wxAccountFansMapper;
private final WxAccountMapper wxAccountMapper;
private final WxMsgMapper msgMapper;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
// 上报地理位置事件
log.info("上报地理位置,纬度 : {},经度 : {},精度 : {}", wxMessage.getLatitude(), wxMessage.getLongitude(),
wxMessage.getPrecision());
// 发送关注消息
List<WxAutoReply> listWxAutoReply = wxAutoReplyService
.list(Wrappers.<WxAutoReply>query().lambda().eq(WxAutoReply::getType, ReplyTypeEnum.MSG.getType())
.eq(WxAutoReply::getAppId, WxMpContextHolder.getAppId())
.eq(WxAutoReply::getReqType, wxMessage.getMsgType()));
// 查询公众号 基本信息
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAccount, wxMessage.getToUser()));
// 查询粉丝基本信息
WxAccountFans fans = wxAccountFansMapper
.selectOne(Wrappers.<WxAccountFans>lambdaQuery().eq(WxAccountFans::getOpenid, wxMessage.getFromUser()));
return MsgHandler.getWxMpXmlOutMessage(wxMessage, listWxAutoReply, fans, msgMapper, wxAccount);
}
}

View File

@@ -0,0 +1,110 @@
package com.pig4cloud.pigx.mp.handler;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.mp.constant.MsgTypeEnum;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxMsg;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxMsgMapper;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
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 lengleng
* <p>
* 保存微信消息
*/
@Slf4j
@Component
@AllArgsConstructor
public class LogHandler extends AbstractHandler {
private final WxAccountFansMapper fansMapper;
private final WxAccountMapper accountMapper;
private final WxMsgMapper msgMapper;
@Override
@SneakyThrows
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
log.debug("接收到请求消息,内容:{}", wxMessage.getContent());
WxAccount wxAccount = accountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAccount, wxMessage.getToUser()));
WxAccountFans fans = fansMapper
.selectOne(Wrappers.<WxAccountFans>lambdaQuery().eq(WxAccountFans::getOpenid, wxMessage.getFromUser()));
if (fans == null) {
return null;
}
WxMsg wxMsg = new WxMsg();
wxMsg.setWxUserId(fans.getId());
wxMsg.setNickName(fans.getNickname());
wxMsg.setHeadimgUrl(fans.getHeadimgUrl());
wxMsg.setType(MsgTypeEnum.USER2MP.getType());
wxMsg.setRepEvent(wxMessage.getEvent());
wxMsg.setRepType(wxMessage.getMsgType());
wxMsg.setRepMediaId(wxMessage.getMediaId());
wxMsg.setOpenId(fans.getOpenid());
wxMsg.setAppId(wxAccount.getAppid());
wxMsg.setAppLogo(wxAccount.getQrUrl());
wxMsg.setAppName(wxAccount.getName());
if (WxConsts.XmlMsgType.TEXT.equals(wxMessage.getMsgType())) {
wxMsg.setRepContent(wxMessage.getContent());
}
if (WxConsts.MenuButtonType.VIEW.equalsIgnoreCase(wxMessage.getEvent())) {
wxMsg.setRepUrl(wxMessage.getEventKey());
}
if (WxConsts.MenuButtonType.CLICK.equalsIgnoreCase(wxMessage.getEvent())) {
wxMsg.setRepName(wxMessage.getEventKey());
}
if (WxConsts.XmlMsgType.VOICE.equals(wxMessage.getMsgType())) {
wxMsg.setRepName(wxMessage.getMediaId() + "." + wxMessage.getFormat());
wxMsg.setRepContent(wxMessage.getRecognition());
}
if (WxConsts.XmlMsgType.IMAGE.equals(wxMessage.getMsgType())) {
wxMsg.setRepUrl(wxMessage.getPicUrl());
}
if (WxConsts.XmlMsgType.LINK.equals(wxMessage.getMsgType())) {
wxMsg.setRepName(wxMessage.getTitle());
wxMsg.setRepDesc(wxMessage.getDescription());
wxMsg.setRepUrl(wxMessage.getUrl());
}
if (WxConsts.MediaFileType.FILE.equals(wxMessage.getMsgType())) {
wxMsg.setRepName(wxMessage.getTitle());
wxMsg.setRepDesc(wxMessage.getDescription());
}
if (WxConsts.XmlMsgType.VIDEO.equals(wxMessage.getMsgType())) {
wxMsg.setRepThumbMediaId(wxMessage.getThumbMediaId());
}
if (WxConsts.XmlMsgType.LOCATION.equals(wxMessage.getMsgType())) {
wxMsg.setRepLocationX(wxMessage.getLocationX());
wxMsg.setRepLocationY(wxMessage.getLocationY());
wxMsg.setRepScale(wxMessage.getScale());
wxMsg.setRepContent(wxMessage.getLabel());
}
msgMapper.insert(wxMsg);
log.debug("保存微信用户信息成功 {}", wxMsg);
return null;
}
}

View File

@@ -0,0 +1,135 @@
package com.pig4cloud.pigx.mp.handler;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.mp.config.WxMpContextHolder;
import com.pig4cloud.pigx.mp.entity.WxMpMenu;
import com.pig4cloud.pigx.mp.mapper.WxMenuMapper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
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.message.WxMpXmlOutNewsMessage;
import me.chanjar.weixin.mp.builder.outxml.*;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author Binary Wang(https://github.com/binarywang)
*/
@Slf4j
@Component
@AllArgsConstructor
public class MenuHandler extends AbstractHandler {
private final WxMenuMapper wxMenuMapper;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService weixinService,
WxSessionManager sessionManager) throws WxErrorException {
// 组装菜单回复消息
return getWxMpXmlOutMessage(wxMessage);
}
/**
* 组装菜单回复消息
* @param wxMessage
* @return
*/
public WxMpXmlOutMessage getWxMpXmlOutMessage(WxMpXmlMessage wxMessage) {
WxMpMenu wxMpMenu = wxMenuMapper.selectOne(
Wrappers.<WxMpMenu>lambdaQuery().eq(WxMpMenu::getWxAccountAppid, WxMpContextHolder.getAppId()));
List<JSONObject> buttons = JSONUtil.parseObj(wxMpMenu.getMenu()).getJSONArray("button")
.toList(JSONObject.class);
String eventKey = wxMessage.getEventKey();
// 获取子集
List<JSONObject> buttonList = new ArrayList<>();
for (JSONObject button : buttons) {
JSONArray subButton = button.getJSONArray("sub_button");
if (subButton.isEmpty()) {
buttonList.add(button);
continue;
}
List<JSONObject> subButtonList = subButton.toList(JSONObject.class);
buttonList.addAll(subButtonList);
}
// 遍历规则
for (JSONObject button : buttonList) {
// 可以在这里进行自定义逻辑开发
if (!eventKey.equalsIgnoreCase(button.getStr("key"))) {
continue;
}
WxMpXmlOutMessage wxMpXmlOutMessage = null;
String repType = button.getStr("repType");
String repName = button.getStr("repName");
String repContent = button.getStr("repContent");
String repMediaId = button.getStr("repMediaId");
String repDesc = button.getStr("repDesc");
if (WxConsts.KefuMsgType.TEXT.equals(repType)) {
wxMpXmlOutMessage = new TextBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.content(repContent).build();
}
if (WxConsts.KefuMsgType.IMAGE.equals(repType)) {
wxMpXmlOutMessage = new ImageBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.mediaId(repMediaId).build();
}
if (WxConsts.KefuMsgType.VOICE.equals(repType)) {
wxMpXmlOutMessage = new VoiceBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.mediaId(repMediaId).build();
}
if (WxConsts.KefuMsgType.VIDEO.equals(repType)) {
wxMpXmlOutMessage = new VideoBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.mediaId(repMediaId).title(repName).description(repDesc).build();
}
if (WxConsts.KefuMsgType.MUSIC.equals(repType)) {
String repThumbMediaId = button.getStr("repThumbMediaId");
String repUrl = button.getStr("repUrl");
String repHqUrl = button.getStr("repHqUrl");
wxMpXmlOutMessage = new MusicBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.thumbMediaId(repThumbMediaId).title(repName).description(repDesc).musicUrl(repUrl)
.hqMusicUrl(repHqUrl).build();
}
if (WxConsts.KefuMsgType.NEWS.equals(repType)) {
String content = button.getStr("content");
List<WxMpXmlOutNewsMessage.Item> list = new ArrayList<>();
List<JSONObject> articles = JSONUtil.parseObj(content).getJSONArray("articles")
.toList(JSONObject.class);
WxMpXmlOutNewsMessage.Item t;
for (JSONObject jsonObject : articles) {
t = new WxMpXmlOutNewsMessage.Item();
t.setTitle(jsonObject.getStr("title"));
t.setDescription(jsonObject.getStr("digest"));
t.setPicUrl(jsonObject.getStr("thumbUrl"));
t.setUrl(jsonObject.getStr("url"));
list.add(t);
}
wxMpXmlOutMessage = new NewsBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.articles(list).build();
}
return wxMpXmlOutMessage;
}
return null;
}
}

View File

@@ -0,0 +1,194 @@
package com.pig4cloud.pigx.mp.handler;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.mp.config.WxMpContextHolder;
import com.pig4cloud.pigx.mp.constant.MsgTypeEnum;
import com.pig4cloud.pigx.mp.constant.ReplyMateEnum;
import com.pig4cloud.pigx.mp.constant.ReplyTypeEnum;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
import com.pig4cloud.pigx.mp.entity.WxMsg;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxMsgMapper;
import com.pig4cloud.pigx.mp.service.WxAutoReplyService;
import lombok.AllArgsConstructor;
import me.chanjar.weixin.common.api.WxConsts;
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.message.WxMpXmlOutNewsMessage;
import me.chanjar.weixin.mp.builder.outxml.*;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static me.chanjar.weixin.common.api.WxConsts.XmlMsgType;
/**
* @author JL
*/
@Component
@AllArgsConstructor
public class MsgHandler extends AbstractHandler {
private final WxAutoReplyService wxAutoReplyService;
private final WxAccountFansMapper wxAccountFansMapper;
private final WxAccountMapper wxAccountMapper;
private final WxMsgMapper wxMsgMapper;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
// 组装回复消息
if (!wxMessage.getMsgType().equals(XmlMsgType.EVENT)) {
WxMpXmlOutMessage rs;
WxAccountFans fans = wxAccountFansMapper.selectOne(
Wrappers.<WxAccountFans>lambdaQuery().eq(WxAccountFans::getOpenid, wxMessage.getFromUser()));
// 查询公众号 基本信息
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAccount, wxMessage.getToUser()));
// 1、先处理是否有文本关键字回复
if (WxConsts.KefuMsgType.TEXT.equals(wxMessage.getMsgType())) {
// 先全匹配
List<WxAutoReply> listWxAutoReply = wxAutoReplyService.list(
Wrappers.<WxAutoReply>query().lambda().eq(WxAutoReply::getType, ReplyTypeEnum.KEYWORD.getType())
.eq(WxAutoReply::getRepMate, ReplyMateEnum.ALL.getType())
.eq(WxAutoReply::getAppId, WxMpContextHolder.getAppId())
.eq(WxAutoReply::getReqKey, wxMessage.getContent()));
if (listWxAutoReply != null && listWxAutoReply.size() > 0) {
rs = getWxMpXmlOutMessage(wxMessage, listWxAutoReply, fans, wxMsgMapper, wxAccount);
if (rs != null) {
return rs;
}
}
// 再半匹配
listWxAutoReply = wxAutoReplyService.list(
Wrappers.<WxAutoReply>query().lambda().eq(WxAutoReply::getType, ReplyTypeEnum.KEYWORD.getType())
.eq(WxAutoReply::getAppId, WxMpContextHolder.getAppId())
.eq(WxAutoReply::getRepMate, ReplyMateEnum.LIKE.getType())
.like(WxAutoReply::getReqKey, wxMessage.getContent()));
if (listWxAutoReply != null && listWxAutoReply.size() > 0) {
rs = getWxMpXmlOutMessage(wxMessage, listWxAutoReply, fans, wxMsgMapper, wxAccount);
if (rs != null) {
return rs;
}
}
}
// 2、再处理消息回复
List<WxAutoReply> listWxAutoReply = wxAutoReplyService
.list(Wrappers.<WxAutoReply>query().lambda().eq(WxAutoReply::getAppId, WxMpContextHolder.getAppId())
.eq(WxAutoReply::getType, ReplyTypeEnum.MSG.getType())
.eq(WxAutoReply::getReqType, wxMessage.getMsgType()));
rs = getWxMpXmlOutMessage(wxMessage, listWxAutoReply, fans, wxMsgMapper, wxAccount);
return rs;
}
return null;
}
/**
* 组装回复消息,并记录消息
* @param wxMessage
* @param listWxAutoReply
* @param wxMsgMapper
* @param wxAccount
* @return
*/
public static WxMpXmlOutMessage getWxMpXmlOutMessage(WxMpXmlMessage wxMessage, List<WxAutoReply> listWxAutoReply,
WxAccountFans fans, WxMsgMapper wxMsgMapper, WxAccount wxAccount) {
WxMpXmlOutMessage wxMpXmlOutMessage = null;
// 记录接收消息
if (listWxAutoReply != null && listWxAutoReply.size() > 0) {
WxAutoReply wxAutoReply = listWxAutoReply.get(0);
// 记录回复消息
WxMsg wxMsg = new WxMsg();
wxMsg.setWxUserId(fans.getId());
wxMsg.setNickName(fans.getNickname());
wxMsg.setHeadimgUrl(fans.getHeadimgUrl());
wxMsg.setType(MsgTypeEnum.MP2USER.getType());
wxMsg.setRepType(wxAutoReply.getRepType());
wxMsg.setOpenId(fans.getOpenid());
wxMsg.setAppName(wxAccount.getName());
wxMsg.setAppLogo(wxAccount.getQrUrl());
wxMsg.setAppId(wxAccount.getAppid());
// 文本
if (WxConsts.KefuMsgType.TEXT.equals(wxAutoReply.getRepType())) {
wxMsg.setRepContent(wxAutoReply.getRepContent());
wxMpXmlOutMessage = new TextBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.content(wxAutoReply.getRepContent()).build();
}
// 图片
if (WxConsts.KefuMsgType.IMAGE.equals(wxAutoReply.getRepType())) {
wxMsg.setRepName(wxAutoReply.getRepName());
wxMsg.setRepUrl(wxAutoReply.getRepUrl());
wxMsg.setRepMediaId(wxAutoReply.getRepMediaId());
wxMpXmlOutMessage = new ImageBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.mediaId(wxAutoReply.getRepMediaId()).build();
}
if (WxConsts.KefuMsgType.VOICE.equals(wxAutoReply.getRepType())) {
wxMsg.setRepName(wxAutoReply.getRepName());
wxMsg.setRepUrl(wxAutoReply.getRepUrl());
wxMsg.setRepMediaId(wxAutoReply.getRepMediaId());
wxMpXmlOutMessage = new VoiceBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.mediaId(wxAutoReply.getRepMediaId()).build();
}
if (WxConsts.KefuMsgType.VIDEO.equals(wxAutoReply.getRepType())) {
wxMsg.setRepName(wxAutoReply.getRepName());
wxMsg.setRepDesc(wxAutoReply.getRepDesc());
wxMsg.setRepUrl(wxAutoReply.getRepUrl());
wxMsg.setRepMediaId(wxAutoReply.getRepMediaId());
wxMpXmlOutMessage = new VideoBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.mediaId(wxAutoReply.getRepMediaId()).title(wxAutoReply.getRepName())
.description(wxAutoReply.getRepDesc()).build();
}
if (WxConsts.KefuMsgType.MUSIC.equals(wxAutoReply.getRepType())) {
wxMsg.setRepName(wxAutoReply.getRepName());
wxMsg.setRepDesc(wxAutoReply.getRepDesc());
wxMsg.setRepUrl(wxAutoReply.getRepUrl());
wxMsg.setRepHqUrl(wxAutoReply.getRepHqUrl());
wxMsg.setRepThumbMediaId(wxAutoReply.getRepThumbMediaId());
wxMsg.setRepThumbUrl(wxAutoReply.getRepThumbUrl());
wxMpXmlOutMessage = new MusicBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.thumbMediaId(wxAutoReply.getRepThumbMediaId()).title(wxAutoReply.getRepName())
.description(wxAutoReply.getRepDesc()).musicUrl(wxAutoReply.getRepUrl())
.hqMusicUrl(wxAutoReply.getRepHqUrl()).build();
}
if (WxConsts.KefuMsgType.NEWS.equals(wxAutoReply.getRepType())) {
List<WxMpXmlOutNewsMessage.Item> list = new ArrayList<>();
JSONArray jsonArray = JSONUtil.parseArray(wxAutoReply.getContent());
for (Object obj : jsonArray) {
JSONObject jsonObject = (JSONObject) obj;
WxMpXmlOutNewsMessage.Item t = new WxMpXmlOutNewsMessage.Item();
t.setTitle(jsonObject.getStr("title"));
t.setDescription(jsonObject.getStr("digest"));
t.setPicUrl(jsonObject.getStr("thumbUrl"));
t.setUrl(jsonObject.getStr("url"));
list.add(t);
}
wxMsg.setRepName(wxAutoReply.getRepName());
wxMsg.setRepDesc(wxAutoReply.getRepDesc());
wxMsg.setRepUrl(wxAutoReply.getRepUrl());
wxMsg.setRepMediaId(wxAutoReply.getRepMediaId());
wxMsg.setContent(wxAutoReply.getContent());
wxMpXmlOutMessage = new NewsBuilder().fromUser(wxMessage.getToUser()).toUser(wxMessage.getFromUser())
.articles(list).build();
}
wxMsgMapper.insert(wxMsg);
}
return wxMpXmlOutMessage;
}
}

View File

@@ -0,0 +1,23 @@
package com.pig4cloud.pigx.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 Binary Wang(https://github.com/binarywang)
*/
@Component
public class NullHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
return null;
}
}

View File

@@ -0,0 +1,25 @@
package com.pig4cloud.pigx.mp.handler;
import me.chanjar.weixin.common.error.WxErrorException;
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 Binary Wang(https://github.com/binarywang)
*/
@Component
public class ScanHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map, WxMpService wxMpService,
WxSessionManager wxSessionManager) throws WxErrorException {
// 扫码事件处理
return null;
}
}

View File

@@ -0,0 +1,26 @@
package com.pig4cloud.pigx.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 Binary Wang(https://github.com/binarywang)
*/
@Component
public class StoreCheckNotifyHandler extends AbstractHandler {
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
// TODO 处理门店审核事件
return null;
}
}

View File

@@ -0,0 +1,111 @@
package com.pig4cloud.pigx.mp.handler;
import cn.binarywang.tools.generator.ChineseNameGenerator;
import cn.hutool.core.util.BooleanUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.mp.builder.TextBuilder;
import com.pig4cloud.pigx.mp.config.WxMpContextHolder;
import com.pig4cloud.pigx.mp.constant.ReplyTypeEnum;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxMsgMapper;
import com.pig4cloud.pigx.mp.service.WxAutoReplyService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
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.springframework.stereotype.Component;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.List;
import java.util.Map;
/**
* @author Binary Wang(https://github.com/binarywang)
*/
@Slf4j
@Component
@AllArgsConstructor
public class SubscribeHandler extends AbstractHandler {
private final WxAutoReplyService wxAutoReplyService;
private final WxAccountFansMapper wxAccountFansMapper;
private final WxAccountMapper wxAccountMapper;
private final WxMsgMapper msgMapper;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService weixinService,
WxSessionManager sessionManager) throws WxErrorException {
log.info("新关注用户 OPENID: " + wxMessage.getFromUser());
// 获取微信用户基本信息
try {
WxMpUser wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser(), null);
WxAccountFans wxAccountFans = wxAccountFansMapper.selectOne(
Wrappers.<WxAccountFans>lambdaUpdate().eq(WxAccountFans::getOpenid, wxMessage.getFromUser()));
// 删除历史关注保存的信息
if (wxAccountFans != null) {
wxAccountFansMapper.deleteById(wxAccountFans);
}
wxAccountFans = new WxAccountFans();
wxAccountFans.setOpenid(wxMpUser.getOpenId());
wxAccountFans.setSubscribeStatus(String.valueOf(BooleanUtil.toInt(wxMpUser.getSubscribe())));
wxAccountFans.setSubscribeTime(LocalDateTime
.ofInstant(Instant.ofEpochMilli(wxMpUser.getSubscribeTime() * 1000L), ZoneId.systemDefault()));
// 随机生成一个昵称,方便平台内部使用
String generatedName = ChineseNameGenerator.getInstance().generate();
wxAccountFans.setNickname(generatedName);
wxAccountFans.setLanguage(wxMpUser.getLanguage());
wxAccountFans.setHeadimgUrl(wxMpUser.getHeadImgUrl());
wxAccountFans.setRemark(wxMpUser.getRemark());
wxAccountFans.setTagIds(wxAccountFans.getTagIds());
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAppid, WxMpContextHolder.getAppId()));
wxAccountFans.setWxAccountId(wxAccount.getId());
wxAccountFans.setWxAccountAppid(wxAccount.getAppid());
wxAccountFans.setWxAccountName(wxAccount.getName());
wxAccountFansMapper.insert(wxAccountFans);
return this.handleSpecial(wxMessage, wxAccountFans);
}
catch (WxErrorException e) {
log.error("该公众号没有获取用户信息失败!", e);
}
return new TextBuilder().build("感谢关注", wxMessage, weixinService);
}
/**
* 处理特殊请求,比如如果是扫码进来的,可以做相应处理
*/
private WxMpXmlOutMessage handleSpecial(WxMpXmlMessage wxMessage, WxAccountFans fans) {
// 发送关注消息
List<WxAutoReply> listWxAutoReply = wxAutoReplyService
.list(Wrappers.<WxAutoReply>query().lambda().eq(WxAutoReply::getType, ReplyTypeEnum.ATTENTION.getType())
.eq(WxAutoReply::getAppId, WxMpContextHolder.getAppId()));
// 查询公众号 基本信息
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAccount, wxMessage.getToUser()));
WxMpXmlOutMessage wxMpXmlOutMessage = MsgHandler.getWxMpXmlOutMessage(wxMessage, listWxAutoReply, fans,
msgMapper, wxAccount);
return wxMpXmlOutMessage;
}
}

View File

@@ -0,0 +1,38 @@
package com.pig4cloud.pigx.mp.handler;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pig4cloud.pigx.mp.constant.SubStatusEnum;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import lombok.AllArgsConstructor;
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 Binary Wang(https://github.com/binarywang)
*/
@Slf4j
@Component
@AllArgsConstructor
public class UnsubscribeHandler extends AbstractHandler {
private final WxAccountFansMapper wxAccountFansMapper;
@Override
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map<String, Object> context, WxMpService wxMpService,
WxSessionManager sessionManager) {
String openId = wxMessage.getFromUser();
log.info("取消关注用户 OPENID: " + openId);
WxAccountFans fans = new WxAccountFans();
fans.setSubscribeStatus(SubStatusEnum.UNSUB.getType());
wxAccountFansMapper.update(fans, Wrappers.<WxAccountFans>lambdaUpdate().eq(WxAccountFans::getOpenid, openId));
return null;
}
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import org.apache.ibatis.annotations.Mapper;
/**
* 微信公众号粉丝
*
* @author lengleng
* @date 2019-03-26 22:08:08
*/
@Mapper
public interface WxAccountFansMapper extends PigxBaseMapper<WxAccountFans> {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import org.apache.ibatis.annotations.Mapper;
/**
* 公众号账户
*
* @author lengleng
* @date 2019-03-26 22:07:53
*/
@Mapper
public interface WxAccountMapper extends PigxBaseMapper<WxAccount> {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxAccountTag;
import org.apache.ibatis.annotations.Mapper;
/**
* 公众号账户
*
* @author lengleng
* @date 2019-03-26 22:07:53
*/
@Mapper
public interface WxAccountTagMapper extends PigxBaseMapper<WxAccountTag> {
}

View File

@@ -0,0 +1,16 @@
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
import org.apache.ibatis.annotations.Mapper;
/**
* 消息自动回复
*
* @author JL
* @date 2019-04-18 15:40:39
*/
@Mapper
public interface WxAutoReplyMapper extends PigxBaseMapper<WxAutoReply> {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxFansMsgRes;
import org.apache.ibatis.annotations.Mapper;
/**
* lengleng
*
* @author lengleng
* @date 2019-03-27 20:45:48
*/
@Mapper
public interface WxFansMsgResMapper extends PigxBaseMapper<WxFansMsgRes> {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxMpMenu;
import org.apache.ibatis.annotations.Mapper;
/**
* lengleng
*
* @author lengleng
* @date 2019-03-27 20:45:18
*/
@Mapper
public interface WxMenuMapper extends PigxBaseMapper<WxMpMenu> {
}

View File

@@ -0,0 +1,16 @@
package com.pig4cloud.pigx.mp.mapper;
import com.pig4cloud.pigx.common.data.datascope.PigxBaseMapper;
import com.pig4cloud.pigx.mp.entity.WxMsg;
import org.apache.ibatis.annotations.Mapper;
/**
* 微信消息
*
* @author JL
* @date 2019-05-28 16:12:10
*/
@Mapper
public interface WxMsgMapper extends PigxBaseMapper<WxMsg> {
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
/**
* 微信公众号粉丝
*
* @author lengleng
* @date 2019-03-26 22:08:08
*/
public interface WxAccountFansService extends IService<WxAccountFans> {
/**
* 同步指定公众号粉丝
* @param appId
* @return
*/
Boolean syncAccountFans(String appId);
/**
* 分页查询粉丝
* @param page 粉丝
* @param wxAccountFans 查询条件
* @return
*/
IPage getFansWithTagPage(Page page, WxAccountFans wxAccountFans);
/**
* 更新粉丝信息
* @param wxAccountFans 信息
* @return
*/
Boolean updateFans(WxAccountFans wxAccountFans);
Boolean unblack(Long[] ids, String appId);
Boolean black(Long[] ids, String appId);
}

View File

@@ -0,0 +1,46 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.entity.WxAccount;
/**
* 公众号账户
*
* @author lengleng
* @date 2019-03-26 22:07:53
*/
public interface WxAccountService extends IService<WxAccount> {
/**
* 生成公众号二维码
* @param appId
* @return
*/
R generateQr(String appId);
/**
* 获取公众号统计数据
* @param appId
* @param interval
* @return
*/
R statistics(String appId, String interval);
}

View File

@@ -0,0 +1,42 @@
package com.pig4cloud.pigx.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.mp.entity.WxAccountTag;
/**
* 微信账户标签服务
*
* @author lishangbu
* @date 2021/12/31
*/
public interface WxAccountTagService extends IService<WxAccountTag> {
/**
* 保存账户标签
* @param accountTag 账户标签
* @return 保存成功后的账户标签
*/
WxAccountTag saveAccountTag(WxAccountTag accountTag);
/**
* 更新账户标签
* @param accountTag 待更新的账户标签
* @return 修改成功后的账户标签
*/
WxAccountTag updateAccountTag(WxAccountTag accountTag);
/**
* 删除账户标签
* @param wxAccountTag 待删除的账户标签
* @return 删除成功返回true,删除失败返回false
*/
Boolean removeAccountTagById(WxAccountTag wxAccountTag);
/**
* 同步账户标签
* @param appId appId
* @return Boolean
*/
Boolean syncAccountTags(String appId);
}

View File

@@ -0,0 +1,14 @@
package com.pig4cloud.pigx.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
/**
* 消息自动回复
*
* @author JL
* @date 2019-04-18 15:40:39
*/
public interface WxAutoReplyService extends IService<WxAutoReply> {
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.service;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.entity.WxMpMenu;
/**
* 微信菜单业务
*
* @author lengleng
* @date 2019-03-27 20:45:18
*/
public interface WxMenuService extends IService<WxMpMenu> {
/**
* 新增微信公众号按钮
* @param wxMenus json
* @param appId 公众号信息
* @return
*/
Boolean save(JSONObject wxMenus, String appId);
/**
* 发布到微信
* @param appId 公众号信息
* @return
*/
R push(String appId);
/**
* 通过appid 查询菜单信息
* @param appId
* @return
*/
R getByAppId(String appId);
/**
* 通过appid 删除菜单
* @param appId
* @return
*/
R delete(String appId);
}

View File

@@ -0,0 +1,22 @@
package com.pig4cloud.pigx.mp.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.entity.WxMsg;
/**
* 微信消息
*
* @author JL
* @date 2019-05-28 16:12:10
*/
public interface WxMsgService extends IService<WxMsg> {
/**
* 保存信息并向用户推送
* @param wxMsg
* @return
*/
R saveAndPushMsg(WxMsg wxMsg);
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.service.impl;
import cn.binarywang.tools.generator.ChineseNameGenerator;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.data.tenant.TenantBroker;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxAccountTag;
import com.pig4cloud.pigx.mp.entity.vo.WxAccountFansVo;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountTagMapper;
import com.pig4cloud.pigx.mp.service.WxAccountFansService;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.api.WxMpUserBlacklistService;
import me.chanjar.weixin.mp.api.WxMpUserService;
import me.chanjar.weixin.mp.api.WxMpUserTagService;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import me.chanjar.weixin.mp.bean.result.WxMpUserList;
import org.springframework.beans.BeanUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author lengleng
* @date 2019-03-26 22:08:08
* <p>
* 微信公众号粉丝
*/
@Slf4j
@Service
@AllArgsConstructor
public class WxAccountFansServiceImpl extends ServiceImpl<WxAccountFansMapper, WxAccountFans>
implements WxAccountFansService {
private static final int SIZE = 100;
private final WxAccountTagMapper wxAccountTagMapper;
private final WxAccountMapper wxAccountMapper;
/**
* 分页查询粉丝
* @param page 粉丝
* @param wxAccountFans 查询条件
* @return
*/
@SneakyThrows
@Override
public IPage getFansWithTagPage(Page page, WxAccountFans wxAccountFans) {
// 查询当前公众号的标签
List<WxAccountTag> wxAccountTags = wxAccountTagMapper.selectList(Wrappers.emptyWrapper());
// 翻页查询粉丝
LambdaQueryWrapper<WxAccountFans> wrapper = Wrappers.<WxAccountFans>lambdaQuery()
.eq(StrUtil.isNotBlank(wxAccountFans.getWxAccountAppid()), WxAccountFans::getWxAccountAppid,
wxAccountFans.getWxAccountAppid())
.like(StrUtil.isNotBlank(wxAccountFans.getNickname()), WxAccountFans::getNickname,
wxAccountFans.getNickname());
IPage<WxAccountFans> fansPage = baseMapper.selectPage(page, wrapper);
List<WxAccountFansVo> voList = fansPage.getRecords().stream().map(fans -> {//
WxAccountFansVo vo = new WxAccountFansVo();
BeanUtils.copyProperties(fans, vo);
// 赋值 TAG_NAME
if (ArrayUtil.isNotEmpty(fans.getTagIds())) {
List<WxAccountTag> tagList = wxAccountTags.stream()
.filter(tag -> tag.getWxAccountAppid().endsWith(fans.getWxAccountAppid()) //
&& ArrayUtil.contains(fans.getTagIds(), tag.getTagId()))
.collect(Collectors.toList());
vo.setTagList(tagList);
}
return vo;
}).collect(Collectors.toList());
page.setRecords(voList);
page.setTotal(fansPage.getTotal());
return page;
}
/**
* 更新粉丝信息
* @param wxAccountFans 信息
* @return
*/
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public Boolean updateFans(WxAccountFans wxAccountFans) {
baseMapper.updateById(wxAccountFans);
// 获取操作微信接口类
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(wxAccountFans.getWxAccountAppid());
if (StrUtil.isNotBlank(wxAccountFans.getRemark())) {
WxMpUserService wxMpUserService = wxMpService.getUserService();
wxMpUserService.userUpdateRemark(wxAccountFans.getOpenid(), wxAccountFans.getRemark());
}
// 更新用户标签
WxMpUserTagService userTagService = wxMpService.getUserTagService();
if (ArrayUtil.isNotEmpty(wxAccountFans.getTagIds())) {
// 移除原有标签
List<Long> oldTag = userTagService.userTagList(wxAccountFans.getOpenid());
for (Long tagId : oldTag) {
userTagService.batchUntagging(tagId, new String[] { wxAccountFans.getOpenid() });
}
// 添加新标签
for (Long tagId : wxAccountFans.getTagIds()) {
userTagService.batchTagging(tagId, new String[] { wxAccountFans.getOpenid() });
}
}
return Boolean.TRUE;
}
@SneakyThrows
@Override
public Boolean unblack(Long[] ids, String appId) {
List<WxAccountFans> wxAccountFans = this.listByIds(CollUtil.toList(ids));
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpUserBlacklistService blackListService = wxMpService.getBlackListService();
List<String> collect = wxAccountFans.stream().map(WxAccountFans::getOpenid).collect(Collectors.toList());
blackListService.pullFromBlacklist(collect);
// 更新数据
List<WxAccountFans> fansList = wxAccountFans.stream().peek(item -> {
item.setIsBlack(CommonConstants.SUCCESS);
}).collect(Collectors.toList());
this.updateBatchById(fansList);
return Boolean.TRUE;
}
@SneakyThrows
@Override
public Boolean black(Long[] ids, String appId) {
List<WxAccountFans> wxAccountFans = this.listByIds(CollUtil.toList(ids));
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpUserBlacklistService blackListService = wxMpService.getBlackListService();
List<String> collect = wxAccountFans.stream().map(WxAccountFans::getOpenid).collect(Collectors.toList());
blackListService.pushToBlacklist(collect);
// 更新数据
List<WxAccountFans> fansList = wxAccountFans.stream().peek(item -> {
item.setIsBlack(CommonConstants.FAIL);
}).collect(Collectors.toList());
this.updateBatchById(fansList);
return Boolean.TRUE;
}
/**
* 获取公众号粉丝,生产建议异步
* @param appId
* @return
*/
@Async
@Override
public Boolean syncAccountFans(String appId) {
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>query().lambda().eq(WxAccount::getAppid, appId));
// 获取操作微信接口类
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpUserService wxMpUserService = wxMpService.getUserService();
// 根据公众号查询已同步用户openid 查询最新的一条 (注意不能按订阅时间排序)
String finalNextOpenId = queryNextOpenId(appId);
TenantBroker.runAs(() -> WxMpInitConfigRunner.getTenants().get(appId),
(id) -> fetchUser(finalNextOpenId, wxAccount, wxMpUserService));
log.info("公众号 {} 粉丝同步完成", wxAccount.getName());
return Boolean.TRUE;
}
/**
* 获取微信用户
* @param nextOpenid 下一组开始的openid
* @param wxAccount 公众号信息
* @param wxMpUserService mp操作类
*/
private void fetchUser(String nextOpenid, WxAccount wxAccount, WxMpUserService wxMpUserService) {
try {
WxMpUserList wxMpUserList = wxMpUserService.userList(nextOpenid);
// openId 分组 每组 100个 openid
List<List<String>> openIdsList = CollUtil.split(wxMpUserList.getOpenids(), SIZE).stream()
.filter(CollUtil::isNotEmpty).collect(Collectors.toList());
// 处理每个分组. 调用查询用户信息
for (List<String> openIdList : openIdsList) {
log.info("开始批量获取用户信息 {}", openIdList);
List<WxAccountFans> wxAccountFansList = new ArrayList<>();
wxMpUserService.userInfoList(openIdList).forEach(wxMpUser -> {
WxAccountFans wxAccountFans = buildDbUser(wxAccount, wxMpUser);
wxAccountFansList.add(wxAccountFans);
});
this.saveOrUpdateBatch(wxAccountFansList);
log.info("批量插入用户信息完成 {}", openIdList);
}
// 如果nextOpenId 不为空,则继续获取
if (StrUtil.isNotBlank(wxMpUserList.getNextOpenid())) {
fetchUser(wxMpUserList.getNextOpenid(), wxAccount, wxMpUserService);
}
}
catch (Exception e) {
log.warn("同步微信公众号 {} 粉丝异常 {}", wxAccount.getName(), e.getMessage());
// 失败重试逻辑
String finalNextOpenId = queryNextOpenId(wxAccount.getAppid());
fetchUser(finalNextOpenId, wxAccount, wxMpUserService);
}
}
/**
* 构建数据库存取对象
* @param wxAccount 公众号信息
* @param wxMpUser 用户微信信息
*/
private WxAccountFans buildDbUser(WxAccount wxAccount, WxMpUser wxMpUser) {
WxAccountFans wxAccountFans = new WxAccountFans();
wxAccountFans.setOpenid(wxMpUser.getOpenId());
wxAccountFans.setSubscribeStatus(String.valueOf(BooleanUtil.toInt(wxMpUser.getSubscribe())));
// 2020-11-25 部分用户订阅时间为空,跳过此字段处理
if (ObjectUtil.isNotEmpty(wxMpUser.getSubscribeTime())) {
wxAccountFans.setSubscribeTime(LocalDateTime
.ofInstant(Instant.ofEpochMilli(wxMpUser.getSubscribeTime() * 1000L), ZoneId.systemDefault()));
}
// 随机生成一个昵称,方便平台内部使用
String generatedName = ChineseNameGenerator.getInstance().generate();
wxAccountFans.setNickname(generatedName);
wxAccountFans.setLanguage(wxMpUser.getLanguage());
wxAccountFans.setHeadimgUrl(wxMpUser.getHeadImgUrl());
wxAccountFans.setRemark(wxMpUser.getRemark());
wxAccountFans.setTagIds(wxAccountFans.getTagIds());
wxAccountFans.setWxAccountId(wxAccount.getId());
wxAccountFans.setWxAccountAppid(wxAccount.getAppid());
wxAccountFans.setWxAccountName(wxAccount.getName());
wxAccountFans.setIsBlack(CommonConstants.SUCCESS);
return wxAccountFans;
}
/**
* 查询当前公众号最后插入的openId
* @param appId 公众号标识
* @return openid / null
*/
private String queryNextOpenId(String appId) {
Page<WxAccountFans> queryPage = new Page<>(0, 1);
Page<WxAccountFans> fansPage = baseMapper.selectPage(queryPage, Wrappers.<WxAccountFans>query().lambda()
.eq(WxAccountFans::getWxAccountAppid, appId).orderByDesc(WxAccountFans::getCreateTime));
String nextOpenId = null;
if (fansPage.getTotal() > 0) {
nextOpenId = fansPage.getRecords().get(0).getOpenid();
}
return nextOpenId;
}
}

View File

@@ -0,0 +1,130 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.service.WxAccountService;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpDataCubeService;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.datacube.WxDataCubeArticleResult;
import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult;
import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult;
import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 公众号账户
*
* @author lengleng
* @date 2019-03-26 22:07:53
*/
@Slf4j
@Service
public class WxAccountServiceImpl extends ServiceImpl<WxAccountMapper, WxAccount> implements WxAccountService {
/**
* 生成公众号二维码
* @param appId
* @return
*/
@Override
public R generateQr(String appId) {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
try {
WxMpQrCodeTicket ticket = wxMpService.getQrcodeService().qrCodeCreateLastTicket(1);
String url = wxMpService.getQrcodeService().qrCodePictureUrl(ticket.getTicket());
WxAccount wxAccount = baseMapper
.selectOne(Wrappers.<WxAccount>query().lambda().eq(WxAccount::getAppid, appId));
wxAccount.setQrUrl(url);
baseMapper.updateById(wxAccount);
}
catch (WxErrorException e) {
log.error(" 获取公众号二维码失败", e);
return R.failed("更新公众号二维码失败");
}
return R.ok();
}
/**
* 获取公众号统计数据
* @param appId 公众号信息
* @param interval 时间间隔
* @return
*/
@Override
public R statistics(String appId, String interval) {
String[] split = interval.split(StrUtil.DASHED);
Date start = new Date(Long.parseLong(split[0]));
Date end = new Date(Long.parseLong(split[1]));
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpDataCubeService cubeService = wxMpService.getDataCubeService();
List<List<Object>> result = new ArrayList<>();
try {
// 获取累计用户数据
List<Object> cumulateList = cubeService.getUserCumulate(start, end).stream()
.map(WxDataCubeUserCumulate::getCumulateUser).collect(Collectors.toList());
result.add(cumulateList);
// 获取用户分享数据
List<Object> shareList = cubeService.getUserShare(start, end).stream()
.map(WxDataCubeArticleResult::getShareCount).collect(Collectors.toList());
result.add(shareList);
// 获取消息发送概况数据
List<Object> upstreamList = cubeService.getUpstreamMsg(start, end).stream()
.map(WxDataCubeMsgResult::getMsgCount).collect(Collectors.toList());
result.add(upstreamList);
// 获取接口调用概况数据
List<WxDataCubeInterfaceResult> interfaceSummaryList = cubeService.getInterfaceSummary(start, end);
List<Object> interfaceList = interfaceSummaryList.stream().map(WxDataCubeInterfaceResult::getCallbackCount)
.collect(Collectors.toList());
result.add(interfaceList);
// 接口日期保存
List<Object> dateList = interfaceSummaryList.stream().map(WxDataCubeInterfaceResult::getRefDate)
.collect(Collectors.toList());
result.add(dateList);
}
catch (WxErrorException e) {
log.error(" 获取公众号统计数据报错", e);
return R.failed("获取公众号数据失败:" + e.getError().getErrorMsg());
}
return R.ok(result);
}
}

View File

@@ -0,0 +1,123 @@
package com.pig4cloud.pigx.mp.service.impl;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountTag;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountTagMapper;
import com.pig4cloud.pigx.mp.service.WxAccountTagService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.tag.WxUserTag;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import java.util.List;
/**
* 微信账户标签服务实现类
*
* @author lishangbu
* @date 2021/12/31
*/
@Service
@RequiredArgsConstructor
public class WxAccountTagServiceImpl extends ServiceImpl<WxAccountTagMapper, WxAccountTag>
implements WxAccountTagService {
private final WxAccountMapper accountMapper;
/**
* 保存账户标签
* @param accountTag 账户标签
* @return 保存成功true保存失败false
*/
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public WxAccountTag saveAccountTag(WxAccountTag accountTag) {
WxAccountTag wxAccountTag = baseMapper
.selectOne(Wrappers.<WxAccountTag>lambdaQuery().eq(WxAccountTag::getTag, accountTag.getTag()));
Assert.isNull(wxAccountTag, StrUtil.format("名称为[{}]的标签已经存在", accountTag.getTag()));
// 调用微信公众号接口删除
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(accountTag.getWxAccountAppid());
WxUserTag wxUserTag = wxMpService.getUserTagService().tagCreate(accountTag.getTag());
accountTag.setTagId(wxUserTag.getId());
// 查询公众号详细信息
WxAccount wxAccount = accountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAppid, accountTag.getWxAccountAppid()));
accountTag.setWxAccountId(wxAccount.getId());
accountTag.setWxAccountAppid(wxAccount.getAppid());
accountTag.setWxAccountName(wxAccount.getName());
baseMapper.insert(accountTag);
return accountTag;
}
/**
* 更新账户标签
* @param accountTag 待更新的账户标签
* @return
*/
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public WxAccountTag updateAccountTag(WxAccountTag accountTag) {
baseMapper.updateById(accountTag);
// 调用微信公众号接口更新
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(accountTag.getWxAccountAppid());
wxMpService.getUserTagService().tagUpdate(accountTag.getTagId(), accountTag.getTag());
return accountTag;
}
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public Boolean removeAccountTagById(WxAccountTag tag) {
WxAccountTag wxAccountTag = baseMapper.selectById(tag.getId());
Assert.notNull(wxAccountTag, "要删除的标签不存在");
// 调用微信公众号接口删除
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(tag.getWxAccountAppid());
wxMpService.getUserTagService().tagDelete(wxAccountTag.getTagId());
return removeById(wxAccountTag.getId());
}
/**
* 同步账户标签
* @param appId appId
* @return Boolean
*/
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public Boolean syncAccountTags(String appId) {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
List<WxUserTag> wxUserTags = wxMpService.getUserTagService().tagGet();
// 删除旧数据
baseMapper.delete(Wrappers.<WxAccountTag>lambdaQuery().eq(WxAccountTag::getWxAccountAppid, appId));
WxAccount wxAccount = accountMapper.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAppid, appId));
for (WxUserTag wxUserTag : wxUserTags) {
WxAccountTag tag = new WxAccountTag();
tag.setTag(wxUserTag.getName());
tag.setTagId(wxUserTag.getId());
tag.setWxAccountId(wxAccount.getId());
tag.setWxAccountAppid(wxAccount.getAppid());
tag.setWxAccountName(wxAccount.getName());
baseMapper.insert(tag);
}
return Boolean.TRUE;
}
}

View File

@@ -0,0 +1,18 @@
package com.pig4cloud.pigx.mp.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.mp.entity.WxAutoReply;
import com.pig4cloud.pigx.mp.mapper.WxAutoReplyMapper;
import com.pig4cloud.pigx.mp.service.WxAutoReplyService;
import org.springframework.stereotype.Service;
/**
* 消息自动回复
*
* @author JL
* @date 2019-04-18 15:40:39
*/
@Service
public class WxAutoReplyServiceImpl extends ServiceImpl<WxAutoReplyMapper, WxAutoReply> implements WxAutoReplyService {
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright (c) 2018-2025, lengleng All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* Neither the name of the pig4cloud.com developer nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
* Author: lengleng (wangiegie@gmail.com)
*/
package com.pig4cloud.pigx.mp.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxMpMenu;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxMenuMapper;
import com.pig4cloud.pigx.mp.service.WxMenuService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpMenuService;
import me.chanjar.weixin.mp.api.WxMpService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 微信菜单业务
*
* @author lengleng
* @date 2019-03-27 20:45:18
*/
@Slf4j
@Service
@AllArgsConstructor
public class WxMenuServiceImpl extends ServiceImpl<WxMenuMapper, WxMpMenu> implements WxMenuService {
private static final String PUB_ED = "1";
private final WxAccountMapper wxAccountMapper;
/**
* 新增微信公众号按钮
* @param wxMenus json
* @param appId 公众号
* @return
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean save(JSONObject wxMenus, String appId) {
baseMapper.delete(Wrappers.<WxMpMenu>lambdaQuery().eq(WxMpMenu::getWxAccountAppid, appId));
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAppid, appId));
WxMpMenu wxMpMenu = new WxMpMenu();
wxMpMenu.setMenu(wxMenus.toStringPretty());
wxMpMenu.setWxAccountId(wxAccount.getId());
wxMpMenu.setWxAccountAppid(wxAccount.getAppid());
wxMpMenu.setWxAccountName(wxAccount.getName());
baseMapper.insert(wxMpMenu);
return Boolean.TRUE;
}
/**
* 发布到微信
* @param appId 公众号信息
* @return
*/
@Override
public R push(String appId) {
List<WxMpMenu> wxMpMenuList = baseMapper
.selectList(Wrappers.<WxMpMenu>lambdaQuery().eq(WxMpMenu::getWxAccountAppid, appId));
if (CollUtil.isEmpty(wxMpMenuList)) {
return R.failed("微信菜单配置未保存,不能发布");
}
WxMpMenu wxMpMenu = wxMpMenuList.get(0);
// 判断是否发布
if (PUB_ED.equals(wxMpMenu.getPubFlag())) {
return R.failed("微信菜单配置已发布,不要重复发布");
}
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMenuService menuService = wxMpService.getMenuService();
// 给数据库保存的加一层
try {
menuService.menuCreate(wxMpMenu.getMenu());
}
catch (WxErrorException e) {
log.error("发布微信菜单失败", e.getError().getErrorMsg());
return R.failed(e.getError().getErrorMsg());
}
// 更新菜单发布标志
wxMpMenu.setPubFlag(PUB_ED);
baseMapper.updateById(wxMpMenu);
return R.ok();
}
/**
* 通过appid 查询菜单信息
* @param appId
* @return
*/
@Override
public R getByAppId(String appId) {
List<WxMpMenu> wxMpMenuList = baseMapper
.selectList(Wrappers.<WxMpMenu>lambdaQuery().eq(WxMpMenu::getWxAccountAppid, appId));
if (CollUtil.isEmpty(wxMpMenuList)) {
return R.ok();
}
return R.ok(wxMpMenuList.get(0).getMenu());
}
/**
* 通过appid 删除菜单
* @param appId
* @return
*/
@Override
public R delete(String appId) {
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(appId);
WxMpMenuService menuService = wxMpService.getMenuService();
try {
menuService.menuDelete();
}
catch (WxErrorException e) {
log.error("微信菜单删除失败", e.getError().getErrorMsg());
return R.failed(e.getError().getErrorMsg());
}
baseMapper.delete(Wrappers.<WxMpMenu>lambdaQuery().eq(WxMpMenu::getWxAccountAppid, appId));
return R.ok();
}
}

View File

@@ -0,0 +1,151 @@
package com.pig4cloud.pigx.mp.service.impl;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.mp.config.WxMpInitConfigRunner;
import com.pig4cloud.pigx.mp.constant.MsgTypeEnum;
import com.pig4cloud.pigx.mp.entity.WxAccount;
import com.pig4cloud.pigx.mp.entity.WxAccountFans;
import com.pig4cloud.pigx.mp.entity.WxMsg;
import com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper;
import com.pig4cloud.pigx.mp.mapper.WxAccountMapper;
import com.pig4cloud.pigx.mp.mapper.WxMsgMapper;
import com.pig4cloud.pigx.mp.service.WxMsgService;
import lombok.AllArgsConstructor;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpKefuService;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* 微信消息
*
* @author JL
* @date 2019-05-28 16:12:10
*/
@Service
@AllArgsConstructor
public class WxMsgServiceImpl extends ServiceImpl<WxMsgMapper, WxMsg> implements WxMsgService {
private final WxAccountFansMapper accountFansMapper;
private final WxAccountMapper accountMapper;
private final WxMsgMapper msgMapper;
/**
* 保存信息并向用户推送
* @param wxMsg 推送消息内容
* @return
*/
@Override
public R saveAndPushMsg(WxMsg wxMsg) {
// 查询用户详细
WxAccountFans wxUser = accountFansMapper.selectById(wxMsg.getWxUserId());
WxAccount wxAccount = accountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAppid, wxMsg.getAppId()));
// 维护消息-用户
wxMsg.setNickName(wxAccount.getName());
wxMsg.setHeadimgUrl(wxUser.getHeadimgUrl());
wxMsg.setType(MsgTypeEnum.MP2USER.getType());
// 维护消息-公众号
wxMsg.setAppLogo(wxAccount.getQrUrl());
wxMsg.setAppName(wxAccount.getName());
WxMpKefuMessage wxMpKefuMessage = null;
if (WxConsts.KefuMsgType.TEXT.equals(wxMsg.getRepType())) {
wxMsg.setRepContent(wxMsg.getRepContent());
wxMpKefuMessage = WxMpKefuMessage.TEXT().build();
wxMpKefuMessage.setContent(wxMsg.getRepContent());
}
if (WxConsts.KefuMsgType.IMAGE.equals(wxMsg.getRepType())) {// 图片
wxMsg.setRepName(wxMsg.getRepName());
wxMsg.setRepUrl(wxMsg.getRepUrl());
wxMsg.setRepMediaId(wxMsg.getRepMediaId());
wxMpKefuMessage = WxMpKefuMessage.IMAGE().build();
wxMpKefuMessage.setMediaId(wxMsg.getRepMediaId());
}
if (WxConsts.KefuMsgType.VOICE.equals(wxMsg.getRepType())) {
wxMsg.setRepName(wxMsg.getRepName());
wxMsg.setRepUrl(wxMsg.getRepUrl());
wxMsg.setRepMediaId(wxMsg.getRepMediaId());
wxMpKefuMessage = WxMpKefuMessage.VOICE().build();
wxMpKefuMessage.setMediaId(wxMsg.getRepMediaId());
}
if (WxConsts.KefuMsgType.VIDEO.equals(wxMsg.getRepType())) {
wxMsg.setRepName(wxMsg.getRepName());
wxMsg.setRepDesc(wxMsg.getRepDesc());
wxMsg.setRepUrl(wxMsg.getRepUrl());
wxMsg.setRepMediaId(wxMsg.getRepMediaId());
wxMpKefuMessage = WxMpKefuMessage.VIDEO().build();
wxMpKefuMessage.setMediaId(wxMsg.getRepMediaId());
wxMpKefuMessage.setTitle(wxMsg.getRepName());
wxMpKefuMessage.setDescription(wxMsg.getRepDesc());
}
if (WxConsts.KefuMsgType.MUSIC.equals(wxMsg.getRepType())) {
wxMsg.setRepName(wxMsg.getRepName());
wxMsg.setRepDesc(wxMsg.getRepDesc());
wxMsg.setRepUrl(wxMsg.getRepUrl());
wxMsg.setRepHqUrl(wxMsg.getRepHqUrl());
wxMpKefuMessage = WxMpKefuMessage.MUSIC().build();
wxMpKefuMessage.setTitle(wxMsg.getRepName());
wxMpKefuMessage.setDescription(wxMsg.getRepDesc());
wxMpKefuMessage.setMusicUrl(wxMsg.getRepUrl());
wxMpKefuMessage.setHqMusicUrl(wxMsg.getRepHqUrl());
wxMpKefuMessage.setThumbMediaId(wxMsg.getRepThumbMediaId());
}
if (WxConsts.KefuMsgType.NEWS.equals(wxMsg.getRepType())) {
List<WxMpKefuMessage.WxArticle> list = new ArrayList<>();
JSONArray jsonArray = JSONUtil.parseArray(wxMsg.getContent());
for (Object obj : jsonArray) {
JSONObject jsonObject = (JSONObject) obj;
WxMpKefuMessage.WxArticle t = new WxMpKefuMessage.WxArticle();
t.setTitle(jsonObject.getStr("title"));
t.setDescription(jsonObject.getStr("digest"));
t.setPicUrl(jsonObject.getStr("thumbUrl"));
t.setUrl(jsonObject.getStr("url"));
list.add(t);
}
wxMsg.setRepName(wxMsg.getRepName());
wxMsg.setRepDesc(wxMsg.getRepDesc());
wxMsg.setRepUrl(wxMsg.getRepUrl());
wxMsg.setRepMediaId(wxMsg.getRepMediaId());
wxMsg.setContent(wxMsg.getContent());
wxMpKefuMessage = WxMpKefuMessage.NEWS().build();
wxMpKefuMessage.setArticles(list);
}
if (wxMpKefuMessage == null) {
return R.failed("非法消息类型");
}
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(wxMsg.getAppId());
WxMpKefuService wxMpKefuService = wxMpService.getKefuService();
wxMpKefuMessage.setToUser(wxUser.getOpenid());
try {
wxMpKefuService.sendKefuMessage(wxMpKefuMessage);
msgMapper.insert(wxMsg);
return R.ok(wxMsg);
}
catch (WxErrorException e) {
return R.failed(e.getError().getErrorMsg());
}
}
}

View File

@@ -0,0 +1,18 @@
server:
port: 6000
spring:
application:
name: @artifactId@
cloud:
nacos:
username: @nacos.username@
password: @nacos.password@
discovery:
server-addr: ${NACOS_HOST:pigx-register}:${NACOS_PORT:8848}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
config:
import:
- optional:nacos:application-@profiles.active@.yml
- optional:nacos:${spring.application.name}-@profiles.active@.yml

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (c) 2018-2025, lengleng All rights reserved.
~
~ Redistribution and use in source and binary forms, with or without
~ modification, are permitted provided that the following conditions are met:
~
~ Redistributions of source code must retain the above copyright notice,
~ this list of conditions and the following disclaimer.
~ Redistributions in binary form must reproduce the above copyright
~ notice, this list of conditions and the following disclaimer in the
~ documentation and/or other materials provided with the distribution.
~ Neither the name of the pig4cloud.com developer nor the names of its
~ contributors may be used to endorse or promote products derived from
~ this software without specific prior written permission.
~ Author: lengleng (wangiegie@gmail.com)
-->
<!--
小技巧: 在根pom里面设置统一存放路径统一管理方便维护
<properties>
<log-path>/Users/lengleng</log-path>
</properties>
1. 其他模块加日志输出直接copy本文件放在resources 目录即可
2. 注意修改 <property name="${log-path}/log.path" value=""/> 的value模块
-->
<configuration debug="false" scan="false">
<property name="log.path" value="logs/${project.artifactId}"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- Console log output -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Log file debug output -->
<appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM, aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
</encoder>
</appender>
<!-- Log file error output -->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${log.path}/%d{yyyy-MM}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>50MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%date [%thread] %-5level [%logger{50}] %file:%line - %msg%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
<logger name="org.activiti.engine.impl.db" level="DEBUG">
<appender-ref ref="debug"/>
</logger>
<!--nacos 心跳 INFO 屏蔽-->
<logger name="com.alibaba.nacos" level="OFF">
<appender-ref ref="error"/>
</logger>
<!-- Level: FATAL 0 ERROR 3 WARN 4 INFO 6 DEBUG 7 -->
<root level="INFO">
<appender-ref ref="console"/>
<appender-ref ref="debug"/>
<appender-ref ref="error"/>
</root>
</configuration>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pig4cloud.pigx.mp.mapper.WxAccountFansMapper">
<resultMap id="wxAccountFansMap" type="com.pig4cloud.pigx.mp.entity.WxAccountFans">
<id property="id" column="id"/>
<result property="openid" column="openid"/>
<result property="subscribeStatus" column="subscribe_status"/>
<result property="subscribeTime" column="subscribe_time"/>
<result property="nickname" column="nickname"/>
<result property="gender" column="gender"/>
<result property="language" column="language"/>
<result property="country" column="country"/>
<result property="province" column="province"/>
<result property="city" column="city"/>
<result property="headimgUrl" column="headimg_url"/>
<result property="remark" column="remark"/>
<result property="wxAccountId" column="wx_account_id"/>
<result property="wxAccountAppid" column="wx_account_appid"/>
<result property="wxAccountName" column="wx_account_name"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
</mapper>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pig4cloud.pigx.mp.mapper.WxAccountMapper">
<resultMap id="wxAccountMap" type="com.pig4cloud.pigx.mp.entity.WxAccount">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="account" column="account"/>
<result property="appid" column="appid"/>
<result property="appsecret" column="appsecret"/>
<result property="url" column="url"/>
<result property="token" column="token"/>
<result property="aeskey" column="aeskey"/>
<result property="qrUrl" column="qr_url"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
</mapper>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2018-2019
~ All rights reserved, Designed By www.joolun.com
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pig4cloud.pigx.mp.mapper.WxAutoReplyMapper">
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pig4cloud.pigx.mp.mapper.WxMenuMapper">
</mapper>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright (C) 2018-2019
~ All rights reserved, Designed By www.joolun.com
-->
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.pig4cloud.pigx.mp.mapper.WxMsgMapper">
</mapper>