Commit 39b2afd506f177c7410e63f0af3df4980d22340c

Authored by YunaiV
1 parent 71beeabe

mp:完成的公众号的管理界面

Showing 16 changed files with 272 additions and 184 deletions
yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java
... ... @@ -9,9 +9,12 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
9 9 */
10 10 public interface ErrorCodeConstants {
11 11  
12   - // ========== 用户相关 1004001000============
13   - ErrorCode WX_ACCOUNT_NOT_EXISTS = new ErrorCode(1006001000, "公众号账户不存在");
14   - ErrorCode WX_ACCOUNT_FANS_NOT_EXISTS = new ErrorCode(1006001001, "粉丝账号不存在");
  12 + // ========== 公众号账号 1006000000============
  13 + ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1006000000, "公众号账号不存在");
  14 + ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1006000001, "生成公众号二维码失败,原因:{}");
  15 + ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1006000001, "清空公众号的 API 配额失败,原因:{}");
  16 +
  17 + // TODO 要处理下
15 18 ErrorCode COMMON_NOT_EXISTS = new ErrorCode(1006001002, "用户不存在");
16 19  
17 20 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java
... ... @@ -2,7 +2,10 @@ package cn.iocoder.yudao.module.mp.controller.admin.account;
2 2  
3 3 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 4 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5   -import cn.iocoder.yudao.module.mp.controller.admin.account.vo.*;
  5 +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReqVO;
  6 +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
  7 +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountRespVO;
  8 +import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO;
6 9 import cn.iocoder.yudao.module.mp.convert.account.MpAccountConvert;
7 10 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 11 import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
... ... @@ -18,54 +21,72 @@ import javax.validation.Valid;
18 21  
19 22 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
20 23  
21   -@Api(tags = "管理后台 - 公众号账户")
  24 +@Api(tags = "管理后台 - 公众号账号")
22 25 @RestController
23 26 @RequestMapping("/mp/account")
24 27 @Validated
25 28 public class MpAccountController {
26 29  
27 30 @Resource
28   - private MpAccountService wxAccountService;
  31 + private MpAccountService mpAccountService;
29 32  
30 33 @PostMapping("/create")
31   - @ApiOperation("创建公众号账户")
  34 + @ApiOperation("创建公众号账号")
32 35 @PreAuthorize("@ss.hasPermission('mp:account:create')")
33 36 public CommonResult<Long> createWxAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) {
34   - return success(wxAccountService.createAccount(createReqVO));
  37 + return success(mpAccountService.createAccount(createReqVO));
35 38 }
36 39  
37 40 @PutMapping("/update")
38   - @ApiOperation("更新公众号账户")
  41 + @ApiOperation("更新公众号账号")
39 42 @PreAuthorize("@ss.hasPermission('mp:account:update')")
40 43 public CommonResult<Boolean> updateWxAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) {
41   - wxAccountService.updateAccount(updateReqVO);
  44 + mpAccountService.updateAccount(updateReqVO);
42 45 return success(true);
43 46 }
44 47  
45 48 @DeleteMapping("/delete")
46   - @ApiOperation("删除公众号账户")
  49 + @ApiOperation("删除公众号账号")
47 50 @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
48 51 @PreAuthorize("@ss.hasPermission('mp:account:delete')")
49 52 public CommonResult<Boolean> deleteWxAccount(@RequestParam("id") Long id) {
50   - wxAccountService.deleteAccount(id);
  53 + mpAccountService.deleteAccount(id);
51 54 return success(true);
52 55 }
53 56  
54 57 @GetMapping("/get")
55   - @ApiOperation("获得公众号账户")
  58 + @ApiOperation("获得公众号账号")
56 59 @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class)
57 60 @PreAuthorize("@ss.hasPermission('mp:account:query')")
58 61 public CommonResult<MpAccountRespVO> getWxAccount(@RequestParam("id") Long id) {
59   - MpAccountDO wxAccount = wxAccountService.getAccount(id);
  62 + MpAccountDO wxAccount = mpAccountService.getAccount(id);
60 63 return success(MpAccountConvert.INSTANCE.convert(wxAccount));
61 64 }
62 65  
63 66 @GetMapping("/page")
64   - @ApiOperation("获得公众号账户分页")
  67 + @ApiOperation("获得公众号账号分页")
65 68 @PreAuthorize("@ss.hasPermission('mp:account:query')")
66 69 public CommonResult<PageResult<MpAccountRespVO>> getWxAccountPage(@Valid MpAccountPageReqVO pageVO) {
67   - PageResult<MpAccountDO> pageResult = wxAccountService.getAccountPage(pageVO);
  70 + PageResult<MpAccountDO> pageResult = mpAccountService.getAccountPage(pageVO);
68 71 return success(MpAccountConvert.INSTANCE.convertPage(pageResult));
69 72 }
70 73  
  74 + @PutMapping("/generate-qr-code")
  75 + @ApiOperation("生成公众号二维码")
  76 + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
  77 + @PreAuthorize("@ss.hasPermission('mp:account:qr-code')")
  78 + public CommonResult<Boolean> generateAccountQrCode(@RequestParam("id") Long id) {
  79 + mpAccountService.generateAccountQrCode(id);
  80 + return success(true);
  81 + }
  82 +
  83 + @PutMapping("/clear-quota")
  84 + @ApiOperation("清空公众号 API 配额")
  85 + @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class)
  86 + @PreAuthorize("@ss.hasPermission('mp:account:clear-quota')")
  87 + public CommonResult<Boolean> clearAccountQuota(@RequestParam("id") Long id) {
  88 + mpAccountService.clearAccountQuota(id);
  89 + return success(true);
  90 + }
  91 +
