2026-06-24 f4bd1f3c89d906131495a0aca5aaf82966378510
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package cn.iocoder.yudao.module.mp.service.message;
 
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO;
import cn.iocoder.yudao.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO;
import cn.iocoder.yudao.module.mp.convert.message.MpMessageConvert;
import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.message.MpMessageDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.user.MpUserDO;
import cn.iocoder.yudao.module.mp.dal.mysql.message.MpMessageMapper;
import cn.iocoder.yudao.module.mp.enums.message.MpMessageSendFromEnum;
import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
import cn.iocoder.yudao.module.mp.framework.mp.core.util.MpUtils;
import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
import cn.iocoder.yudao.module.mp.service.material.MpMaterialService;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import cn.iocoder.yudao.module.mp.service.user.MpUserService;
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.api.WxConsts;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import me.chanjar.weixin.mp.bean.result.WxMpUser;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
 
import java.util.concurrent.TimeUnit;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MESSAGE_SEND_FAIL;
 
/**
 * 粉丝消息 Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class MpMessageServiceImpl implements MpMessageService {
 
    @Resource
    @Lazy // 延迟加载,避免循环依赖
    private MpAccountService mpAccountService;
    @Resource
    private MpUserService mpUserService;
    @Resource
    private MpMaterialService mpMaterialService;
 
    @Resource
    private MpMessageMapper mpMessageMapper;
 
    @Resource
    @Lazy // 延迟加载,解决循环依赖的问题
    private MpServiceFactory mpServiceFactory;
 
    @Resource
    private Validator validator;
 
    @Override
    public PageResult<MpMessageDO> getMessagePage(MpMessagePageReqVO pageReqVO) {
        return mpMessageMapper.selectPage(pageReqVO);
    }
 
    @Override
    @SneakyThrows
    public void receiveMessage(WxMpService weixinService, String appId, WxMpXmlMessage wxMessage) {
        // 获得关联信息
        MpAccountDO account = mpAccountService.getAccountFromCache(appId);
        Assert.notNull(account, "公众号账号({}) 不存在", appId);
 
        // 获取用户
        MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser());
        if (user == null) {
            // 特殊情况:因为 receiveMessage 是异步记录,可能 SubscribeHandler 还没存储好 User,此时 sleep 轮询
            for (int i = 0; i < 3; i++) {
                ThreadUtil.sleep(5, TimeUnit.SECONDS);
                user = mpUserService.getUser(appId, wxMessage.getFromUser());
                if (user != null) {
                    break;
                }
                log.warn("[receiveMessage][粉丝({}/{}) 不存在,第 {} 次重试失败]", appId, wxMessage.getFromUser(), i + 1);
            }
        }
        // 特殊情况:可能 SubscribeHandler 没处理正确(例如说发生异常),则主动创建
        if (user == null) {
            log.warn("[receiveMessage][粉丝({}/{}) 不存在,主动创建]", appId, wxMessage.getFromUser());
            WxMpUser wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser());
            user = mpUserService.saveUser(appId, wxMpUser);
        }
        Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser());
 
        // 记录消息
        MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
                .setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom());
        downloadMessageMedia(message);
        mpMessageMapper.insert(message);
    }
 
    @Override
    public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) {
        // 校验消息格式
        MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO);
 
        // 获得关联信息
        MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId());
        Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId());
        MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid());
        Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid());
 
        // 记录消息
        MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user).
                setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
        downloadMessageMedia(message);
        mpMessageMapper.insert(message);
 
        // 转换返回 WxMpXmlOutMessage 对象
        return MpMessageConvert.INSTANCE.convert02(message, account);
    }
 
    @Override
    public MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO) {
        // 校验消息格式
        MpUtils.validateMessage(validator, sendReqVO.getType(), sendReqVO);
 
        // 获得关联信息
        MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId());
        MpAccountDO account = mpAccountService.getRequiredAccount(user.getAccountId());
 
        // 发送客服消息
        WxMpKefuMessage wxMessage = MpMessageConvert.INSTANCE.convert(sendReqVO, user);
        WxMpService mpService = mpServiceFactory.getRequiredMpService(user.getAppId());
        try {
            mpService.getKefuService().sendKefuMessageWithResponse(wxMessage);
        } catch (WxErrorException e) {
            throw exception(MESSAGE_SEND_FAIL, e.getError().getErrorMsg());
        }
 
        // 记录消息
        MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user)
                .setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom());
        downloadMessageMedia(message);
        mpMessageMapper.insert(message);
        return message;
    }
 
    /**
     * 下载消息使用到的媒体文件,并上传到文件服务
     *
     * @param message 消息
     */
    private void downloadMessageMedia(MpMessageDO message) {
        if (StrUtil.isNotEmpty(message.getMediaId())) {
            message.setMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(),
                    message.getMediaId(), MpUtils.getMediaFileType(message.getType())));
        }
        if (StrUtil.isNotEmpty(message.getThumbMediaId())) {
            message.setThumbMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(),
                    message.getThumbMediaId(), WxConsts.MediaFileType.THUMB));
        }
    }
 
}