feat(wx-account-fans): 添加粉丝导出功能及数据脱敏处理

- 新增 WxAccountFansExcelVO 类用于粉丝数据的 Excel 导出
- 在 WxAccountFansController 中实现导出接口,支持按条件筛选粉丝
- 更新 WxAccountFansServiceImpl 中的粉丝更新逻辑,确保数据同步至微信
- 对粉丝的 OpenID 进行脱敏处理,确保隐私安全

(cherry picked from commit a277413d3d7681407daca46bbb635f57a032b25f)
This commit is contained in:
PIGCLOUD committed 2026-11-03 14:49:18 +08:00
1 parent e63dc8678e
commit cb08f11a5d
5 files changed
+353 -241

No files matched your search

+4
View File
@@ -18,6 +18,10 @@
</properties>
<dependencies>
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-excel</artifactId>
</dependency>
<!--注册中心客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
@@ -16,7 +16,21 @@
*/
package com.pig4cloud.pigx.mp.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import com.pig4cloud.pigx.common.core.constant.CommonConstants;
import com.pig4cloud.pigx.common.core.constant.enums.YesNoEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.pig4cloud.pigx.common.excel.annotation.ResponseExcel;
import com.pig4cloud.pigx.common.sensitive.util.DesensitizedUtils;
import com.pig4cloud.pigx.mp.entity.vo.WxAccountFansExcelVO;
import java.util.List;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.annotation.SysLog;
import com.pig4cloud.pigx.common.security.annotation.HasPermission;
@@ -26,7 +40,7 @@ import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
/**
* 微信公众号粉丝管理
* 微信公众号粉丝管理,委托服务层维护粉丝及微信状态,并提供按列表条件筛选的脱敏 Excel 导出。
*
* @author lengleng
* @date 2019-03-26 22:08:08
@@ -34,10 +48,43 @@ import org.springframework.web.bind.annotation.*;
@RestController
@AllArgsConstructor
@RequestMapping("/wx-account-fans")
@Tag(name = "公众号粉丝管理")
public class WxAccountFansController {
private final WxAccountFansService wxAccountFansService;
/**
* 导出当前租户内符合筛选条件的全部粉丝,不受分页参数限制。 OpenID 始终按密钥规则脱敏,仅转换导出副本,不修改数据库实体。
* @param query 查询条件;公众号 AppID 为空时不限定公众号,昵称为空时不限定昵称
* @return Excel 行数据;无匹配记录时返回空列表,由 ResponseExcel 输出表头
*/
@Operation(summary = "导出公众号粉丝", description = "按公众号和昵称筛选全部记录,OpenID 脱敏导出")
@ResponseExcel(name = "fans")
@GetMapping("/export")
@HasPermission("mp_wxaccountfans_sync")
public List<WxAccountFansExcelVO> export(WxAccountFans query) {
return wxAccountFansService
.list(Wrappers.<WxAccountFans>lambdaQuery()
.eq(StrUtil.isNotBlank(query.getWxAccountAppid()), WxAccountFans::getWxAccountAppid,
query.getWxAccountAppid())
.like(StrUtil.isNotBlank(query.getNickname()), WxAccountFans::getNickname, query.getNickname()))
.stream()
.map(fan -> {
WxAccountFansExcelVO row = new WxAccountFansExcelVO();
BeanUtil.copyProperties(fan, row, "openid", "subscribeStatus", "subscribeTime", "isBlack", "tagIds");
// Excel 不经过 Jackson 的 @Sensitive 序列化,单独对导出副本脱敏。
row.setOpenid(DesensitizedUtils.key(fan.getOpenid()));
row.setSubscribeStatus(YesNoEnum.YES.getCode().equals(fan.getSubscribeStatus()) ? "已关注" : "未关注");
row.setSubscribeTime(fan.getSubscribeTime() == null ? StrUtil.EMPTY
: LocalDateTimeUtil.format(fan.getSubscribeTime(), DatePattern.NORM_DATETIME_PATTERN));
row.setIsBlack(CommonConstants.FAIL.equals(fan.getIsBlack()) ? YesNoEnum.YES.getDesc()
: YesNoEnum.NO.getDesc());
row.setTagIds(ArrayUtil.join(fan.getTagIds(), StrUtil.COMMA));
return row;
})
.toList();
}
/**
* 分页查询
* @param page 分页对象
@@ -60,15 +107,20 @@ public class WxAccountFansController {
}
/**
* 修改微信公众号粉丝
* @param wxAccountFans 微信公众号粉丝
* @return R
* 修改粉丝备注及标签,通过 ID 恢复数据库中的真实 OpenID 和公众号 AppID 后调用微信。
* @param wxAccountFans 待更新数据,须包含粉丝 ID;回显 OpenID 可为脱敏值
* @return 更新成功返回成功响应;记录不存在返回失败响应,客户端应保留编辑状态并提示刷新
* @throws Exception 数据库或微信调用异常,交由统一异常处理器转换为失败响应
*/
@Operation(summary = "修改公众号粉丝")
@SysLog("修改微信公众号粉丝")
@PutMapping
@HasPermission("mp_wxaccountfans_edit")
public R updateById(@RequestBody WxAccountFans wxAccountFans) {
return R.ok(wxAccountFansService.updateFans(wxAccountFans));
if (!Boolean.TRUE.equals(wxAccountFansService.updateFans(wxAccountFans))) {
return R.failed("粉丝记录不存在,请刷新后重试");
}
return R.ok(Boolean.TRUE);
}
/**
@@ -19,6 +19,8 @@ package com.pig4cloud.pigx.mp.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.extension.activerecord.Model;
import com.pig4cloud.pigx.common.core.util.TenantTable;
import com.pig4cloud.pigx.common.sensitive.annotation.Sensitive;
import com.pig4cloud.pigx.common.sensitive.core.SensitiveTypeEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -46,6 +48,7 @@ public class WxAccountFans extends Model<WxAccountFans> {
/**
* 用户标识
*/
@Sensitive(type = SensitiveTypeEnum.KEY)
private String openid;
/**
@@ -0,0 +1,50 @@
package com.pig4cloud.pigx.mp.entity.vo;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.apache.fesod.sheet.annotation.write.style.ColumnWidth;
/**
* 公众号粉丝 Excel 行,由导出接口从实体转换;状态和日期为展示文本,OpenID 为脱敏副本。 该对象仅用于文件输出,不接受写入请求,不参与粉丝身份识别。
*/
@Data
@ColumnWidth(24)
public class WxAccountFansExcelVO {
/** 用户标识,固定六位掩码,仅保留末三位;null 时单元格为空。 */
@ExcelProperty("OpenID")
private String openid;
/** 关注状态展示文本:已关注或未关注。 */
@ExcelProperty("关注状态")
private String subscribeStatus;
/** 关注时间,格式 yyyy-MM-dd HH:mm:ss;未记录时为空。 */
@ExcelProperty("关注时间")
private String subscribeTime;
/** 粉丝昵称,可为空。 */
@ExcelProperty("昵称")
private String nickname;
/** 微信返回的语言代码,可为空。 */
@ExcelProperty("语言")
private String language;
/** 黑名单状态展示文本:是或否。 */
@ExcelProperty("黑名单")
private String isBlack;
/** 标签 ID 的逗号分隔文本,无标签时为空。 */
@ExcelProperty("标签ID")
private String tagIds;
/** 粉丝备注,可为空。 */
@ExcelProperty("备注")
private String remark;
/** 粉丝所属公众号名称,可为空。 */
@ExcelProperty("公众号名称")
private String wxAccountName;
}
@@ -69,273 +69,276 @@ import java.util.List;
@Service
@AllArgsConstructor
public class WxAccountFansServiceImpl extends ServiceImpl<WxAccountFansMapper, WxAccountFans>
implements WxAccountFansService {
implements WxAccountFansService {
private static final int SIZE = 100;
private static final int SIZE = 100;
private final WxAccountTagMapper wxAccountTagMapper;
private final WxAccountTagMapper wxAccountTagMapper;
private final WxAccountMapper wxAccountMapper;
private final WxAccountMapper wxAccountMapper;
/**
* 保存粉丝
*
* @param wxMpUser wx mp 用户
* @return boolean
*/
@Override
@Transactional(rollbackFor = Exception.class)
public WxAccountFans saveFans(WxMpUser wxMpUser) {
// 删除历史关注保存的信息
baseMapper.delete(Wrappers.<WxAccountFans>lambdaQuery().eq(WxAccountFans::getOpenid, wxMpUser.getOpenId()));
/**
* 保存粉丝
* @param wxMpUser wx mp 用户
* @return boolean
*/
@Override
@Transactional(rollbackFor = Exception.class)
public WxAccountFans saveFans(WxMpUser wxMpUser) {
// 删除历史关注保存的信息
baseMapper.delete(Wrappers.<WxAccountFans>lambdaQuery().eq(WxAccountFans::getOpenid, wxMpUser.getOpenId()));
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()));
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());
// 随机生成一个昵称,方便平台内部使用
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());
return wxAccountFans;
}
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>lambdaQuery().eq(WxAccount::getAppid, WxMpContextHolder.getAppId()));
wxAccountFans.setWxAccountId(wxAccount.getId());
wxAccountFans.setWxAccountAppid(wxAccount.getAppid());
wxAccountFans.setWxAccountName(wxAccount.getName());
return wxAccountFans;
}
/**
* 分页查询粉丝
*
* @param page 粉丝
* @param wxAccountFans 查询条件
* @return
*/
@SneakyThrows
@Override
public IPage getFansWithTagPage(Page page, WxAccountFans wxAccountFans) {
// 查询当前公众号的标签
List<WxAccountTag> wxAccountTags = wxAccountTagMapper.selectList(Wrappers.emptyWrapper());
/**
* 分页查询粉丝
* @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());
// 翻页查询粉丝
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);
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()))
.toList();
vo.setTagList(tagList);
}
return vo;
}).toList();
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()))
.toList();
vo.setTagList(tagList);
}
return vo;
}).toList();
page.setRecords(voList);
page.setTotal(fansPage.getTotal());
return page;
}
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);
/**
* 更新粉丝数据并同步微信备注、标签;数据库中的 OpenID 和公众号 AppID 覆盖回显身份字段。
* 非空备注才同步至微信,非空标签数组先移除原标签再添加新标签,沿用现有更新行为。
* @param wxAccountFans 待更新粉丝,不能为 null,须包含有效 ID;OpenID 可为脱敏回显值
* @return 记录存在且更新完成返回 true;记录不存在返回 false,不写库也不调用微信
* @throws Exception 数据库或微信 API 调用失败时向上抛出,数据库事务回滚
*/
@Override
@SneakyThrows
@Transactional(rollbackFor = Exception.class)
public Boolean updateFans(WxAccountFans wxAccountFans) {
WxAccountFans stored = baseMapper.selectById(wxAccountFans.getId());
if (stored == null) {
return Boolean.FALSE;
}
// OpenID 回显经过脱敏,持久化与微信操作必须使用数据库中的真实身份。
wxAccountFans.setOpenid(stored.getOpenid());
wxAccountFans.setWxAccountAppid(stored.getWxAccountAppid());
baseMapper.updateById(wxAccountFans);
// 获取操作微信接口类
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(wxAccountFans.getWxAccountAppid());
// 获取操作微信接口类
WxMpService wxMpService = WxMpInitConfigRunner.getMpServices().get(wxAccountFans.getWxAccountAppid());
if (StrUtil.isNotBlank(wxAccountFans.getRemark())) {
WxMpUserService wxMpUserService = wxMpService.getUserService();
wxMpUserService.userUpdateRemark(wxAccountFans.getOpenid(), wxAccountFans.getRemark());
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()});
}
// 更新用户标签
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()) {
// 添加新标签
for (Long tagId : wxAccountFans.getTagIds()) {
userTagService.batchTagging(tagId, new String[]{wxAccountFans.getOpenid()});
}
}
return Boolean.TRUE;
}
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).toList();
blackListService.pullFromBlacklist(collect);
// 更新数据
List<WxAccountFans> fansList = wxAccountFans.stream().peek(item -> {
item.setIsBlack(CommonConstants.SUCCESS);
}).toList();
this.updateBatchById(fansList);
@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).toList();
blackListService.pullFromBlacklist(collect);
// 更新数据
List<WxAccountFans> fansList = wxAccountFans.stream().peek(item -> {
item.setIsBlack(CommonConstants.SUCCESS);
}).toList();
this.updateBatchById(fansList);
return Boolean.TRUE;
}
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).toList();
blackListService.pushToBlacklist(collect);
// 更新数据
List<WxAccountFans> fansList = wxAccountFans.stream().peek(item -> {
item.setIsBlack(CommonConstants.FAIL);
}).toList();
this.updateBatchById(fansList);
@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).toList();
blackListService.pushToBlacklist(collect);
// 更新数据
List<WxAccountFans> fansList = wxAccountFans.stream().peek(item -> {
item.setIsBlack(CommonConstants.FAIL);
}).toList();
this.updateBatchById(fansList);
return Boolean.TRUE;
}
return Boolean.TRUE;
}
/**
* 获取公众号粉丝,生产建议异步
*
* @param appId
* @return
*/
@Async
@Override
public void syncAccountFans(String appId) {
WxAccount wxAccount = wxAccountMapper
.selectOne(Wrappers.<WxAccount>query().lambda().eq(WxAccount::getAppid, appId));
/**
* 获取公众号粉丝,生产建议异步
* @param appId
* @return
*/
@Async
@Override
public void 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));
// 获取操作微信接口类
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());
}
log.info("公众号 {} 粉丝同步完成", wxAccount.getName());
}
/**
* 获取微信用户
*
* @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)
.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 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)
.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())));
/**
* 构建数据库存取对象
* @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()));
}
// 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;
}
// 随机生成一个昵称,方便平台内部使用
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;
}
/**
* 查询当前公众号最后插入的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;
}
}