Commit f05e086aab719fed978bd7f11f94ffff69e003d9
1 parent
a341c44c
mp:实现 user 的批量同步
Showing
9 changed files
with
157 additions
and
25 deletions
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/tag/MpTagController.java
| ... | ... | @@ -63,8 +63,8 @@ public class MpTagController { |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 65 | @PostMapping("/sync") |
| 66 | - @ApiOperation("同步公众标签") | |
| 67 | - @ApiImplicitParam(name = "id", value = "公众号账号的编号", required = true, dataTypeClass = Long.class) | |
| 66 | + @ApiOperation("同步公众号标签") | |
| 67 | + @ApiImplicitParam(name = "accountId", value = "公众号账号的编号", required = true, dataTypeClass = Long.class) | |
| 68 | 68 | @PreAuthorize("@ss.hasPermission('mp:tag:sync')") |
| 69 | 69 | public CommonResult<Boolean> syncTag(@RequestParam("accountId") Long accountId) { |
| 70 | 70 | mpTagService.syncTag(accountId); | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.http
0 → 100644
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/user/MpUserController.java
| ... | ... | @@ -7,6 +7,7 @@ import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert; |
| 7 | 7 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 8 | 8 | import cn.iocoder.yudao.module.mp.service.user.MpUserService; |
| 9 | 9 | import io.swagger.annotations.Api; |
| 10 | +import io.swagger.annotations.ApiImplicitParam; | |
| 10 | 11 | import io.swagger.annotations.ApiOperation; |
| 11 | 12 | import org.springframework.security.access.prepost.PreAuthorize; |
| 12 | 13 | import org.springframework.validation.annotation.Validated; |
| ... | ... | @@ -34,4 +35,13 @@ public class MpUserController { |
| 34 | 35 | return success(MpUserConvert.INSTANCE.convertPage(pageResult)); |
| 35 | 36 | } |
| 36 | 37 | |
| 38 | + @PostMapping("/sync") | |
| 39 | + @ApiOperation("同步公众号粉丝") | |
| 40 | + @ApiImplicitParam(name = "accountId", value = "公众号账号的编号", required = true, dataTypeClass = Long.class) | |
| 41 | + @PreAuthorize("@ss.hasPermission('mp:user:sync')") | |
| 42 | + public CommonResult<Boolean> syncUser(@RequestParam("accountId") Long accountId) { | |
| 43 | + mpUserService.syncUser(accountId); | |
| 44 | + return success(true); | |
| 45 | + } | |
| 46 | + | |
| 37 | 47 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/user/MpUserConvert.java
| ... | ... | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.mp.convert.user; |
| 3 | 3 | import cn.hutool.core.date.LocalDateTimeUtil; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 6 | +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 6 | 7 | import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserRespVO; |
| 7 | 8 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 8 | 9 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| ... | ... | @@ -44,4 +45,8 @@ public interface MpUserConvert { |
| 44 | 45 | return user; |
| 45 | 46 | } |
| 46 | 47 | |
| 48 | + default List<MpUserDO> convertList(MpAccountDO account, List<WxMpUser> wxUsers) { | |
| 49 | + return CollectionUtils.convertList(wxUsers, wxUser -> convert(account, wxUser)); | |
| 50 | + } | |
| 51 | + | |
| 47 | 52 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/tag/MpTagDO.java
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/user/MpUserDO.java
| ... | ... | @@ -2,20 +2,24 @@ package cn.iocoder.yudao.module.mp.dal.dataobject.user; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 4 | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; | |
| 5 | 6 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 7 | +import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO; | |
| 6 | 8 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 9 | +import com.baomidou.mybatisplus.annotation.TableField; | |
| 7 | 10 | import com.baomidou.mybatisplus.annotation.TableId; |
| 8 | 11 | import com.baomidou.mybatisplus.annotation.TableName; |
| 9 | 12 | import lombok.*; |
| 10 | 13 | |
| 11 | 14 | import java.time.LocalDateTime; |
| 15 | +import java.util.List; | |
| 12 | 16 | |
| 13 | 17 | /** |
| 14 | 18 | * 微信公众号粉丝 DO |
| 15 | 19 | * |
| 16 | 20 | * @author 芋道源码 |
| 17 | 21 | */ |
| 18 | -@TableName("mp_user") | |
| 22 | +@TableName(value = "mp_user", autoResultMap = true) | |
| 19 | 23 | @KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 |
| 20 | 24 | @Data |
| 21 | 25 | @EqualsAndHashCode(callSuper = true) |
| ... | ... | @@ -82,6 +86,13 @@ public class MpUserDO extends BaseDO { |
| 82 | 86 | * 备注 |
| 83 | 87 | */ |
| 84 | 88 | private String remark; |
| 89 | + /** | |
| 90 | + * 标签编号数组 | |
| 91 | + * | |
| 92 | + * 注意,对应的是 {@link MpTagDO#getTagId()} 字段 | |
| 93 | + */ | |
| 94 | + @TableField(typeHandler = LongListTypeHandler.class) | |
| 95 | + private List<Long> tagIds; | |
| 85 | 96 | |
| 86 | 97 | /** |
| 87 | 98 | * 微信公众号 ID | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/user/MpUserMapper.java
| ... | ... | @@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO; |
| 7 | 7 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 8 | 8 | import org.apache.ibatis.annotations.Mapper; |
| 9 | 9 | |
| 10 | +import java.util.List; | |
| 11 | + | |
| 10 | 12 | @Mapper |
| 11 | 13 | public interface MpUserMapper extends BaseMapperX<MpUserDO> { |
| 12 | 14 | |
| ... | ... | @@ -18,9 +20,16 @@ public interface MpUserMapper extends BaseMapperX<MpUserDO> { |
| 18 | 20 | .orderByDesc(MpUserDO::getId)); |
| 19 | 21 | } |
| 20 | 22 | |
| 21 | - default MpUserDO selectByAppIdAndOpenid(String appId, String openId) { | |
| 23 | + default MpUserDO selectByAppIdAndOpenid(String appId, String openid) { | |
| 22 | 24 | return selectOne(MpUserDO::getAppId, appId, |
| 23 | - MpUserDO::getOpenid, openId); | |
| 25 | + MpUserDO::getOpenid, openid); | |
| 26 | + } | |
| 27 | + | |
| 28 | + default List<MpUserDO> selectListByAppIdAndOpenid(String appId, List<String> openids) { | |
| 29 | + return selectList(new LambdaQueryWrapperX<MpUserDO>() | |
| 30 | + .eq(MpUserDO::getAppId, appId) | |
| 31 | + .in(MpUserDO::getOpenid, openids)); | |
| 32 | + | |
| 24 | 33 | } |
| 25 | 34 | |
| 26 | 35 | } | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserService.java
| ... | ... | @@ -9,61 +9,68 @@ import java.util.Collection; |
| 9 | 9 | import java.util.List; |
| 10 | 10 | |
| 11 | 11 | /** |
| 12 | - * 微信公众号粉丝 Service 接口 | |
| 12 | + * 公众号粉丝 Service 接口 | |
| 13 | 13 | * |
| 14 | 14 | * @author 芋道源码 |
| 15 | 15 | */ |
| 16 | 16 | public interface MpUserService { |
| 17 | 17 | |
| 18 | 18 | /** |
| 19 | - * 获得微信公众号粉丝 | |
| 19 | + * 获得公众号粉丝 | |
| 20 | 20 | * |
| 21 | 21 | * @param id 编号 |
| 22 | - * @return 微信公众号粉丝 | |
| 22 | + * @return 公众号粉丝 | |
| 23 | 23 | */ |
| 24 | 24 | MpUserDO getUser(Long id); |
| 25 | 25 | |
| 26 | 26 | /** |
| 27 | - * 使用 appId + openId,获得微信公众号粉丝 | |
| 27 | + * 使用 appId + openId,获得公众号粉丝 | |
| 28 | 28 | * |
| 29 | - * @param appId 微信公众号 appId | |
| 30 | - * @param openId 微信公众号 openId | |
| 31 | - * @return 微信公众号粉丝 | |
| 29 | + * @param appId 公众号 appId | |
| 30 | + * @param openId 公众号 openId | |
| 31 | + * @return 公众号粉丝 | |
| 32 | 32 | */ |
| 33 | 33 | MpUserDO getUser(String appId, String openId); |
| 34 | 34 | |
| 35 | 35 | /** |
| 36 | - * 获得微信公众号粉丝列表 | |
| 36 | + * 获得公众号粉丝列表 | |
| 37 | 37 | * |
| 38 | 38 | * @param ids 编号 |
| 39 | - * @return 微信公众号粉丝列表 | |
| 39 | + * @return 公众号粉丝列表 | |
| 40 | 40 | */ |
| 41 | 41 | List<MpUserDO> getUserList(Collection<Long> ids); |
| 42 | 42 | |
| 43 | 43 | /** |
| 44 | - * 获得微信公众号粉丝分页 | |
| 44 | + * 获得公众号粉丝分页 | |
| 45 | 45 | * |
| 46 | 46 | * @param pageReqVO 分页查询 |
| 47 | - * @return 微信公众号粉丝分页 | |
| 47 | + * @return 公众号粉丝分页 | |
| 48 | 48 | */ |
| 49 | 49 | PageResult<MpUserDO> getUserPage(MpUserPageReqVO pageReqVO); |
| 50 | 50 | |
| 51 | 51 | /** |
| 52 | - * 保存微信公众号粉丝 | |
| 52 | + * 保存公众号粉丝 | |
| 53 | 53 | * |
| 54 | 54 | * 新增或更新,根据是否存在数据库中 |
| 55 | 55 | * |
| 56 | - * @param appId 微信公众号 appId | |
| 57 | - * @param wxMpUser 微信公众号粉丝的信息 | |
| 58 | - * @return 微信公众号粉丝 | |
| 56 | + * @param appId 公众号 appId | |
| 57 | + * @param wxMpUser 公众号粉丝的信息 | |
| 58 | + * @return 公众号粉丝 | |
| 59 | 59 | */ |
| 60 | 60 | MpUserDO saveUser(String appId, WxMpUser wxMpUser); |
| 61 | 61 | |
| 62 | 62 | /** |
| 63 | - * 更新微信公众号粉丝,取消关注 | |
| 63 | + * 同步一个公众号粉丝 | |
| 64 | 64 | * |
| 65 | - * @param appId 微信公众号 appId | |
| 66 | - * @param openId 微信公众号粉丝的 openid | |
| 65 | + * @param accountId 公众号账号的编号 | |
| 66 | + */ | |
| 67 | + void syncUser(Long accountId); | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * 更新公众号粉丝,取消关注 | |
| 71 | + * | |
| 72 | + * @param appId 公众号 appId | |
| 73 | + * @param openId 公众号粉丝的 openid | |
| 67 | 74 | */ |
| 68 | 75 | void updateUserUnsubscribe(String appId, String openId); |
| 69 | 76 | ... | ... |
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/user/MpUserServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.mp.service.user; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.collection.CollUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 3 | 5 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 4 | 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 5 | 8 | import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO; |
| 6 | 9 | import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert; |
| 7 | 10 | import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; |
| 8 | 11 | import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; |
| 9 | 12 | import cn.iocoder.yudao.module.mp.dal.mysql.user.MpUserMapper; |
| 13 | +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; | |
| 10 | 14 | import cn.iocoder.yudao.module.mp.service.account.MpAccountService; |
| 11 | 15 | import lombok.extern.slf4j.Slf4j; |
| 16 | +import me.chanjar.weixin.common.error.WxErrorException; | |
| 17 | +import me.chanjar.weixin.mp.api.WxMpService; | |
| 12 | 18 | import me.chanjar.weixin.mp.bean.result.WxMpUser; |
| 19 | +import me.chanjar.weixin.mp.bean.result.WxMpUserList; | |
| 13 | 20 | import org.springframework.context.annotation.Lazy; |
| 21 | +import org.springframework.scheduling.annotation.Async; | |
| 14 | 22 | import org.springframework.stereotype.Service; |
| 15 | 23 | import org.springframework.validation.annotation.Validated; |
| 16 | 24 | |
| 17 | 25 | import javax.annotation.Resource; |
| 18 | 26 | import java.time.LocalDateTime; |
| 27 | +import java.util.ArrayList; | |
| 19 | 28 | import java.util.Collection; |
| 20 | 29 | import java.util.List; |
| 30 | +import java.util.Map; | |
| 21 | 31 | |
| 22 | 32 | /** |
| 23 | 33 | * 微信公众号粉丝 Service 实现类 |
| ... | ... | @@ -31,7 +41,11 @@ public class MpUserServiceImpl implements MpUserService { |
| 31 | 41 | |
| 32 | 42 | @Resource |
| 33 | 43 | @Lazy // 延迟加载,解决循环依赖的问题 |
| 34 | - private MpAccountService accountService; | |
| 44 | + private MpAccountService mpAccountService; | |
| 45 | + | |
| 46 | + @Resource | |
| 47 | + @Lazy // 延迟加载,解决循环依赖的问题 | |
| 48 | + private MpServiceFactory mpServiceFactory; | |
| 35 | 49 | |
| 36 | 50 | @Resource |
| 37 | 51 | private MpUserMapper mpUserMapper; |
| ... | ... | @@ -59,7 +73,7 @@ public class MpUserServiceImpl implements MpUserService { |
| 59 | 73 | @Override |
| 60 | 74 | public MpUserDO saveUser(String appId, WxMpUser wxMpUser) { |
| 61 | 75 | // 构建保存的 MpUserDO 对象 |
| 62 | - MpAccountDO account = accountService.getAccountFromCache(appId); | |
| 76 | + MpAccountDO account = mpAccountService.getAccountFromCache(appId); | |
| 63 | 77 | MpUserDO user = MpUserConvert.INSTANCE.convert(account, wxMpUser); |
| 64 | 78 | |
| 65 | 79 | // 根据情况,插入或更新 |
| ... | ... | @@ -74,6 +88,74 @@ public class MpUserServiceImpl implements MpUserService { |
| 74 | 88 | } |
| 75 | 89 | |
| 76 | 90 | @Override |
| 91 | + @Async | |
| 92 | + public void syncUser(Long accountId) { | |
| 93 | + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); | |
| 94 | + // for 循环,避免递归出意外问题,导致死循环 | |
| 95 | + String nextOpenid = null; | |
| 96 | + for (int i = 0; i < Short.MAX_VALUE; i++) { | |
| 97 | + log.info("[syncUser][第({}) 次加载公众号用户列表,nextOpenid({})]", i, nextOpenid); | |
| 98 | + try { | |
| 99 | + nextOpenid = syncUser0(account, nextOpenid); | |
| 100 | + } catch (WxErrorException e) { | |
| 101 | + log.error("[syncUser][第({}) 次同步用户异常]", i, e); | |
| 102 | + break; | |
| 103 | + } | |
| 104 | + // 如果 nextOpenid 为空,表示已经同步完毕 | |
| 105 | + if (StrUtil.isEmpty(nextOpenid)) { | |
| 106 | + break; | |
| 107 | + } | |
| 108 | + } | |
| 109 | + } | |
| 110 | + | |
| 111 | + private String syncUser0(MpAccountDO account, String nextOpenid) throws WxErrorException { | |
| 112 | + // 第一步,从公众号流式加载用户 | |
| 113 | + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getId()); | |
| 114 | + WxMpUserList wxUserList = mpService.getUserService().userList(nextOpenid); | |
| 115 | + if (CollUtil.isEmpty(wxUserList.getOpenids())) { | |
| 116 | + return null; | |
| 117 | + } | |
| 118 | + | |
| 119 | + // 第二步,分批加载用户信息 | |
| 120 | + List<List<String>> openidsList = CollUtil.split(wxUserList.getOpenids(), 100); | |
| 121 | + for (List<String> openids : openidsList) { | |
| 122 | + log.info("[syncUser][批量加载用户信息,openids({})]", openids); | |
| 123 | + List<WxMpUser> wxUsers = mpService.getUserService().userInfoList(openids); | |
| 124 | + batchSaveUser(account, wxUsers); | |
| 125 | + } | |
| 126 | + | |
| 127 | + // 返回下一次的 nextOpenId | |
| 128 | + return wxUserList.getNextOpenid(); | |
| 129 | + } | |
| 130 | + | |
| 131 | + private void batchSaveUser(MpAccountDO account, List<WxMpUser> wxUsers) { | |
| 132 | + if (CollUtil.isEmpty(wxUsers)) { | |
| 133 | + return; | |
| 134 | + } | |
| 135 | + // 1. 获得数据库已保存的用户列表 | |
| 136 | + List<MpUserDO> dbUsers = mpUserMapper.selectListByAppIdAndOpenid(account.getAppId(), | |
| 137 | + CollectionUtils.convertList(wxUsers, WxMpUser::getOpenId)); | |
| 138 | + Map<String, MpUserDO> openId2Users = CollectionUtils.convertMap(dbUsers, MpUserDO::getOpenid); | |
| 139 | + | |
| 140 | + // 2.1 根据情况,插入或更新 | |
| 141 | + List<MpUserDO> users = MpUserConvert.INSTANCE.convertList(account, wxUsers); | |
| 142 | + List<MpUserDO> newUsers = new ArrayList<>(); | |
| 143 | + for (MpUserDO user : users) { | |
| 144 | + MpUserDO dbUser = openId2Users.get(user.getOpenid()); | |
| 145 | + if (dbUser == null) { // 新增:稍后批量插入 | |
| 146 | + newUsers.add(user); | |
| 147 | + } else { // 更新:直接执行更新 | |
| 148 | + user.setId(dbUser.getId()); | |
| 149 | + mpUserMapper.updateById(user); | |
| 150 | + } | |
| 151 | + } | |
| 152 | + // 2.2 批量插入 | |
| 153 | + if (CollUtil.isNotEmpty(newUsers)) { | |
| 154 | + mpUserMapper.insertBatch(newUsers); | |
| 155 | + } | |
| 156 | + } | |
| 157 | + | |
| 158 | + @Override | |
| 77 | 159 | public void updateUserUnsubscribe(String appId, String openId) { |
| 78 | 160 | MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, openId); |
| 79 | 161 | if (dbUser == null) { | ... | ... |