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,8 +63,8 @@ public class MpTagController {
63 } 63 }
64 64
65 @PostMapping("/sync") 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 @PreAuthorize("@ss.hasPermission('mp:tag:sync')") 68 @PreAuthorize("@ss.hasPermission('mp:tag:sync')")
69 public CommonResult<Boolean> syncTag(@RequestParam("accountId") Long accountId) { 69 public CommonResult<Boolean> syncTag(@RequestParam("accountId") Long accountId) {
70 mpTagService.syncTag(accountId); 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,6 +7,7 @@ import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 7 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
8 import cn.iocoder.yudao.module.mp.service.user.MpUserService; 8 import cn.iocoder.yudao.module.mp.service.user.MpUserService;
9 import io.swagger.annotations.Api; 9 import io.swagger.annotations.Api;
  10 +import io.swagger.annotations.ApiImplicitParam;
10 import io.swagger.annotations.ApiOperation; 11 import io.swagger.annotations.ApiOperation;
11 import org.springframework.security.access.prepost.PreAuthorize; 12 import org.springframework.security.access.prepost.PreAuthorize;
12 import org.springframework.validation.annotation.Validated; 13 import org.springframework.validation.annotation.Validated;
@@ -34,4 +35,13 @@ public class MpUserController { @@ -34,4 +35,13 @@ public class MpUserController {
34 return success(MpUserConvert.INSTANCE.convertPage(pageResult)); 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,6 +3,7 @@ package cn.iocoder.yudao.module.mp.convert.user;
3 import cn.hutool.core.date.LocalDateTimeUtil; 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 4 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
5 import cn.iocoder.yudao.framework.common.pojo.PageResult; 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  6 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
6 import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserRespVO; 7 import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserRespVO;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 8 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 9 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
@@ -44,4 +45,8 @@ public interface MpUserConvert { @@ -44,4 +45,8 @@ public interface MpUserConvert {
44 return user; 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,6 +17,9 @@ import me.chanjar.weixin.mp.bean.tag.WxUserTag;
17 @Data 17 @Data
18 @EqualsAndHashCode(callSuper = true) 18 @EqualsAndHashCode(callSuper = true)
19 @ToString(callSuper = true) 19 @ToString(callSuper = true)
  20 +@Builder
  21 +@NoArgsConstructor
  22 +@AllArgsConstructor
20 public class MpTagDO extends BaseDO { 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,20 +2,24 @@ package cn.iocoder.yudao.module.mp.dal.dataobject.user;
2 2
3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 3 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; 4 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
  5 +import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
5 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 6 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
  7 +import cn.iocoder.yudao.module.mp.dal.dataobject.tag.MpTagDO;
6 import com.baomidou.mybatisplus.annotation.KeySequence; 8 import com.baomidou.mybatisplus.annotation.KeySequence;
  9 +import com.baomidou.mybatisplus.annotation.TableField;
7 import com.baomidou.mybatisplus.annotation.TableId; 10 import com.baomidou.mybatisplus.annotation.TableId;
8 import com.baomidou.mybatisplus.annotation.TableName; 11 import com.baomidou.mybatisplus.annotation.TableName;
9 import lombok.*; 12 import lombok.*;
10 13
11 import java.time.LocalDateTime; 14 import java.time.LocalDateTime;
  15 +import java.util.List;
12 16
13 /** 17 /**
14 * 微信公众号粉丝 DO 18 * 微信公众号粉丝 DO
15 * 19 *
16 * @author 芋道源码 20 * @author 芋道源码
17 */ 21 */
18 -@TableName("mp_user") 22 +@TableName(value = "mp_user", autoResultMap = true)
19 @KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 23 @KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
20 @Data 24 @Data
21 @EqualsAndHashCode(callSuper = true) 25 @EqualsAndHashCode(callSuper = true)
@@ -82,6 +86,13 @@ public class MpUserDO extends BaseDO { @@ -82,6 +86,13 @@ public class MpUserDO extends BaseDO {
82 * 备注 86 * 备注
83 */ 87 */
84 private String remark; 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 * 微信公众号 ID 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,6 +7,8 @@ import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 7 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
8 import org.apache.ibatis.annotations.Mapper; 8 import org.apache.ibatis.annotations.Mapper;
9 9
  10 +import java.util.List;
  11 +
10 @Mapper 12 @Mapper
11 public interface MpUserMapper extends BaseMapperX<MpUserDO> { 13 public interface MpUserMapper extends BaseMapperX<MpUserDO> {
12 14
@@ -18,9 +20,16 @@ public interface MpUserMapper extends BaseMapperX&lt;MpUserDO&gt; { @@ -18,9 +20,16 @@ public interface MpUserMapper extends BaseMapperX&lt;MpUserDO&gt; {
18 .orderByDesc(MpUserDO::getId)); 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 return selectOne(MpUserDO::getAppId, appId, 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,61 +9,68 @@ import java.util.Collection;
9 import java.util.List; 9 import java.util.List;
10 10
11 /** 11 /**
12 - * 微信公众号粉丝 Service 接口 12 + * 公众号粉丝 Service 接口
13 * 13 *
14 * @author 芋道源码 14 * @author 芋道源码
15 */ 15 */
16 public interface MpUserService { 16 public interface MpUserService {
17 17
18 /** 18 /**
19 - * 获得微信公众号粉丝 19 + * 获得公众号粉丝
20 * 20 *
21 * @param id 编号 21 * @param id 编号
22 - * @return 微信公众号粉丝 22 + * @return 公众号粉丝
23 */ 23 */
24 MpUserDO getUser(Long id); 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 MpUserDO getUser(String appId, String openId); 33 MpUserDO getUser(String appId, String openId);
34 34
35 /** 35 /**
36 - * 获得微信公众号粉丝列表 36 + * 获得公众号粉丝列表
37 * 37 *
38 * @param ids 编号 38 * @param ids 编号
39 - * @return 微信公众号粉丝列表 39 + * @return 公众号粉丝列表
40 */ 40 */
41 List<MpUserDO> getUserList(Collection<Long> ids); 41 List<MpUserDO> getUserList(Collection<Long> ids);
42 42
43 /** 43 /**
44 - * 获得微信公众号粉丝分页 44 + * 获得公众号粉丝分页
45 * 45 *
46 * @param pageReqVO 分页查询 46 * @param pageReqVO 分页查询
47 - * @return 微信公众号粉丝分页 47 + * @return 公众号粉丝分页
48 */ 48 */
49 PageResult<MpUserDO> getUserPage(MpUserPageReqVO pageReqVO); 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 MpUserDO saveUser(String appId, WxMpUser wxMpUser); 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 void updateUserUnsubscribe(String appId, String openId); 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 package cn.iocoder.yudao.module.mp.service.user; 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 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 5 import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
4 import cn.iocoder.yudao.framework.common.pojo.PageResult; 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
5 import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO; 8 import cn.iocoder.yudao.module.mp.controller.admin.user.vo.MpUserPageReqVO;
6 import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert; 9 import cn.iocoder.yudao.module.mp.convert.user.MpUserConvert;
7 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO; 10 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO; 11 import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
9 import cn.iocoder.yudao.module.mp.dal.mysql.user.MpUserMapper; 12 import cn.iocoder.yudao.module.mp.dal.mysql.user.MpUserMapper;
  13 +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
10 import cn.iocoder.yudao.module.mp.service.account.MpAccountService; 14 import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
11 import lombok.extern.slf4j.Slf4j; 15 import lombok.extern.slf4j.Slf4j;
  16 +import me.chanjar.weixin.common.error.WxErrorException;
  17 +import me.chanjar.weixin.mp.api.WxMpService;
12 import me.chanjar.weixin.mp.bean.result.WxMpUser; 18 import me.chanjar.weixin.mp.bean.result.WxMpUser;
  19 +import me.chanjar.weixin.mp.bean.result.WxMpUserList;
13 import org.springframework.context.annotation.Lazy; 20 import org.springframework.context.annotation.Lazy;
  21 +import org.springframework.scheduling.annotation.Async;
14 import org.springframework.stereotype.Service; 22 import org.springframework.stereotype.Service;
15 import org.springframework.validation.annotation.Validated; 23 import org.springframework.validation.annotation.Validated;
16 24
17 import javax.annotation.Resource; 25 import javax.annotation.Resource;
18 import java.time.LocalDateTime; 26 import java.time.LocalDateTime;
  27 +import java.util.ArrayList;
19 import java.util.Collection; 28 import java.util.Collection;
20 import java.util.List; 29 import java.util.List;
  30 +import java.util.Map;
21 31
22 /** 32 /**
23 * 微信公众号粉丝 Service 实现类 33 * 微信公众号粉丝 Service 实现类
@@ -31,7 +41,11 @@ public class MpUserServiceImpl implements MpUserService { @@ -31,7 +41,11 @@ public class MpUserServiceImpl implements MpUserService {
31 41
32 @Resource 42 @Resource
33 @Lazy // 延迟加载,解决循环依赖的问题 43 @Lazy // 延迟加载,解决循环依赖的问题
34 - private MpAccountService accountService; 44 + private MpAccountService mpAccountService;
  45 +
  46 + @Resource
  47 + @Lazy // 延迟加载,解决循环依赖的问题
  48 + private MpServiceFactory mpServiceFactory;
35 49
36 @Resource 50 @Resource
37 private MpUserMapper mpUserMapper; 51 private MpUserMapper mpUserMapper;
@@ -59,7 +73,7 @@ public class MpUserServiceImpl implements MpUserService { @@ -59,7 +73,7 @@ public class MpUserServiceImpl implements MpUserService {
59 @Override 73 @Override
60 public MpUserDO saveUser(String appId, WxMpUser wxMpUser) { 74 public MpUserDO saveUser(String appId, WxMpUser wxMpUser) {
61 // 构建保存的 MpUserDO 对象 75 // 构建保存的 MpUserDO 对象
62 - MpAccountDO account = accountService.getAccountFromCache(appId); 76 + MpAccountDO account = mpAccountService.getAccountFromCache(appId);
63 MpUserDO user = MpUserConvert.INSTANCE.convert(account, wxMpUser); 77 MpUserDO user = MpUserConvert.INSTANCE.convert(account, wxMpUser);
64 78
65 // 根据情况,插入或更新 79 // 根据情况,插入或更新
@@ -74,6 +88,74 @@ public class MpUserServiceImpl implements MpUserService { @@ -74,6 +88,74 @@ public class MpUserServiceImpl implements MpUserService {
74 } 88 }
75 89
76 @Override 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 public void updateUserUnsubscribe(String appId, String openId) { 159 public void updateUserUnsubscribe(String appId, String openId) {
78 MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, openId); 160 MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, openId);
79 if (dbUser == null) { 161 if (dbUser == null) {