Commit f05e086aab719fed978bd7f11f94ffff69e003d9

Authored by YunaiV
1 parent a341c44c

mp:实现 user 的批量同步

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
  1 +### 请求 /mp/user/sync 接口 => 成功
  2 +POST {{baseUrl}}/mp/user/sync?accountId=1
  3 +Content-Type: application/json
  4 +Authorization: Bearer {{token}}
  5 +tenant-id: {{adminTenentId}}
... ...
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
... ... @@ -17,6 +17,9 @@ import me.chanjar.weixin.mp.bean.tag.WxUserTag;
17 17 @Data
18 18 @EqualsAndHashCode(callSuper = true)
19 19 @ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
20 23 public class MpTagDO extends BaseDO {
21 24  
22 25 /**
... ...
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&lt;MpUserDO&gt; {
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) {
... ...