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
package cn.iocoder.yudao.module.mp.service.menu;
 
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO;
import cn.iocoder.yudao.module.mp.convert.menu.MpMenuConvert;
import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
import cn.iocoder.yudao.module.mp.dal.dataobject.menu.MpMenuDO;
import cn.iocoder.yudao.module.mp.dal.mysql.menu.MpMenuMapper;
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.message.MpMessageService;
import cn.iocoder.yudao.module.mp.service.message.bo.MpMessageSendOutReqBO;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
 
import jakarta.annotation.Resource;
import jakarta.validation.Validator;
import java.util.List;
 
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MENU_DELETE_FAIL;
import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.MENU_SAVE_FAIL;
 
/**
 * 公众号菜单 Service 实现类
 *
 * @author 芋道源码
 */
@Service
@Validated
@Slf4j
public class MpMenuServiceImpl implements MpMenuService {
 
    @Resource
    private MpMessageService mpMessageService;
    @Resource
    @Lazy // 延迟加载,避免循环引用报错
    private MpAccountService mpAccountService;
 
    @Resource
    @Lazy // 延迟加载,避免循环引用报错
    private MpServiceFactory mpServiceFactory;
 
    @Resource
    private Validator validator;
 
    @Resource
    private MpMenuMapper mpMenuMapper;
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveMenu(MpMenuSaveReqVO createReqVO) {
        MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId());
        WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId());
 
        // 参数校验
        createReqVO.getMenus().forEach(this::validateMenu);
 
        // 第一步,同步公众号
        WxMenu wxMenu = new WxMenu();
        wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus()));
        try {
            mpService.getMenuService().menuCreate(wxMenu);
        } catch (WxErrorException e) {
            throw exception(MENU_SAVE_FAIL, e.getError().getErrorMsg());
        }
 
        // 第二步,存储到数据库
        mpMenuMapper.deleteByAccountId(createReqVO.getAccountId());
        createReqVO.getMenus().forEach(menu -> {
            // 先保存顶级菜单
            MpMenuDO menuDO = createMenu(menu, null, account);
            // 再保存子菜单
            if (CollUtil.isEmpty(menu.getChildren())) {
                return;
            }
            menu.getChildren().forEach(childMenu -> createMenu(childMenu, menuDO, account));
        });
    }
 
    /**
     * 校验菜单的格式是否正确
     *
     * @param menu 菜单
     */
    private void validateMenu(MpMenuSaveReqVO.Menu menu) {
        MpUtils.validateButton(validator, menu.getType(), menu.getReplyMessageType(), menu);
        // 子菜单
        if (CollUtil.isEmpty(menu.getChildren())) {
            return;
        }
        menu.getChildren().forEach(this::validateMenu);
    }
 
    /**
     * 创建菜单,并存储到数据库
     *
     * @param wxMenu 菜单信息
     * @param parentMenu 父菜单
     * @param account 公众号账号
     * @return 创建后的菜单
     */
    private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) {
        // 创建菜单
        MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren())
                ? new MpMenuDO().setName(wxMenu.getName())
                : MpMenuConvert.INSTANCE.convert02(wxMenu);
        // 设置菜单的公众号账号信息
        if (account != null) {
            menu.setAccountId(account.getId()).setAppId(account.getAppId());
        }
        // 设置父编号
        if (parentMenu != null) {
            menu.setParentId(parentMenu.getId());
        } else {
            menu.setParentId(MpMenuDO.ID_ROOT);
        }
 
        // 插入到数据库
        mpMenuMapper.insert(menu);
        return menu;
    }
 
    @Override
    public void deleteMenuByAccountId(Long accountId) {
        WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId);
        // 第一步,同步公众号
        try {
            mpService.getMenuService().menuDelete();
        } catch (WxErrorException e) {
            throw exception(MENU_DELETE_FAIL, e.getError().getErrorMsg());
        }
 
        // 第二步,存储到数据库
        mpMenuMapper.deleteByAccountId(accountId);
    }
 
    @Override
    public WxMpXmlOutMessage reply(String appId, String key, String openid) {
        // 第一步,获得菜单
        MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key);
        if (menu == null) {
            log.error("[reply][appId({}) key({}) 找不到对应的菜单]", appId, key);
            return null;
        }
        // 按钮必须要有消息类型,不然后续无法回复消息
        if (StrUtil.isEmpty(menu.getReplyMessageType())) {
            log.error("[reply][menu({}) 不存在对应的消息类型]", menu);
            return null;
        }
 
        // 第二步,回复消息
        MpMessageSendOutReqBO sendReqBO = MpMenuConvert.INSTANCE.convert(openid, menu);
        return mpMessageService.sendOutMessage(sendReqBO);
    }
 
    @Override
    public List<MpMenuDO> getMenuListByAccountId(Long accountId) {
        return mpMenuMapper.selectListByAccountId(accountId);
    }
 
}