71 92 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountBaseVO.java
... ... @@ -3,10 +3,10 @@ package cn.iocoder.yudao.module.mp.controller.admin.account.vo;
3 3 import io.swagger.annotations.ApiModelProperty;
4 4 import lombok.Data;
5 5  
6   -import javax.validation.constraints.NotNull;
  6 +import javax.validation.constraints.NotEmpty;
7 7  
8 8 /**
9   - * 公众号账户 Base VO,提供给添加、修改、详细的子 VO 使用
  9 + * 公众号账号 Base VO,提供给添加、修改、详细的子 VO 使用
10 10 * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
11 11 *
12 12 * @author fengdan
... ... @@ -14,29 +14,30 @@ import javax.validation.constraints.NotNull;
14 14 @Data
15 15 public class MpAccountBaseVO {
16 16  
17   - @ApiModelProperty(value = "公众号名称", required = true)
18   - @NotNull(message = "公众号名称不能为空")
  17 + @ApiModelProperty(value = "公众号名称", required = true, example = "芋道源码")
  18 + @NotEmpty(message = "公众号名称不能为空")
19 19 private String name;
20 20  
21   - @ApiModelProperty(value = "公众号账户", required = true)
22   - @NotNull(message = "公众号账户不能为空")
  21 + @ApiModelProperty(value = "公众号微信号", required = true, example = "yudaoyuanma")
  22 + @NotEmpty(message = "公众号微信号不能为空")
23 23 private String account;
24 24  
25 25 @ApiModelProperty(value = "公众号 appId", required = true, example = "wx5b23ba7a5589ecbb")
26   - @NotNull(message = "公众号 appId 不能为空")
  26 + @NotEmpty(message = "公众号 appId 不能为空")
27 27 private String appId;
28 28  
29   - @ApiModelProperty(value = "公众号密钥", required = true)
30   - @NotNull(message = "公众号密钥不能为空")
  29 + @ApiModelProperty(value = "公众号密钥", required = true, example = "3a7b3b20c537e52e74afd395eb85f61f")
  30 + @NotEmpty(message = "公众号密钥不能为空")
31 31 private String appSecret;
32 32  
33   - @ApiModelProperty(value = "公众号 token", required = true)
  33 + @ApiModelProperty(value = "公众号 token", required = true, example = "kangdayuzhen")
  34 + @NotEmpty(message = "公众号 token 不能为空")
34 35 private String token;
35 36  
36   - @ApiModelProperty(value = "加密密钥")
  37 + @ApiModelProperty(value = "加密密钥", example = "gjN+Ksei")
37 38 private String aesKey;
38 39  
39   - @ApiModelProperty(value = "备注")
  40 + @ApiModelProperty(value = "备注", example = "请关注芋道源码,学习技术")
40 41 private String remark;
41 42  
42 43 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java
... ... @@ -5,7 +5,7 @@ import lombok.Data;
5 5 import lombok.EqualsAndHashCode;
6 6 import lombok.ToString;
7 7  
8   -@ApiModel("管理后台 - 公众号账户创建 Request VO")
  8 +@ApiModel("管理后台 - 公众号账号创建 Request VO")
9 9 @Data
10 10 @EqualsAndHashCode(callSuper = true)
11 11 @ToString(callSuper = true)
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java
... ... @@ -4,7 +4,7 @@ import lombok.*;
4 4 import io.swagger.annotations.*;
5 5 import cn.iocoder.yudao.framework.common.pojo.PageParam;
6 6  
7   -@ApiModel("管理后台 - 公众号账户分页 Request VO")
  7 +@ApiModel("管理后台 - 公众号账号分页 Request VO")
8 8 @Data
9 9 @EqualsAndHashCode(callSuper = true)
10 10 @ToString(callSuper = true)
... ... @@ -13,7 +13,7 @@ public class MpAccountPageReqVO extends PageParam {
13 13 @ApiModelProperty(value = "公众号名称", notes = "模糊匹配")
14 14 private String name;
15 15  
16   - @ApiModelProperty(value = "公众号账户", notes = "模糊匹配")
  16 + @ApiModelProperty(value = "公众号账号", notes = "模糊匹配")
17 17 private String account;
18 18  
19 19 @ApiModelProperty(value = "公众号 appid", notes = "模糊匹配")
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountRespVO.java
... ... @@ -8,25 +8,19 @@ import lombok.ToString;
8 8  
9 9 import java.util.Date;
10 10  
11   -/**
12   - * @author fengdan
13   - */
14   -@ApiModel("管理后台 - 公众号账户 Response VO")
  11 +@ApiModel("管理后台 - 公众号账号 Response VO")
