GitEgg框架集成weixin-java-miniapp工具包以实现微信小程序相关接口调用功能,weixin-java-miniapp底层支持多租户扩展。每个小程序都有唯一的appid,weixin-java-miniapp的多租户实现并不是以租户标识TenantId来区分的,而是在接口调用时,传入appid,动态切换ThreadLocal的appid来实现多租户的。并且其多个微信小程序的配置,都是在配置yml文件中的,在实际业务运营过程中,如果需要新增多租户小程序就修改配置文件显然是不合适的。
现在我们需要结合weixin-java-miniapp的多租户实现整合到我们的框架中,使多租户可通过系统配置界面来新增多租户小程序。前面我们讲了如何集成以及如何使用weixin-java-miniapp实现微信授权登录及账号绑定等,现在只需要在原来的基础上增加数据配置存储,在服务启动时由原先的读取配置文件加载相应的微信小程序接口实例,修改为可以通过读取配置文件和读取缓存配置来生成相应的微信小程序接口实例。
在数据库设计的时候,我们需要知道微信小程序授权时,哪些字段需要配置,是可选字段还是必填字段,这里我们通过weixin-java-miniapp的springboot工程配置文件可知,所需字段有:
# 公众号配置(必填)
wx.miniapp.appid = appId
wx.miniapp.secret = @secret
wx.miniapp.token = @token
wx.miniapp.aesKey = @aesKey
wx.miniapp.msgDataFormat = @msgDataFormat # 消息格式,XML或者JSON.
# 存储配置redis(可选)
# 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool)
wx.miniapp.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate
wx.miniapp.config-storage.key-prefix = wa # 相关redis前缀配置: wa(默认)
wx.miniapp.config-storage.redis.host = 127.0.0.1
wx.miniapp.config-storage.redis.port = 6379
# http客户端配置
wx.miniapp.config-storage.http-client-type=HttpClient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp
wx.miniapp.config-storage.http-proxy-host=
wx.miniapp.config-storage.http-proxy-port=
wx.miniapp.config-storage.http-proxy-username=
wx.miniapp.config-storage.http-proxy-password=
根据我们的设计,配置文件中需要增加租户字段,我们需要兼容即使用配置文件来配置微信小程序,又可以使用配置界面将微信小程序配置信息配置到数据库中,同时,增加md5字段配置,用于在读取配置时比较配置信息是否有更改。所以,保存微信小程序配置的数据库设计如下:
CREATE TABLE `t_wechat_miniapp` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '租户id',
`miniapp_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序名称',
`appid` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序appid',
`secret` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序secret',
`token` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序token',
`aes_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '微信小程序aesKey',
`msg_data_format` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '消息格式,XML或者JSON',
`storage_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置类型: Memory(默认), Jedis, RedisTemplate',
`key_prefix` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '相关redis前缀配置: wa(默认)',
`redis_host` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'Redis服务器地址',
`redis_port` int(11) NULL DEFAULT NULL COMMENT 'Redis服务器端口',
`http_client_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http客户端类型: HttpClient(默认), OkHttp, JoddHttp',
`http_proxy_host` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_host',
`http_proxy_port` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_port',
`http_proxy_username` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_username',
`http_proxy_password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'http_proxy_password',
`status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '1' COMMENT '状态 1有效 0禁用',
`md5` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'MD5',
`comments` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '描述',
`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',
`creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间',
`operator` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
`del_flag` tinyint(2) NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '微信小程序配置' ROW_FORMAT = DYNAMIC;
通过代码生成器根据新设计的表进行CRUD代码生成,详细步骤不再赘述,前面有详细讲解如何根据数据库表设计生成前后端代码。只是这里需要增加业务逻辑处理,以及更新到缓存配置。
/**
* 创建微信小程序配置
* @param miniapp
* @return
*/
@Override
public boolean createMiniapp(CreateMiniappDTO miniapp) {
Miniapp miniappEntity = BeanCopierUtils.copyByClass(miniapp, Miniapp.class);
try {
String miniappEntityStr = JsonUtils.objToJson(miniappEntity);
miniappEntity.setMd5(SecureUtil.md5(miniappEntityStr));
} catch (Exception e) {
log.error("创建微信小程序配置时,md5加密失败:{}", e);
throw new BusinessException("创建微信小程序配置时,md5加密失败:" + e);
}
boolean result = this.save(miniappEntity);
if (result)
{
// 更新到缓存
Miniapp miniappEntityLocal = this.getById(miniappEntity.getId());
MiniappDTO miniappDTO = BeanCopierUtils.copyByClass(miniappEntityLocal, MiniappDTO.class);
this.addOrUpdateMiniappCache(miniappDTO);
}
return result;
}
/**
* 更新微信小程序配置
* @param miniapp
* @return
*/
@Override
public boolean updateMiniapp(UpdateMiniappDTO miniapp) {
Miniapp miniappEntity = BeanCopierUtils.copyByClass(miniapp, Miniapp.class);
Miniapp miniappEntityOld = this.getById(miniappEntity.getId());
try {
String miniappEntityStr = JsonUtils.objToJson(miniappEntity);
miniappEntity.setMd5(SecureUtil.md5(miniappEntityStr));
} catch (Exception e) {
log.error("创建微信小程序配置时,md5加密失败:{}", e);
throw new BusinessException("创建微信小程序配置时,md5加密失败:" + e);
}
boolean result = this.updateById(miniappEntity);
if (result)
{
// 把旧的删掉
MiniappDTO miniappDTOOld = BeanCopierUtils.copyByClass(miniappEntityOld, MiniappDTO.class);
this.deleteMiniappCache(miniappDTOOld);
// 更新到缓存
Miniapp miniappEntityLocal = this.getById(miniappEntity.getId());
MiniappDTO miniappDTO = BeanCopierUtils.copyByClass(miniappEntityLocal, MiniappDTO.class);
this.addOrUpdateMiniappCache(miniappDTO);
}
return result;
}
/**
* 删除微信小程序配置
* @param miniappId
* @return
*/
@Override
public boolean deleteMiniapp(Long miniappId) {
// 从缓存删除
Miniapp miniappEntity = this.getById(miniappId);
MiniappDTO miniappDTO = BeanCopierUtils.copyByClass(miniappEntity, MiniappDTO.class);
this.deleteMiniappCache(miniappDTO);
// 从数据库中删除
boolean result = this.removeById(miniappId);
return result;
}
private void addOrUpdateMiniappCache(MiniappDTO miniappDTO) {
try {
String redisKey = MiniappConstant.WX_MINIAPP_CONFIG_KEY;
if (enable) {
redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappDTO.getAppid();
}
redisTemplate.opsForHash().put(redisKey, miniappDTO.getTenantId().toString(), JsonUtils.objToJson(miniappDTO));
// wxMaService增加config
this.addConfig(miniappDTO);
} catch (Exception e) {
log.error("初始化微信小程序配置失败:{}" , e);
}
}
private void deleteMiniappCache(MiniappDTO miniappDTO) {
try {
String redisKey = MiniappConstant.WX_MINIAPP_CONFIG_KEY;
if (enable) {
redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappDTO.getAppid();
}
redisTemplate.opsForHash().delete(redisKey, miniappDTO.getTenantId().toString(), JsonUtils.objToJson(miniappDTO));
// wxMaService删除config
this.removeConfig(miniappDTO);
} catch (Exception e) {
log.error("初始化微信小程序配置失败:{}" , e);
}
}
/**
* @author GitEgg
* @date 2023/7/21
*/
public class GitEggWxMaRedissonConfigImpl extends WxMaRedissonConfigImpl {
protected String configKey;
protected String tenantId;
protected String md5;
public GitEggWxMaRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
super(redissonClient, keyPrefix);
}
public GitEggWxMaRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
super(redissonClient);
}
......
}
@Data
@ConfigurationProperties(prefix = "wx.miniapp")
public class WxMaProperties {
private List<Config> configs;
@Data
public static class Config {
/**
* 租户
*/
private Long tenantId;
/**
* 设置微信小程序的appid
*/
private String appid;
/**
* 设置微信小程序的Secret
*/
private String secret;
/**
* 设置微信小程序消息服务器配置的token
*/
private String token;
/**
* 设置微信小程序消息服务器配置的EncodingAESKey
*/
private String aesKey;
/**
* 消息格式,XML或者JSON
*/
private String msgDataFormat;
}
}
@Bean
public WxMaService wxMaService() {
List<WxMaProperties.Config> configs = this.properties.getConfigs();
//已添加缓存配置,如果配置文件没有,那么在缓存新增时,仍然可以setConfigs
// if (configs == null) {
// throw new WxRuntimeException("大哥,拜托先看下项目首页的说明(readme文件),添加下相关配置,注意别配错了!");
// }
WxMaService maService = new WxMaServiceImpl();
if (null != configs)
{
maService.setMultiConfigs(
configs.stream()
.map(a -> {
// WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
// WxMaDefaultConfigImpl config = new WxMaRedisConfigImpl(new JedisPool());
GitEggWxMaRedissonConfigImpl config = new GitEggWxMaRedissonConfigImpl(redissonClient);
// 使用上面的配置时,需要同时引入jedis-lock的依赖,否则会报类无法找到的异常
config.setTenantId(null != a.getTenantId() ? a.getTenantId().toString() : AuthConstant.DEFAULT_TENANT_ID.toString());
config.setConfigKey(config.getTenantId() + StrPool.UNDERLINE + a.getAppid());
config.setAppid(a.getAppid());
config.setSecret(a.getSecret());
config.setToken(a.getToken());
config.setAesKey(a.getAesKey());
config.setMsgDataFormat(a.getMsgDataFormat());
return config;
}).collect(Collectors.toMap( GitEggWxMaRedissonConfigImpl::getConfigKey, a -> a, (o, n) -> o)));
}
return maService;
}
/**
* 排除多租户插件查询微信配置列表
* @param miniappDTO
* @return
*/
@InterceptorIgnore(tenantLine = "true")
List<MiniappDTO> initMiniappList(@Param("miniapp") QueryMiniappDTO miniappDTO);
<!-- 不区分租户查询微信小程序配置信息 -->
<select id="getMiniapp" resultType="com.gitegg.boot.extension.wx.miniapp.dto.MiniappDTO" parameterType="com.gitegg.boot.extension.wx.miniapp.dto.QueryMiniappDTO">
SELECT
<include refid="Base_Column_List"/>
FROM t_wechat_miniapp
WHERE del_flag = 0
<if test="miniapp.miniappName != null and miniapp.miniappName != ''">
AND miniapp_name = #{miniapp.miniappName}
</if>
<if test="miniapp.appid != null and miniapp.appid != ''">
AND appid = #{miniapp.appid}
</if>
<if test="miniapp.secret != null and miniapp.secret != ''">
AND secret = #{miniapp.secret}
</if>
<if test="miniapp.status != null and miniapp.status != ''">
AND status = #{miniapp.status}
</if>
ORDER BY id DESC
</select>
/**
* 初始化微信小程序配置表列表
* @return
*/
@Override
public void initMiniappList() {
QueryMiniappDTO miniappDTO = new QueryMiniappDTO();
miniappDTO.setStatus(String.valueOf(GitEggConstant.ENABLE));
// 这里初始化所有的配置,不再只初始化已启用的配置
List<MiniappDTO> miniappInfoList = miniappMapper.initMiniappList(miniappDTO);
// 判断是否开启了租户模式,如果开启了,那么需要按租户进行分类存储
if (enable) {
Map<String, List<MiniappDTO>> miniappListMap =
miniappInfoList.stream().collect(Collectors.groupingBy(MiniappDTO::getAppid));
miniappListMap.forEach((key, value) -> {
String redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + key;
redisTemplate.delete(redisKey);
addMiniapp(redisKey, value);
});
} else {
redisTemplate.delete(MiniappConstant.WX_MINIAPP_CONFIG_KEY);
addMiniapp(MiniappConstant.WX_MINIAPP_CONFIG_KEY, miniappInfoList);
}
}
/**
* 容器启动完成加载扩展信息数据到缓存
* @author GitEgg
*/
@Slf4j
@RequiredArgsConstructor(onConstructor_ = @Autowired)
@Component
public class InitExtensionCacheRunner implements CommandLineRunner {
private final IJustAuthConfigService justAuthConfigService;
private final IJustAuthSourceService justAuthSourceService;
private final IMailChannelService mailChannelService;
private final IMiniappService miniappService;
@Override
public void run(String... args) {
log.info("InitExtensionCacheRunner running");
// 初始化第三方登录主配置
justAuthConfigService.initJustAuthConfigList();
// 初始化第三方登录 第三方配置
justAuthSourceService.initJustAuthSourceList();
// 初始化邮件配置信息
mailChannelService.initMailChannelList();
// 初始化微信配置信息
miniappService.initMiniappList();
}
}
/**
* 通过appid获取appid,忽略租户插件
* @param miniappId
* @return
*/
@Override
public String getMiniappId(String miniappId) {
if (enable) {
// 如果前端传了租户,那么先使用前端的租户,如果没有传租户,那么从系统中查询租户
String tenantId = GitEggAuthUtils.getTenantId();
if (!StringUtils.isEmpty(tenantId))
{
String miniappStr = (String) redisTemplate.opsForHash().get(MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappId, tenantId);
if (!StringUtils.isEmpty(miniappStr))
{
// 转为系统配置对象
try {
// 从缓存获取配置对象,如果md5配置和系统配置不一样,那么需要重新add
MiniappDTO miniappDTO = JsonUtils.jsonToPojo(miniappStr, MiniappDTO.class);
return this.ifConfig(miniappDTO);
} catch (Exception e) {
log.error("获取微信小程序配置时发生异常:{}", e);
throw new BusinessException("获取微信小程序配置时发生异常。");
}
}
// 缓存配置中没有也需要直接返回,因为有可能是配置文件配置的
return tenantId + StrPool.UNDERLINE + miniappId;
} else {
String redisKey = MiniappConstant.WX_MINIAPP_TENANT_CONFIG_KEY + miniappId;
// 取缓存中所有appid的配置租户,如果存在多个租户,那么提示错误,让前端选择租户;如果只有一个租户,那么返回
List<Object> values = redisTemplate.opsForHash().values(redisKey);
if (!CollectionUtils.isEmpty(values))
{
if (values.size() > GitEggConstant.Number.ONE)
{
throw new BusinessException("此小程序配置在多个租户下,请选择所需要操作的租户。");
}
String miniappConfig = (String) values.stream().findFirst().orElse(null);
try {
MiniappDTO miniappConfigDTO = JsonUtils.jsonToPojo(miniappConfig, MiniappDTO.class);
return this.ifConfig(miniappConfigDTO);
} catch (Exception e) {
log.error("获取缓存小程序配置失败:{}", e);
throw new BusinessException("小程序已被禁用,请联系管理员");
}
}
else
{
return AuthConstant.DEFAULT_TENANT_ID + StrPool.UNDERLINE + miniappId;
}
}
} else {
return AuthConstant.DEFAULT_TENANT_ID + StrPool.UNDERLINE + miniappId;
}
}
if (!miniappService.switchover(appid)) {
throw new IllegalArgumentException(String.format("未找到对应appid=[%s]的配置,请核实!", appid));
}
import request from '@/common/utils/request'
const wechatLoginApi = {
Login: '/extension/wx/user/'
}
export default wechatLoginApi
/**
* 微信登录
* @param {Object} appId
* @param {Object} parameter
*/
export function wechatLogin (appId, parameter) {
return request({
url: wechatLoginApi.Login + appId + '/login',
method: 'get',
params: parameter
})
}
/**
* 获取微信信息
* @param {Object} appId
* @param {Object} parameter
*/
export function wechatInfo (appId, parameter) {
return request({
url: wechatLoginApi.Login + appId + '/info',
method: 'get',
params: parameter
})
}
/**
* 获取手机号
* @param {Object} appId
* @param {Object} parameter
*/
export function wechatPhone (appId, parameter) {
return request({
url: wechatLoginApi.Login + appId + '/phone',
method: 'get',
params: parameter
})
}
在修改配置时,一定需要注意权限问题,一般情况下,在不同的租户下不允许配置相同的微信小程序,因为appid是唯一的,在发布微信小程序后,微信小程序是唯一的。当然也有特殊的情况,比如,同一个小程序作为多个租户相同的商户管理端,那么在此时,需要让用户在前端选择输入租户标识以确定登录用户属于那个租户,即多个租户共用同一个微信小程序。
Gitee:
GitHub: