diff --git a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java index fe39c15a4..f5d4ea625 100644 --- a/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java +++ b/yudao-module-mp/yudao-module-mp-api/src/main/java/cn/iocoder/yudao/module/mp/enums/ErrorCodeConstants.java @@ -14,6 +14,9 @@ public interface ErrorCodeConstants { ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1006000001, "生成公众号二维码失败,原因:{}"); ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1006000001, "清空公众号的 API 配额失败,原因:{}"); + // ========== 公众号账号 1006001000============ + ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1006001000, "获取用户增减数据失败,原因:{}"); + // TODO 要处理下 ErrorCode COMMON_NOT_EXISTS = new ErrorCode(1006001002, "用户不存在"); diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java index 460b9f5ff..866f7475a 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/MpAccountController.java @@ -33,14 +33,14 @@ public class MpAccountController { @PostMapping("/create") @ApiOperation("创建公众号账号") @PreAuthorize("@ss.hasPermission('mp:account:create')") - public CommonResult createWxAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) { + public CommonResult createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) { return success(mpAccountService.createAccount(createReqVO)); } @PutMapping("/update") @ApiOperation("更新公众号账号") @PreAuthorize("@ss.hasPermission('mp:account:update')") - public CommonResult updateWxAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) { + public CommonResult updateAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) { mpAccountService.updateAccount(updateReqVO); return success(true); } @@ -49,7 +49,7 @@ public class MpAccountController { @ApiOperation("删除公众号账号") @ApiImplicitParam(name = "id", value = "编号", required = true, dataTypeClass = Long.class) @PreAuthorize("@ss.hasPermission('mp:account:delete')") - public CommonResult deleteWxAccount(@RequestParam("id") Long id) { + public CommonResult deleteAccount(@RequestParam("id") Long id) { mpAccountService.deleteAccount(id); return success(true); } @@ -58,7 +58,7 @@ public class MpAccountController { @ApiOperation("获得公众号账号") @ApiImplicitParam(name = "id", value = "编号", required = true, example = "1024", dataTypeClass = Long.class) @PreAuthorize("@ss.hasPermission('mp:account:query')") - public CommonResult getWxAccount(@RequestParam("id") Long id) { + public CommonResult getAccount(@RequestParam("id") Long id) { MpAccountDO wxAccount = mpAccountService.getAccount(id); return success(MpAccountConvert.INSTANCE.convert(wxAccount)); } @@ -66,7 +66,7 @@ public class MpAccountController { @GetMapping("/page") @ApiOperation("获得公众号账号分页") @PreAuthorize("@ss.hasPermission('mp:account:query')") - public CommonResult> getWxAccountPage(@Valid MpAccountPageReqVO pageVO) { + public CommonResult> getAccountPage(@Valid MpAccountPageReqVO pageVO) { PageResult pageResult = mpAccountService.getAccountPage(pageVO); return success(MpAccountConvert.INSTANCE.convertPage(pageResult)); } diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java index 047a26ecd..37453892a 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java @@ -13,7 +13,7 @@ import javax.validation.constraints.*; @ToString(callSuper = true) public class MpAccountUpdateReqVO extends MpAccountBaseVO { - @ApiModelProperty(value = "编号", required = true) + @ApiModelProperty(value = "编号", required = true, example = "1024") @NotNull(message = "编号不能为空") private Long id; diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java new file mode 100644 index 000000000..5562c34b7 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/MpStatisticsController.java @@ -0,0 +1,38 @@ +package cn.iocoder.yudao.module.mp.controller.admin.statistics; + +import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.module.mp.convert.statistics.MpStatisticsConvert; +import cn.iocoder.yudao.module.mp.service.statistics.MpStatisticsService; +import cn.iocoder.yudao.module.mp.controller.admin.statistics.vo.MpStatisticsGetReqVO; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; + +@Api(tags = "管理后台 - 公众号统计") +@RestController +@RequestMapping("/mp/statistics") +@Validated +public class MpStatisticsController { + + @Resource + private MpStatisticsService mpStatisticsService; + + @GetMapping("/user-summary") + @ApiOperation("获得用户增减数据") + @PreAuthorize("@ss.hasPermission('mp:statistics:query')") + public CommonResult> getAccount(MpStatisticsGetReqVO getReqVO) { + List list = mpStatisticsService.getUserSummary(getReqVO.getId(), getReqVO.getDate()); + return success(MpStatisticsConvert.INSTANCE.convertList01(list)); + } + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java new file mode 100644 index 000000000..b33b3c0b7 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@ApiModel("管理后台 - 获得统计数据 Request VO") +@Data +public class MpStatisticsGetReqVO { + + @ApiModelProperty(value = "公众号账号的编号", required = true, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long id; + + @ApiModelProperty(value = "查询时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotNull(message = "查询时间范围不能为空") + private LocalDateTime[] date; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java new file mode 100644 index 000000000..06c6454f1 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.module.mp.controller.admin.statistics.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.util.Date; + +@ApiModel("管理后台 - 某一天的用户增减数据 Response VO") +@Data +public class MpStatisticsUserSummaryRespVO { + + @ApiModelProperty(value = "日期", required = true) + private Date refDate; + + @ApiModelProperty(value = "用户来源", required = true, example = "0") + private Integer userSource; + + @ApiModelProperty(value = "新关注的用户数量", required = true, example = "10") + private Integer newUser; + + @ApiModelProperty(value = "取消关注的用户数量", required = true, example = "20") + private Integer cancelUser; + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java new file mode 100644 index 000000000..777961dfc --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/convert/statistics/MpStatisticsConvert.java @@ -0,0 +1,16 @@ +package cn.iocoder.yudao.module.mp.convert.statistics; + +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpStatisticsConvert { + + MpStatisticsConvert INSTANCE = Mappers.getMapper(MpStatisticsConvert.class); + + List convertList01(List list); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java index 2d84d4bef..02df9fc57 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/DefaultMpServiceFactory.java @@ -38,7 +38,11 @@ public class DefaultMpServiceFactory implements MpServiceFactory { /** * 微信 appId 与 WxMpService 的映射 */ - private volatile Map mpServices; + private volatile Map appId2MpServices; + /** + * 公众号账号 id 与 WxMpService 的映射 + */ + private volatile Map id2MpServices; /** * 微信 appId 与 WxMpMessageRouter 的映射 */ @@ -62,26 +66,34 @@ public class DefaultMpServiceFactory implements MpServiceFactory { @Override public void init(List list) { - Map mpServices = Maps.newHashMap(); + Map appId2MpServices = Maps.newHashMap(); + Map id2MpServices = Maps.newHashMap(); Map mpMessageRouters = Maps.newHashMap(); // 处理 list list.forEach(account -> { // 构建 WxMpService 对象 WxMpService mpService = buildMpService(account); - mpServices.put(account.getAppId(), mpService); + appId2MpServices.put(account.getAppId(), mpService); + id2MpServices.put(account.getId(), mpService); // 构建 WxMpMessageRouter 对象 WxMpMessageRouter mpMessageRouter = buildMpMessageRouter(mpService); mpMessageRouters.put(account.getAppId(), mpMessageRouter); }); // 设置到缓存 - this.mpServices = mpServices; + this.appId2MpServices = appId2MpServices; + this.id2MpServices = id2MpServices; this.mpMessageRouters = mpMessageRouters; } + @Override + public WxMpService getMpService(Long id) { + return id2MpServices.get(id); + } + @Override public WxMpService getMpService(String appId) { - return mpServices.get(appId); + return appId2MpServices.get(appId); } @Override diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java index 7da6f07ae..ce017ff64 100644 --- a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/framework/mp/core/MpServiceFactory.java @@ -21,6 +21,20 @@ public interface MpServiceFactory { */ void init(List list); + /** + * 获得 id 对应的 WxMpService 实例 + * + * @param id 微信公众号的编号 + * @return WxMpService 实例 + */ + WxMpService getMpService(Long id); + + default WxMpService getRequiredMpService(Long id) { + WxMpService wxMpService = getMpService(id); + Assert.notNull(wxMpService, "找到对应 id({}) 的 WxMpService,请核实!", id); + return wxMpService; + } + /** * 获得 appId 对应的 WxMpService 实例 * diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java new file mode 100644 index 000000000..65bb159c8 --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsService.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.module.mp.service.statistics; + +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 公众号统计 Service 接口 + * + * @author 芋道源码 + */ +public interface MpStatisticsService { + + /** + * 获取用户增减数据 + * + * @param id 公众号账号编号 + * @param date 时间区间 + * @return 用户增减数据 + */ + List getUserSummary(Long id, LocalDateTime[] date); + +} diff --git a/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java new file mode 100644 index 000000000..7d9d6eadb --- /dev/null +++ b/yudao-module-mp/yudao-module-mp-biz/src/main/java/cn/iocoder/yudao/module/mp/service/statistics/MpStatisticsServiceImpl.java @@ -0,0 +1,42 @@ +package cn.iocoder.yudao.module.mp.service.statistics; + +import cn.hutool.core.date.DateUtil; +import cn.iocoder.yudao.module.mp.framework.mp.core.MpServiceFactory; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; +import static cn.iocoder.yudao.module.mp.enums.ErrorCodeConstants.STATISTICS_GET_USER_SUMMARY_FAIL; + +/** + * 公众号统计 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class MpStatisticsServiceImpl implements MpStatisticsService { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + public List getUserSummary(Long id, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(id); + try { + return mpService.getDataCubeService().getUserSummary( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_USER_SUMMARY_FAIL, e.getError().getErrorMsg()); + } + } + + +} diff --git a/yudao-ui-admin/src/api/mp/statistics.js b/yudao-ui-admin/src/api/mp/statistics.js new file mode 100644 index 000000000..a1f182a86 --- /dev/null +++ b/yudao-ui-admin/src/api/mp/statistics.js @@ -0,0 +1,28 @@ +import request from '@/utils/request' + +// TODO 获得公众号账号分页 +export function getInterfaceSummary(query) { + return request({ + url: '/mp/account/page', + method: 'get', + params: query + }) +} + +// TODO 获得公众号账号分页 +export function getUserSummary(query) { + return request({ + url: '/mp/statistics/user-summary', + method: 'get', + params: query + }) +} + +// TODO 获得公众号账号分页 +export function getUserCumulate(query) { + return request({ + url: '/mp/account/page', + method: 'get', + params: query + }) +} diff --git a/yudao-ui-admin/src/utils/dateUtils.js b/yudao-ui-admin/src/utils/dateUtils.js index dd4ea5313..5c45b3679 100644 --- a/yudao-ui-admin/src/utils/dateUtils.js +++ b/yudao-ui-admin/src/utils/dateUtils.js @@ -32,3 +32,35 @@ export function beginOfDay(date) { export function endOfDay(date) { return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); } + +export function betweenDay(date1, date2) { + // 适配 string 字符串的日期 + if (typeof date1 === 'string') { + date1 = new Date(date1); + } + if (typeof date2 === 'string') { + date2 = new Date(date2); + } + return Math.floor((date2.getTime() - date1.getTime()) / (24 * 3600 * 1000)); +} + +export function formatDate(date, fmt) { + const o = { + "M+": date.getMonth() + 1, //月份 + "d+": date.getDate(), //日 + "H+": date.getHours(), //小时 + "m+": date.getMinutes(), //分 + "s+": date.getSeconds(), //秒 + "q+": Math.floor((date.getMonth() + 3) / 3), //季度 + "S": date.getMilliseconds() //毫秒 + }; + if (/(y+)/.test(fmt)) { // 年份 + fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); + } + for (const k in o) { + if (new RegExp("(" + k + ")").test(fmt)) { + fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); + } + } + return fmt; +} diff --git a/yudao-ui-admin/src/views/mp/account/index.vue b/yudao-ui-admin/src/views/mp/account/index.vue index 97d6ff514..87f116212 100644 --- a/yudao-ui-admin/src/views/mp/account/index.vue +++ b/yudao-ui-admin/src/views/mp/account/index.vue @@ -33,7 +33,7 @@ diff --git a/yudao-ui-admin/src/views/mp/statistics/index.vue b/yudao-ui-admin/src/views/mp/statistics/index.vue new file mode 100644 index 000000000..2fbf53243 --- /dev/null +++ b/yudao-ui-admin/src/views/mp/statistics/index.vue @@ -0,0 +1,319 @@ + + + + +