15 12 @Data
16 13 @EqualsAndHashCode(callSuper = true)
17 14 @ToString(callSuper = true)
18 15 public class MpAccountRespVO extends MpAccountBaseVO {
19 16  
20   - @ApiModelProperty(value = "编号", required = true)
  17 + @ApiModelProperty(value = "编号", required = true, example = "1024")
21 18 private Long id;
22 19  
23   - @ApiModelProperty(value = "二维码图片URL")
  20 + @ApiModelProperty(value = "二维码图片URL", example = "https://www.iocoder.cn/1024.png")
24 21 private String qrCodeUrl;
25 22  
26 23 @ApiModelProperty(value = "创建时间", required = true)
27 24 private Date createTime;
28 25  
29   - @ApiModelProperty(value = "公众号密钥", required = true)
30   - private String appSecret;
31   -
32 26 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java
... ... @@ -7,7 +7,7 @@ import javax.validation.constraints.*;
7 7 /**
8 8 * @author fengdan
9 9 */
10   -@ApiModel("管理后台 - 公众号账户更新 Request VO")
  10 +@ApiModel("管理后台 - 公众号账号更新 Request VO")
11 11 @Data
12 12 @EqualsAndHashCode(callSuper = true)
13 13 @ToString(callSuper = true)
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/dataobject/account/MpAccountDO.java
1 1 package cn.iocoder.yudao.module.mp.dal.dataobject.account;
2 2  
3   -import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
4 3 import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
5 4 import com.baomidou.mybatisplus.annotation.KeySequence;
6 5 import com.baomidou.mybatisplus.annotation.TableId;
7 6 import com.baomidou.mybatisplus.annotation.TableName;
8   -import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties;
9 7 import lombok.*;
10   -import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
11   -import me.chanjar.weixin.mp.config.WxMpConfigStorage;
12   -import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl;
13 8  
14 9 /**
15   - * 公众号账户 DO
  10 + * 公众号账号 DO
16 11 *
17 12 * @author 芋道源码
18 13 */
... ... @@ -36,7 +31,7 @@ public class MpAccountDO extends TenantBaseDO {
36 31 */
37 32 private String name;
38 33 /**
39   - * 公众号账户
  34 + * 公众号账号
40 35 */
41 36 private String account;
42 37 /**
... ... @@ -52,7 +47,7 @@ public class MpAccountDO extends TenantBaseDO {
52 47 */
53 48 private String token;
54 49 /**
55   - * 加密密钥
  50 + * 消息加解密密钥
56 51 */
57 52 private String aesKey;
58 53 /**
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/dal/mysql/account/MpAccountMapper.java
... ... @@ -6,9 +6,6 @@ import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
6 6 import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
7 7 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8 8 import org.apache.ibatis.annotations.Mapper;
9   -import org.apache.ibatis.annotations.Select;
10   -
11   -import java.time.LocalDateTime;
12 9  
13 10 @Mapper
14 11 public interface MpAccountMapper extends BaseMapperX<MpAccountDO> {
... ... @@ -21,7 +18,8 @@ public interface MpAccountMapper extends BaseMapperX&lt;MpAccountDO&gt; {
21 18 .orderByDesc(MpAccountDO::getId));
22 19 }
23 20  
24   - @Select("SELECT COUNT(*) FROM mp_account WHERE update_time > #{maxUpdateTime}")
25   - Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime);
  21 + default MpAccountDO selectByAppId(String appId) {
  22 + return selectOne(MpAccountDO::getAppId, appId);
  23 + }
26 24  
27 25 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/mq/consumer/MpConfigRefreshConsumer.java renamed to yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/mq/consumer/MpAccountRefreshConsumer.java
1 1 package cn.iocoder.yudao.module.mp.mq.consumer;
2 2  
3 3 import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
4   -import cn.iocoder.yudao.module.mp.mq.message.MpConfigRefreshMessage;
  4 +import cn.iocoder.yudao.module.mp.mq.message.MpAccountRefreshMessage;
5 5 import cn.iocoder.yudao.module.mp.service.account.MpAccountService;
6 6 import lombok.extern.slf4j.Slf4j;
7 7 import org.springframework.stereotype.Component;
... ... @@ -9,21 +9,21 @@ import org.springframework.stereotype.Component;
9 9 import javax.annotation.Resource;
10 10  
11 11 /**
12   - * 针对 {@link MpConfigRefreshMessage} 的消费者
  12 + * 针对 {@link MpAccountRefreshMessage} 的消费者
13 13 *
14   - * @author lyz
  14 + * @author 芋道源码
15 15 */
16 16 @Component
17 17 @Slf4j
18   -public class MpConfigRefreshConsumer extends AbstractChannelMessageListener<MpConfigRefreshMessage> {
  18 +public class MpAccountRefreshConsumer extends AbstractChannelMessageListener<MpAccountRefreshMessage> {
19 19  
20 20 @Resource
21   - private MpAccountService wxAccountService;
  21 + private MpAccountService mpAccountService;
22 22  
23 23 @Override
24   - public void onMessage(MpConfigRefreshMessage message) {
25   - log.info("[onMessage][收到 MpConfig 刷新消息]");
26   - wxAccountService.initLocalCache();
  24 + public void onMessage(MpAccountRefreshMessage message) {
  25 + log.info("[onMessage][收到 Account 刷新消息]");
  26 + mpAccountService.initLocalCache();
27 27 }
28 28  
29 29 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/mq/message/MpConfigRefreshMessage.java renamed to yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/mq/message/MpAccountRefreshMessage.java
... ... @@ -5,15 +5,17 @@ import lombok.Data;
5 5 import lombok.EqualsAndHashCode;
6 6  
7 7 /**
8   - * 微信配置数据刷新 Message
  8 + * 公众号账号刷新 Message
  9 + *
  10 + * @author 芋道源码
9 11 */
10 12 @Data
11 13 @EqualsAndHashCode(callSuper = true)
12   -public class MpConfigRefreshMessage extends AbstractChannelMessage {
  14 +public class MpAccountRefreshMessage extends AbstractChannelMessage {
13 15  
14 16 @Override
15 17 public String getChannel() {
16   - return "mp.config-data.refresh";
  18 + return "mp.account.refresh";
17 19 }
18 20  
19 21 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/mq/producer/MpConfigProducer.java renamed to yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/mq/producer/MpAccountProducer.java
1 1 package cn.iocoder.yudao.module.mp.mq.producer;
2 2  
3 3 import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
4   -import cn.iocoder.yudao.module.mp.mq.message.MpConfigRefreshMessage;
  4 +import cn.iocoder.yudao.module.mp.mq.message.MpAccountRefreshMessage;
5 5 import org.springframework.stereotype.Component;
6 6  
7 7 import javax.annotation.Resource;
8 8  
9 9 /**
10   - * 微信配置数据相关消息的 Producer
  10 + * 公众号账号 Producer
  11 + *
  12 + * @author 芋道源码
11 13 */
12 14 @Component
13   -public class MpConfigProducer {
  15 +public class MpAccountProducer {
14 16  
15 17 @Resource
16 18 private RedisMQTemplate redisMQTemplate;
17 19  
18 20 /**
19   - * 发送 {@link MpConfigRefreshMessage} 消息
  21 + * 发送 {@link MpAccountRefreshMessage} 消息
20 22 */
21   - public void sendDictDataRefreshMessage() {
22   - MpConfigRefreshMessage message = new MpConfigRefreshMessage();
  23 + public void sendAccountRefreshMessage() {
  24 + MpAccountRefreshMessage message = new MpAccountRefreshMessage();
23 25 redisMQTemplate.send(message);
24 26 }
25 27  
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountService.java
... ... @@ -5,12 +5,11 @@ import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountCreateReq
5 5 import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountPageReqVO;
6 6 import cn.iocoder.yudao.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO;
7 7 import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
8   -import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
9 8  
10 9 import javax.validation.Valid;
11 10  
12 11 /**
13   - * 公众号账户 Service 接口
  12 + * 公众号账号 Service 接口
14 13 *
15 14 * @author 芋道源码
16 15 */
... ... @@ -22,7 +21,7 @@ public interface MpAccountService {
22 21 void initLocalCache();
23 22  
24 23 /**
25   - * 创建公众号账户
  24 + * 创建公众号账号
26 25 *
27 26 * @param createReqVO 创建信息
28 27 * @return 编号
... ... @@ -30,51 +29,57 @@ public interface MpAccountService {
30 29 Long createAccount(@Valid MpAccountCreateReqVO createReqVO);
31 30  
32 31 /**
33   - * 更新公众号账户
  32 + * 更新公众号账号
34 33 *
35 34 * @param updateReqVO 更新信息
36 35 */
37 36 void updateAccount(@Valid MpAccountUpdateReqVO updateReqVO);
38 37  
39 38 /**
40   - * 删除公众号账户
  39 + * 删除公众号账号
41 40 *
42 41 * @param id 编号
43 42 */
44 43 void deleteAccount(Long id);
45 44  
46 45 /**
47   - * 获得公众号账户
  46 + * 获得公众号账号
48 47 *
49 48 * @param id 编号
50   - * @return 公众号账户
  49 + * @return 公众号账号
51 50 */
52 51 MpAccountDO getAccount(Long id);
53 52  
54 53 /**
55   - * 从缓存中,获得公众号账户
  54 + * 从缓存中,获得公众号账号
56 55 *
57 56 * @param appId 微信公众号 appId
58   - * @return 公众号账户
  57 + * @return 公众号账号
59 58 */
60 59 MpAccountDO getAccountFromCache(String appId);
61 60  
62 61 /**
63   - * 获得公众号账户分页
  62 + * 获得公众号账号分页
64 63 *
65 64 * @param pageReqVO 分页查询
66   - * @return 公众号账户分页
  65 + * @return 公众号账号分页
67 66 */
68 67 PageResult<MpAccountDO> getAccountPage(MpAccountPageReqVO pageReqVO);
69 68  
70   - // TODO 芋艿:去除这样的方法
71 69 /**
72   - * 查询账户
  70 + * 生成公众号账号的二维码
73 71 *
74   - * @param field
75   - * @param val
76   - * @return
  72 + * @param id 编号
  73 + */
  74 + void generateAccountQrCode(Long id);
  75 +
  76 + /**
  77 + * 清空公众号账号的 API 配额
  78 + *
  79 + * 参考文档:<a href="https://developers.weixin.qq.com/doc/offiaccount/Message_Management/API_Call_Limits.html">接口调用频次限制说明</a>
  80 + *
  81 + * @param id 编号
77 82 */
78   - MpAccountDO findBy(SFunction<MpAccountDO, ?> field, Object val);
  83 + void clearAccountQuota(Long id);
79 84  
80 85 }
... ...
yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/account/MpAccountServiceImpl.java
1 1 package cn.iocoder.yudao.module.mp.service.account;
2 2  
  3 +import cn.hutool.core.util.ObjUtil;
3 4 import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
4 5 import cn.iocoder.yudao.framework.common.pojo.PageResult;
5 6 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
... ... @@ -12,24 +13,27 @@ import cn.iocoder.yudao.module.mp.dal.dataobject.account.MpAccountDO;
12 13 import cn.iocoder.yudao.module.mp.dal.mysql.account.MpAccountMapper;
13 14 import cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants;
14 15 import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory;
15   -import cn.iocoder.yudao.module.mp.mq.producer.MpConfigProducer;
16   -import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
  16 +import cn.iocoder.yudao.module.mp.mq.producer.MpAccountProducer;
  17 +import com.google.common.annotations.VisibleForTesting;
17 18 import lombok.Getter;
18 19 import lombok.extern.slf4j.Slf4j;
  20 +import me.chanjar.weixin.common.error.WxErrorException;
  21 +import me.chanjar.weixin.mp.api.WxMpService;
  22 +import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
19 23 import org.springframework.context.annotation.Lazy;
20   -import org.springframework.scheduling.annotation.Scheduled;
21 24 import org.springframework.stereotype.Service;
22 25 import org.springframework.validation.annotation.Validated;
23 26  
24 27 import javax.annotation.PostConstruct;
25 28 import javax.annotation.Resource;
26   -import java.time.LocalDateTime;
27 29 import java.util.List;
28 30 import java.util.Map;
29 31  
  32 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
  33 +import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS;
30 34  
31 35 /**
32   - * 公众号账户 Service 实现类
  36 + * 公众号账号 Service 实现类
33 37 *
34 38 * @author fengdan
35 39 */
... ... @@ -39,12 +43,6 @@ import java.util.Map;
39 43 public class MpAccountServiceImpl implements MpAccountService {
40 44  
41 45 /**
42   - * 定时执行 {@link #schedulePeriodicRefresh()} 的周期
43   - * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高
44   - */
45   - private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L;
46   -
47   - /**
48 46 * 账号缓存
49 47 * key:账号编号 {@link MpAccountDO#getAppId()}
50 48 *
... ... @@ -52,11 +50,6 @@ public class MpAccountServiceImpl implements MpAccountService {
52 50 */
53 51 @Getter
54 52 private volatile Map<String, MpAccountDO> accountCache;
55   - /**
56   - * 缓存菜单的最大更新时间,用于后续的增量轮询,判断是否有更新
57   - */
58   - @Getter
59   - private volatile LocalDateTime maxUpdateTime;
60 53  
61 54 @Resource
62 55 private MpAccountMapper mpAccountMapper;
... ... @@ -66,89 +59,85 @@ public class MpAccountServiceImpl implements MpAccountService {
66 59 private MpServiceFactory mpServiceFactory;
67 60  
68 61 @Resource
69   - private MpConfigProducer mpConfigDataProducer;
  62 + private MpAccountProducer mpAccountProducer;
70 63  
71 64 @Override
72 65 @PostConstruct
73 66 public void initLocalCache() {
74   - initLocalCacheIfUpdate(null);
75   - }
76   -
77   - @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD)
78   - public void schedulePeriodicRefresh() {
79   - initLocalCacheIfUpdate(this.maxUpdateTime);
80   - }
81   -
82   - /**
83   - * 刷新本地缓存
84   - *
85   - * @param maxUpdateTime 最大更新时间
86   - * 1. 如果 maxUpdateTime 为 null,则“强制”刷新缓存
87   - * 2. 如果 maxUpdateTime 不为 null,判断自 maxUpdateTime 是否有数据发生变化,有的情况下才刷新缓存
88   - */
89   - private void initLocalCacheIfUpdate(LocalDateTime maxUpdateTime) {
90 67 // 注意:忽略自动多租户,因为要全局初始化缓存
91 68 TenantUtils.executeIgnore(() -> {
92   - // 第一步:基于 maxUpdateTime 判断缓存是否刷新。
93   - // 如果没有增量的数据变化,则不进行本地缓存的刷新
94   - if (maxUpdateTime != null
95   - && mpAccountMapper.selectCountByUpdateTimeGt(maxUpdateTime) == 0) {
96   - log.info("[initLocalCacheIfUpdate][数据未发生变化({}),本地缓存不刷新]", maxUpdateTime);
97   - return;
98   - }
  69 + // 第一步:查询数据
99 70 List<MpAccountDO> accounts = mpAccountMapper.selectList();
100 71 log.info("[initLocalCacheIfUpdate][缓存公众号账号,数量为:{}]", accounts.size());
101 72  
102 73 // 第二步:构建缓存。创建或更新支付 Client
103 74 mpServiceFactory.init(accounts);
104 75 accountCache = CollectionUtils.convertMap(accounts, MpAccountDO::getAppId);
105   -
106   - // 第三步:设置最新的 maxUpdateTime,用于下次的增量判断。
107   - this.maxUpdateTime = CollectionUtils.getMaxValue(accounts, MpAccountDO::getUpdateTime);
108 76 });
109 77 }
110 78  
111 79 @Override
112 80 public Long createAccount(MpAccountCreateReqVO createReqVO) {
113   - // TODO 芋艿:校验唯一性
  81 + // 校验 appId 唯一
  82 + validateAppIdUnique(null, createReqVO.getAppId());
  83 +
114 84 // 插入
115   - MpAccountDO wxAccount = MpAccountConvert.INSTANCE.convert(createReqVO);
116   - mpAccountMapper.insert(wxAccount);
  85 + MpAccountDO account = MpAccountConvert.INSTANCE.convert(createReqVO);
  86 + mpAccountMapper.insert(account);
117 87  
118   - // TODO 芋艿:刷新的方式
119   - mpConfigDataProducer.sendDictDataRefreshMessage();
120   - // 返回
121   - return wxAccount.getId();
  88 + // 发送刷新消息
  89 + mpAccountProducer.sendAccountRefreshMessage();
  90 + return account.getId();
122 91 }
123 92  
124 93 @Override
125 94 public void updateAccount(MpAccountUpdateReqVO updateReqVO) {
126   - // TODO 芋艿:校验唯一性
127 95 // 校验存在
128   - validateWxAccountExists(updateReqVO.getId());
  96 + validateAccountExists(updateReqVO.getId());
  97 + // 校验 appId 唯一
  98 + validateAppIdUnique(updateReqVO.getId(), updateReqVO.getAppId());
  99 +
129 100 // 更新
130 101 MpAccountDO updateObj = MpAccountConvert.INSTANCE.convert(updateReqVO);
131 102 mpAccountMapper.updateById(updateObj);
132 103  
133   - // TODO 芋艿:刷新的方式
134   - mpConfigDataProducer.sendDictDataRefreshMessage();
  104 + // 发送刷新消息
  105 + mpAccountProducer.sendAccountRefreshMessage();
135 106 }
136 107  
137 108 @Override
138 109 public void deleteAccount(Long id) {
139 110 // 校验存在
140   - validateWxAccountExists(id);
  111 + validateAccountExists(id);
141 112 // 删除
142 113 mpAccountMapper.deleteById(id);
143 114  
144   - // TODO 芋艿:刷新的方式
145   - mpConfigDataProducer.sendDictDataRefreshMessage();
  115 + // 发送刷新消息
  116 + mpAccountProducer.sendAccountRefreshMessage();
146 117 }
147 118  
148   - private void validateWxAccountExists(Long id) {
149   - if (mpAccountMapper.selectById(id) == null) {
150   - throw ServiceExceptionUtil.exception(ErrorCodeConstants.WX_ACCOUNT_NOT_EXISTS);
  119 + private MpAccountDO validateAccountExists(Long id) {
  120 + MpAccountDO account = mpAccountMapper.selectById(id);
  121 + if (account == null) {
  122 + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ACCOUNT_NOT_EXISTS);
151 123 }
  124 + return account;
  125 + }
  126 +
  127 + @VisibleForTesting
  128 + public void validateAppIdUnique(Long id, String appId) {
  129 + // 多个租户,appId 是不能重复,否则公众号回调会无法识别
  130 + TenantUtils.executeIgnore(() -> {
  131 + MpAccountDO account = mpAccountMapper.selectByAppId(appId);
  132 + if (account == null) {
  133 + return;
  134 + }
  135 + // 存在 account 记录的情况下
  136 + if (id == null // 新增时,说明重复
  137 + || ObjUtil.notEqual(id, account.getId())) { // 更新时,如果 id 不一致,说明重复
  138 + throw exception(USER_USERNAME_EXISTS);
  139 + }
  140 + });
152 141 }
153 142  
154 143 @Override
... ... @@ -167,8 +156,36 @@ public class MpAccountServiceImpl implements MpAccountService {
167 156 }
168 157  
169 158 @Override
170   - public MpAccountDO findBy(SFunction<MpAccountDO, ?> field, Object val) {
171   - return mpAccountMapper.selectOne(field, val);
  159 + public void generateAccountQrCode(Long id) {
  160 + // 校验存在
  161 + MpAccountDO account = validateAccountExists(id);
  162 +
  163 + // 生成二维码
  164 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId());
  165 + String qrCodeUrl;
  166 + try {
  167 + WxMpQrCodeTicket qrCodeTicket = mpService.getQrcodeService().qrCodeCreateLastTicket("default");
  168 + qrCodeUrl = mpService.getQrcodeService().qrCodePictureUrl(qrCodeTicket.getTicket());
  169 + } catch (WxErrorException e) {
  170 + throw exception(ErrorCodeConstants.ACCOUNT_GENERATE_QR_CODE_FAIL, e.getError().getErrorMsg());
  171 + }
  172 +
  173 + // 保存二维码
  174 + mpAccountMapper.updateById(new MpAccountDO().setId(id).setQrCodeUrl(qrCodeUrl));
  175 + }
  176 +
  177 + @Override
  178 + public void clearAccountQuota(Long id) {
  179 + // 校验存在
  180 + MpAccountDO account = validateAccountExists(id);
  181 +
  182 + // 生成二维码
  183 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId());
  184 + try {
  185 + mpService.clearQuota(account.getAppId());
  186 + } catch (WxErrorException e) {
  187 + throw exception(ErrorCodeConstants.ACCOUNT_CLEAR_QUOTA_FAIL, e.getError().getErrorMsg());
  188 + }
172 189 }
173 190  
174 191 }
... ...
yudao-ui-admin/src/api/mp/account.js
1 1 import request from '@/utils/request'
2 2  
3   -// 创建公众号账户
  3 +// 创建公众号账号
4 4 export function createAccount(data) {
5 5 return request({
6 6 url: '/mp/account/create',
... ... @@ -9,7 +9,7 @@ export function createAccount(data) {
9 9 })
10 10 }
11 11  
12   -// 更新公众号账户
  12 +// 更新公众号账号
13 13 export function updateAccount(data) {
14 14 return request({
15 15 url: '/mp/account/update',
... ... @@ -18,7 +18,7 @@ export function updateAccount(data) {
18 18 })
19 19 }
20 20  
21   -// 删除公众号账户
  21 +// 删除公众号账号
22 22 export function deleteAccount(id) {
23 23 return request({
24 24 url: '/mp/account/delete?id=' + id,
... ... @@ -26,7 +26,7 @@ export function deleteAccount(id) {
26 26 })
27 27 }
28 28  
29   -// 获得公众号账户
  29 +// 获得公众号账号
30 30 export function getAccount(id) {
31 31 return request({
32 32 url: '/mp/account/get?id=' + id,
... ... @@ -34,7 +34,7 @@ export function getAccount(id) {
34 34 })
35 35 }
36 36  
37   -// 获得公众号账户分页
  37 +// 获得公众号账号分页
38 38 export function getAccountPage(query) {
39 39 return request({
40 40 url: '/mp/account/page',
... ... @@ -43,12 +43,18 @@ export function getAccountPage(query) {
43 43 })
44 44 }
45 45  
46   -// 导出公众号账户 Excel
47   -export function exportAccountExcel(query) {
  46 +// 生成公众号二维码
  47 +export function generateAccountQrCode(id) {
48 48 return request({
49   - url: '/mp/account/export-excel',
50   - method: 'get',
51   - params: query,
52   - responseType: 'blob'
  49 + url: '/mp/account/generate-qr-code?id=' + id,
  50 + method: 'put'
  51 + })
  52 +}
  53 +
  54 +// 清空公众号 API 配额
  55 +export function clearAccountQuota(id) {
  56 + return request({
  57 + url: '/mp/account/clear-quota?id=' + id,
  58 + method: 'put'
53 59 })
54 60 }
... ...
yudao-ui-admin/src/views/mp/account/index.vue
... ... @@ -28,16 +28,25 @@
28 28 <el-table-column label="名称" align="center" prop="name"/>
29 29 <el-table-column label="微信号" align="center" prop="account" width="180"/>
30 30 <el-table-column label="appId" align="center" prop="appId" width="180"/>
31   - <el-table-column label="appSecret" align="center" prop="appSecret" width="180"/>
32   - <el-table-column label="Token" align="center" prop="token"/>
33   - <el-table-column label="密钥" align="center" prop="aesKey"/>
34   - <el-table-column label="二维码" align="center" prop="qrCodeUrl"/>
35   - <el-table-column label="备注" align="center" prop="remark"/>
36   - <el-table-column label="创建时间" align="center" prop="createTime" width="180">
37   - <template slot-scope="scope">
38   - <span>{{ parseTime(scope.row.createTime) }}</span>
  31 +<!-- <el-table-column label="appSecret" align="center" prop="appSecret" width="180"/>-->
  32 +<!-- <el-table-column label="token" align="center" prop="token"/>-->
  33 +<!-- <el-table-column label="消息加解密密钥" align="center" prop="aesKey"/>-->
  34 + <el-table-column label="服务器地址(URL)" align="center" prop="appId" width="360">
  35 + <template v-slot="scope">
  36 + {{ 'http://服务端地址/mp/open/' + scope.row.appId }}
39 37 </template>
40 38 </el-table-column>
  39 + <el-table-column label="二维码" align="center" prop="qrCodeUrl">
  40 + <template v-slot="scope">
  41 + <img v-if="scope.row.qrCodeUrl" :src="scope.row.qrCodeUrl" alt="二维码" style="height: 100px;" />
  42 + </template>
  43 + </el-table-column>
  44 + <el-table-column label="备注" align="center" prop="remark"/>
  45 +<!-- <el-table-column label="创建时间" align="center" prop="createTime" width="180">-->
  46 +<!-- <template slot-scope="scope">-->
  47 +<!-- <span>{{ parseTime(scope.row.createTime) }}</span>-->
  48 +<!-- </template>-->
  49 +<!-- </el-table-column>-->
41 50 <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
42 51 <template slot-scope="scope">
43 52 <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
... ... @@ -46,6 +55,12 @@
46 55 <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
47 56 v-hasPermi="['mp:account:delete']">删除
48 57 </el-button>
  58 + <el-button size="mini" type="text" icon="el-icon-refresh" @click="handleGenerateQrCode(scope.row)"
  59 + v-hasPermi="['mp:account:qr-code']">生成二维码
  60 + </el-button>
  61 + <el-button size="mini" type="text" icon="el-icon-share" @click="handleCleanQuota(scope.row)"
  62 + v-hasPermi="['mp:account:clear-quota']">清空 API 配额
  63 + </el-button>
49 64 </template>
50 65 </el-table-column>
51 66 </el-table>
... ... @@ -55,13 +70,13 @@
55 70  
56 71 <!-- 对话框(添加 / 修改) -->
57 72 <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
58   - <el-form ref="form" :model="form" :rules="rules" label-width="100px">
  73 + <el-form ref="form" :model="form" :rules="rules" label-width="120px">
59 74 <el-form-item label="名称" prop="name">
60 75 <el-input v-model="form.name" placeholder="请输入名称"/>
61 76 </el-form-item>
62 77 <el-form-item label="微信号" prop="account">
63 78 <span slot="label">
64   - <el-tooltip content="在微信公众平台(mp.weixin.qq.com)的菜单【设置】-【公众号设置】-【帐号详情】中能找到原始ID" placement="top">
  79 + <el-tooltip content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 账号详情] 中能找到「微信号」" placement="top">
65 80 <i class="el-icon-question" />
66 81 </el-tooltip>
67 82 微信号
... ... @@ -70,30 +85,30 @@
70 85 </el-form-item>
71 86 <el-form-item label="appId" prop="appId">
72 87 <span slot="label">
73   - <el-tooltip content="在微信公众平台(mp.weixin.qq.com)的菜单【开发】-【基本配置】中能找到 appId" placement="top">
  88 + <el-tooltip content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者ID(AppID)」" placement="top">
74 89 <i class="el-icon-question" />
75 90 </el-tooltip>
76 91 appId
77 92 </span>
78 93 <el-input v-model="form.appId" placeholder="请输入公众号 appId"/>
79 94 </el-form-item>
80   - <el-form-item label="密钥" prop="appSecret">
  95 + <el-form-item label="appSecret" prop="appSecret">
81 96 <span slot="label">
82   - <el-tooltip content="在微信公众平台(mp.weixin.qq.com)的菜单【开发】-【基本配置】中能找到密钥" placement="top">
  97 + <el-tooltip content="在微信公众平台(mp.weixin.qq.com)的菜单 [设置与开发 - 公众号设置 - 基本设置] 中能找到「开发者密码(AppSecret)」" placement="top">
83 98 <i class="el-icon-question" />
84 99 </el-tooltip>
85   - 密钥
  100 + appSecret
86 101 </span>
87 102 <el-input v-model="form.appSecret" placeholder="请输入公众号 appSecret"/>
88 103 </el-form-item>
89 104 <el-form-item label="token" prop="token">
90 105 <el-input v-model="form.token" placeholder="请输入公众号token"/>
91 106 </el-form-item>
92   - <el-form-item label="加密密钥" prop="aesKey">
93   - <el-input v-model="form.aesKey" placeholder="请输入加密密钥"/>
  107 + <el-form-item label="消息加解密密钥" prop="aesKey">
  108 + <el-input v-model="form.aesKey" placeholder="请输入消息加解密密钥"/>
94 109 </el-form-item>
95 110 <el-form-item label="备注" prop="remark">
96   - <el-input v-model="form.remark" placeholder="请输入备注"/>
  111 + <el-input type="textarea" v-model="form.remark" placeholder="请输入备注"/>
97 112 </el-form-item>
98 113 </el-form>
99 114 <div slot="footer" class="dialog-footer">
... ... @@ -105,7 +120,15 @@
105 120 </template>
106 121  
107 122 <script>
108   -import { createAccount, deleteAccount, getAccount, getAccountPage, updateAccount} from '@/api/mp/account'
  123 +import {
  124 + clearAccountQuota,
  125 + createAccount,
  126 + deleteAccount,
  127 + generateAccountQrCode,
  128 + getAccount,
  129 + getAccountPage,
  130 + updateAccount
  131 +} from '@/api/mp/account'
109 132  
110 133 export default {
111 134 name: 'mpAccount',
... ... @@ -120,7 +143,7 @@ export default {
120 143 showSearch: true,
121 144 // 总条数
122 145 total: 0,
123   - // 公众号账户列表
  146 + // 公众号账号列表
124 147 list: [],
125 148 // 弹出层标题
126 149 title: '',
... ... @@ -139,7 +162,7 @@ export default {
139 162 // 表单校验
140 163 rules: {
141 164 name: [{required: true, message: '名称不能为空', trigger: 'blur'}],
142   - account: [{required: true, message: '公众号账户不能为空', trigger: 'blur'}],
  165 + account: [{required: true, message: '公众号账号不能为空', trigger: 'blur'}],
143 166 appId: [{required: true, message: '公众号 appId 不能为空', trigger: 'blur'}],
144 167 appSecret: [{required: true, message: '公众号密钥不能为空', trigger: 'blur'}],
145 168 token: [{required: true, message: '公众号 token 不能为空', trigger: 'blur'}],
... ... @@ -199,7 +222,7 @@ export default {
199 222 handleAdd() {
200 223 this.reset()
201 224 this.open = true
202   - this.title = '添加公众号账户'
  225 + this.title = '添加公众号账号'
203 226 },
204 227 /** 修改按钮操作 */
205 228 handleUpdate(row) {
... ... @@ -208,7 +231,7 @@ export default {
208 231 getAccount(id).then(response => {
209 232 this.form = response.data
210 233 this.open = true
211   - this.title = '修改公众号账户'
  234 + this.title = '修改公众号账号'
212 235 this.disabled = true
213 236 })
214 237 },
... ... @@ -238,7 +261,7 @@ export default {
238 261 /** 删除按钮操作 */
239 262 handleDelete(row) {
240 263 const id = row.id
241   - this.$modal.confirm('是否确认删除公众号账户编号为"' + row.name + '"的数据项?').then(function () {
  264 + this.$modal.confirm('是否确认删除公众号账号编号为"' + row.name + '"的数据项?').then(function () {
242 265 return deleteAccount(id)
243 266 }).then(() => {
244 267 this.getList()
... ... @@ -246,6 +269,27 @@ export default {
246 269 }).catch(() => {
247 270 })
248 271 },
  272 + /** 生成二维码的按钮操作 */
  273 + handleGenerateQrCode(row) {
  274 + const id = row.id
  275 + this.$modal.confirm('是否确认生成公众号账号编号为"' + row.name + '"的二维码?').then(function () {
  276 + return generateAccountQrCode(id)
  277 + }).then(() => {
  278 + this.getList()
  279 + this.$modal.msgSuccess('生成二维码成功')
  280 + }).catch(() => {
  281 + })
  282 + },
  283 + /** 清空二维码 API 配额的按钮操作 */
  284 + handleCleanQuota(row) {
  285 + const id = row.id
  286 + this.$modal.confirm('是否清空生成公众号账号编号为"' + row.name + '"的 API 配额?').then(function () {
  287 + return clearAccountQuota(id)
  288 + }).then(() => {
  289 + this.$modal.msgSuccess('清空 API 配额成功')
  290 + }).catch(() => {
  291 + })
  292 + },
249 293 }
250 294 }
251 295 </script>
... ...