基于Vue+SpringCloudAlibaba微服务电商项目实战-构建会员服务-007:扫码关注&定义消息模版推送

1 扫码实现关注效果的演示

今日课程任务

  1. 微服务接口扫码登录实现原理
  2. 如何生成二维码链接与前端定时ajax刷新
  3. 微信模板接口如何实现发送消息
  4. 登录接口成功,实现发送微信消息模板推送

2 扫码实现关注登录原理的分析

扫码关注的原理

  1. 判断该用户是否有关注每特学院公众号
    数据库表中有wx_open_id字段,如果为空表示没有关注,不为空表示已经关注。
  2. 如果没有关注每特学院公众号,调用微信接口生成二维码链接地址,传递参数userId
    如果查询数据库中wx_open_id字段为空,生成微信二维码

如何关联:
当用户扫描该二维码的时候,会将该userId传递到微信开发者回调的接口;传递过程中核心两个参数openId、userId,判断用户如果没有关联openId的情况下,则关联。

  1. 先根据该openId查询是否有关注公众号,如果有关注,不做操作;
  2. 如果没有关注,获取userId,对应修改openId
    openId表示开放的唯一userId。

3 微信生成临时二维码原理分析

微信服务提供接口:根据userId生成带参数链接的二维码
Vue调用会员服务接口传递token,获取带参数链接的二维码

  1. 根据token获取userId(直接传递userId不安全)
  2. 调用微信服务提供接口生成带参数链接的二维码

如果该用户没有关注微信公众号的情况下,扫二维码链接的时候,不会推送扫码事件,改为推送新用户关注事件SubscribeHandler;
如果该用户已经关注微信公众号情况下,扫二维码链接的时候,推送扫码事件ScanHandler

4 提供微信生成二维码链接地址

帐号管理 生成带参数的二维码 官方文档
https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html

根据userId生成微信二维码链接接口及实现类

public interface WeiXinConstant {
    /**
     * 最大不超过2592000(即30天)
     */
    Integer QR_CODE_EXPIRE_SECONDS = 2592000;
}
@Api(tags = "根据userId生成微信二维码链接")
public interface WeiXinQrCodeService {

    @GetMapping("/getQrUrl")
    BaseResponse<JSONObject> getQrUrl(@RequestParam("userId") Long userId);
}
@RestController
public class WeiXinQrCodeServiceImpl extends BaseApiService implements WeiXinQrCodeService {

    @Autowired
    private WxMpProperties wxMpProperties;

    @Override
    public BaseResponse<JSONObject> getQrUrl(Long userId) {
        if (userId == null) {
            return setResultError("userId不能为空");
        }
        // 获取配置第一个appId
        String appId = wxMpProperties.getConfigs().get(0).getAppId();
        // 根据appId获取对应的WxMpQrcodeService
        WxMpQrcodeService qrcodeService = WxMpConfiguration.getMpServices().get(appId).getQrcodeService();
        try {
            WxMpQrCodeTicket wxMpQrCodeTicket = qrcodeService.qrCodeCreateTmpTicket(userId + "",
                    WeiXinConstant.QR_CODE_EXPIRE_SECONDS);
            if (wxMpQrCodeTicket == null) {
                return setResultError("生成二维码链接失败!");
            }
            String ticket = wxMpQrCodeTicket.getTicket();
            return setResultSuccess(URLEncoder.encode(ticket, "UTF-8"));
        } catch (Exception e) {
            return setResultError("生成二维码链接失败!");
        }
    }
}

通过ticket换取二维码
获取二维码ticket后,开发者可用ticket换取二维码图片。请注意,本接口无须登录态即可调用。
请求说明
HTTP GET请求(请使用https协议)https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=TICKET 提醒:TICKET记得进行UrlEncode

测试效果:
在这里插入图片描述

5 微信客户端测试扫二维码回调接口测试

测试效果:
在这里插入图片描述
为什么需要拼接ticket链接地址
只需要传ticket给客户端,前端拿到ticket自己拼接二维码链接地址,减少带宽传输

6 回调接口中新增关联openid代码

会员服务

@Api(tags = "用户会员信息基本接口")
public interface MemberInfoService {

    /**
     * 根据用户token查询用户信息
     *
     * @param token
     * @return
     */
    @GetMapping("/getTokenUser")
    @ApiOperation("根据token查看用户信息")
    @ApiImplicitParam(name = "token", value = "token", required = true)
    BaseResponse<UserRespDTO> getTokenUser(@RequestParam("token") String token);

    /**
     * 关联用户的openid
     *
     * @param userId
     * @param openId
     * @return
     */
    @PostMapping("/updateUseOpenId")
    @ApiOperation("关联用户的openid")
    BaseResponse<Object> updateUseOpenId(@RequestParam("userId") Long userId,
                                         @RequestParam(name = "openId", required = false) String openId);

    /**
     * 根据openid  查询用户信息
     *
     * @param openId
     * @return
     */
    @GetMapping("/selectByOpenId")
    @ApiOperation("根据openid 查询用户信息")
    BaseResponse<UserRespDTO> selectByOpenId(
            @RequestParam("openId") String openId);


    /**
     * 取消关注
     *
     * @param openId
     * @return
     */
    @GetMapping("/cancelFollowOpenId")
    BaseResponse<Object> cancelFollowOpenId(@RequestParam("openId") String openId);
}
@RestController
public class MemberInfoServiceImpl extends BaseApiService implements MemberInfoService {

    @Autowired
    private TokenUtil tokenUtil;
    @Autowired
    private UserMapper userMapper;

    @Override
    public BaseResponse<UserRespDTO> getTokenUser(String token) {

        if (StringUtils.isEmpty(token)) {
            return setResultError("token不能为空");
        }
        // 从Redis中获取到userId
        String redisValue = tokenUtil.getTokenValue(token);
        if (StringUtils.isEmpty(redisValue)) {
            return setResultError("token已经过期");
        }
        long userId = Long.parseLong(redisValue);
        // 根据userId查询用户信息
        UserDO userDO = userMapper.findByUser(userId);
        if (userDO == null) {
            return setResultError("token已经过期或者错误!");
        }
        UserRespDTO userRespDTO = doToDto(userDO, UserRespDTO.class);
        String mobile = userRespDTO.getMobile();
        userRespDTO.setMobile(DesensitizationUtil.mobileEncrypt(mobile));
        return setResultSuccess(userRespDTO);
    }

    @Override
    public BaseResponse<Object> updateUseOpenId(Long userId, String openId) {
        int reuslt = userMapper.updateUseOpenId(userId, openId);
        return setResultDb(reuslt, "关联成功", "关联失败");
    }

    @Override
    public BaseResponse<UserRespDTO> selectByOpenId(String openId) {
        UserDO userDo = userMapper.selectByOpenId(openId);
        if (userDo == null) {
            return setResultError("根据openId查询该用户没有关注过");
        }
        // 需要将do转换成dto
        UserRespDTO userRespDto = doToDto(userDo, UserRespDTO.class);
        String mobile = userRespDto.getMobile();
        userRespDto.setMobile(DesensitizationUtil.mobileEncrypt(mobile));
        return setResultSuccess(userRespDto);
    }

    @Override
    public BaseResponse<Object> cancelFollowOpenId(String openId) {
        if (StringUtils.isEmpty(openId)) {
            return setResultError("openId不能为空");
        }
        UserDO userDo = userMapper.selectByOpenId(openId);
        if (userDo == null) {
            return setResultError("根据openId查询该用户没有关注过");
        }
        // 已经关注过,则将对应的用户的openid 变为空
        int result = userMapper.cancelFollowOpenId(openId);
        return setResultDb(result, "取消关注成功", "取消关注成功失败!");
    }
}
public interface UserMapper {

    @Insert("INSERT INTO `meite_user` VALUES (null, #{mobile},null, #{password}, null, '0', '0', now()," +
            " now(),'1',null , null, null);\n")
    int register(UserDO userDo);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",user_name as username ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime,IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
            "\n" +
            "from meite_user  where MOBILE=#{mobile}")
    UserDO login(String mobile, String passWord);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",user_name as username ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime,IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
            "\n" +
            "from meite_user  where MOBILE=#{mobile}")
    UserDO existMobile(String mobile);
    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",user_name as username ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime,IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picimg,qq_openid as qqopenid ,wx_openid as wxopenid\n" +
            "\n" +
            "from meite_user  where USER_ID=#{userId}")
    UserDO findByUser(Long userId);

    @Update("\n" +
            "update meite_user set WX_OPENID=#{wxOpenId}  where user_id=#{userId};")
    int updateUseOpenId(Long userId, String wxOpenId);

    @Select("SELECT USER_ID AS USERID ,MOBILE AS MOBILE ,password as password\n" +
            ",email as email ,user_name as username,sex as sex \n" +
            ",age as age ,create_time as createtime, IS_AVAILABLE as isAvailable\n" +
            ",\n" +
            "pic_img  as picImg,qq_openid as qqOpenId ,wx_openid as wxOpenId\n" +
            "\n" +
            "from meite_user  where wx_OpenId=#{wxOpenId}")
    UserDO selectByOpenId(String wxOpenId);


    @Update("\n" +
            "update meite_user set WX_OPENID=null  where WX_OPENID=#{wxOpenId};")
    int cancelFollowOpenId(String wxOpenId);

}

微信服务

@FeignClient("mayikt-member")
public interface MemberInfoServiceFeign extends MemberInfoService {
}
@Component
public class WxMpServiceManage {
    @Autowired
    private MemberInfoServiceFeign memberInfoServiceFeign;

    public WxMpXmlOutMessage handle(Long userId, String openId) {
        // 正常业务逻辑以下两个方法可以封装成一个借口进行操作WxMpServiceManage
        // 先根据openid 查询是否已经关联  ---会员服务
        BaseResponse<UserRespDTO> userRespDtoBaseResponse = memberInfoServiceFeign.selectByOpenId(openId);
        if (userRespDtoBaseResponse.getCode().equals(HttpConstant.RPC_RESULT_SUCCESS)) {
            return null;
        }
        // 如果没有关注过的情况下  update ---会员服务
        memberInfoServiceFeign.updateUseOpenId(userId, openId);
        return null;
    }
}
@Component
public class SubscribeHandler extends AbstractHandler {

    @Autowired
    private WxMpServiceManage wxMpServiceManage;

    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
                                    Map<String, Object> context, WxMpService weixinService,
                                    WxSessionManager sessionManager) throws WxErrorException {

        this.logger.info("新关注用户 OPENID: " + wxMessage.getFromUser());

        // 获取微信用户基本信息
        try {
            WxMpUser userWxInfo = weixinService.getUserService()
                    .userInfo(wxMessage.getFromUser(), null);
            if (userWxInfo != null) {
                // TODO 可以添加关注用户到本地数据库
            }
        } catch (WxErrorException e) {
            if (e.getError().getErrorCode() == 48001) {
                this.logger.info("该公众号没有获取用户信息权限!");
            }
        }

        // 说明没有关注该微信公众号
        String eventKey = wxMessage.getEventKey();
        if (!StringUtils.isEmpty(eventKey)) {
            String qrscene = eventKey.replace("qrscene_", "");
            Long userId = Long.parseLong(qrscene);
            String openId = wxMessage.getFromUser();
            wxMpServiceManage.handle(userId, openId);
        }

        WxMpXmlOutMessage responseResult = null;
        try {
            responseResult = this.handleSpecial(wxMessage);
        } catch (Exception e) {
            this.logger.error(e.getMessage(), e);
        }

        if (responseResult != null) {
            return responseResult;
        }

        try {
            return new TextBuilder().build("感谢关注", wxMessage, weixinService);
        } catch (Exception e) {
            this.logger.error(e.getMessage(), e);
        }

        return null;
    }

    /**
     * 处理特殊请求,比如如果是扫码进来的,可以做相应处理
     */
    private WxMpXmlOutMessage handleSpecial(WxMpXmlMessage wxMessage)
            throws Exception {
        //TODO
        return null;
    }
}
@Component
public class ScanHandler extends AbstractHandler {
    @Autowired
    private WxMpServiceManage wxMpServiceManage;

    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
                                    WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
        // 扫码事件处理

        String eventKey = wxMpXmlMessage.getEventKey();
        if (!StringUtils.isEmpty(eventKey)) {
            Long userId = Long.parseLong(eventKey);
            String openId = wxMpXmlMessage.getFromUser();
            wxMpServiceManage.handle(userId, openId);
        }
        return null;
    }
}
@Component
public class UnsubscribeHandler extends AbstractHandler {

    @Autowired
    private MemberInfoServiceFeign memberInfoServiceFeign;

    @Override
    public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage,
                                    Map<String, Object> context, WxMpService wxMpService,
                                    WxSessionManager sessionManager) {
        String openId = wxMessage.getFromUser();
        this.logger.info("取消关注用户 OPENID: " + openId);
        // TODO 可以更新本地数据库为取消关注状态
        memberInfoServiceFeign.cancelFollowOpenId(openId);
        return null;
    }
}

测试效果:
在这里插入图片描述

7 调用微信消息模版接口发送登录提醒

登录成功后判断用户是否关注微信公众号,如果有关注公众号,发送登录提示。

消息模版接口推送
亲爱的用户:{{first.DATA}}
登陆时间:{{keyword1.DATA}}
登陆ip:{{keyword2.DATA}}
登陆设备:{{keyword3.DATA}}
如果不是您本人登陆,可以联系管理员锁定账号.

@Data
public class LoginTemplateDTO {
    private String phone;
    private Date loginTime;
    private String loginIp;
    private String equipment;

    public LoginTemplateDTO(String phone, Date loginTime, String loginIp, String equipment, String openId) {
        this.phone = phone;
        this.loginTime = loginTime;
        this.loginIp = loginIp;
        this.equipment = equipment;
        this.openId = openId;
    }

    /**
     * openid
     */
    private String openId;
}
@Api(tags = "微信模版消息推送")
public interface WeChatLoginTemplate {

    /**
     *
     * @param loginTemplateDto
     * @return
     */
    @PostMapping("/sendLoginTemplate")
    BaseResponse<Object> sendLoginTemplate(@RequestBody LoginTemplateDTO loginTemplateDto);
}
@RestController
public class WeChatLoginTemplateImpl extends BaseApiService implements WeChatLoginTemplate {
    @Autowired
    private WxMpProperties wxMpProperties;
    @Value("${mayikt.wx.loginTemplateId}")
    private String loginTemplateId;

    @Override
    public BaseResponse<Object> sendLoginTemplate(LoginTemplateDTO loginTemplateDto) {

        String phone = loginTemplateDto.getPhone();
        if (StringUtils.isEmpty(phone)) {
            return setResultError("phone参数不能为空!");
        }
        String loginIp = loginTemplateDto.getLoginIp();
        if (StringUtils.isEmpty(phone)) {
            return setResultError("loginIp参数不能为空!");
        }
        Date loginTime =
                loginTemplateDto.getLoginTime();
        if (loginTime == null) {
            return setResultError("loginTime参数不能为空!");
        }
        String openId = loginTemplateDto.getOpenId();
        if (StringUtils.isEmpty(openId)) {
            return setResultError("loginIp参数不能为空!");
        }
        String equipment = loginTemplateDto.getEquipment();
        WxMpTemplateMessage wxMpTemplateMessage = new WxMpTemplateMessage();
        wxMpTemplateMessage.setTemplateId(loginTemplateId);
        wxMpTemplateMessage.setToUser(openId);
        List<WxMpTemplateData> data = new ArrayList<>();
        data.add(new WxMpTemplateData("first", phone));
        data.add(new WxMpTemplateData("keyword1",
                SimpleDateFormatUtil.getFormatStrByPatternAndDate(loginTime)));
        data.add(new WxMpTemplateData("keyword2", loginIp));
        data.add(new WxMpTemplateData("keyword3", equipment));
        wxMpTemplateMessage.setUrl("http://www.mayikt.com");
        wxMpTemplateMessage.setData(data);
        try {
            String appId = wxMpProperties.getConfigs().get(0).getAppId();
            WxMpTemplateMsgService templateMsgService = WxMpConfiguration.getMpServices().get(appId).getTemplateMsgService();
            templateMsgService.sendTemplateMsg(wxMpTemplateMessage);
            return setResultSuccess();
        } catch (Exception e) {
            return setResultError("发送失败");
        }
    }
}

配置文件新增

mayikt:
  wx:
    loginTemplateId: NysSpBQPDxBK_NCfqpGcG9WEhqzoKy-NAe_J6_dnP6g

测试效果:
在这里插入图片描述

8 登录接口调用微信接口发送登录提醒

改造会员服务登录接口
会员服务登录后如果wxOpenId有值,调用微信消息模板接口

@FeignClient("mayikt-weixin")
public interface WeChatLoginTemplateFeign extends WeChatLoginTemplate {
}
@RestController
@Slf4j
public class MemberLoginServiceImpl extends BaseApiService implements MemberLoginService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private TokenUtil tokenUtil;

    @Value("${mayikt.login.token.prefix}")
    private String loginTokenPrefix;

    @Autowired
    private AsyncLoginLogManage asyncLoginLogManage;

    @Autowired
    private ChannelUtils channelUtils;

    @Override
    public BaseResponse<JSONObject> login(UserLoginDTO userLoginDTO, String sourceIp, String channel, String deviceInfor) throws UnsupportedEncodingException {
        // 参数验证
        String mobile = userLoginDTO.getMobile();
        if (StringUtils.isEmpty(mobile)) {
            return setResultError("mobile参数不能为空");
        }
        String password = userLoginDTO.getPassword();
        if (StringUtils.isEmpty(password)) {
            return setResultError("password参数不能为空");
        }
        // 查询数据库
        String newPassword = MD5Util.MD5(password);
        UserDO loginUserDo = userMapper.login(mobile, newPassword);
        if (loginUserDo == null) {
            return setResultError("手机号码或者密码不正确");
        }
        // 设备信息
        if (StringUtils.isEmpty(deviceInfor)) {
            return setResultError("设备信息不能为空");
        }
        // 如果传中文,请求参数URLEncode处理后再传参
        deviceInfor = URLDecoder.decode(deviceInfor, "UTF-8");
        if (!channelUtils.existChannel(channel)) {
            return setResultError("渠道来源错误");
        }
        Long userId = loginUserDo.getUserId();
        String userToken = tokenUtil.createToken(loginTokenPrefix, userId + "");
        JSONObject resultJSON = new JSONObject();
        resultJSON.put("userToken", userToken);
        String wxOpenId = loginUserDo.getWxOpenId();
        asyncLoginLogManage.loginLog(wxOpenId, mobile, userId, sourceIp, new Date(), userToken
                , channel, deviceInfor);
        return setResultSuccess(resultJSON);
    }
}
@Component
@Slf4j
public class AsyncLoginLogManage {

    @Autowired
    private UserLoginLogMapper userLoginLogMapper;

    @Autowired
    private TokenUtil tokenUtil;

    @Autowired
    private WeChatLoginTemplateFeign weChatLoginTemplateFeign;

    @Async
    public void loginLog(String openId, String mobile, Long userId, String loginIp, Date loginTime, String loginToken, String channel,
                         String equipment) {
        // 1. 根据当前的渠道+userid+可用 查询 该用户是否已经登录过
        UserLoginLogDo userLoginLogDo =
                userLoginLogMapper.selectByUserIdAndLoginType(userId, channel);
        // 2. 如果没有查询到记录情况下
        if (userLoginLogDo != null) {
            String oldToken = userLoginLogDo.getLoginToken();
            // 更新数据库token状态
            userLoginLogMapper.updateUserTokenNotQuipment(oldToken);
            // 从redis删除该token
            tokenUtil.delToken(oldToken);
        }
        // 插入最新的token到数据库中
        UserLoginLogDo newUserLoginLogDo = new UserLoginLogDo(userId, loginIp, loginTime, loginToken, channel, equipment);
        userLoginLogMapper.insertUserLoginLog(newUserLoginLogDo);

        // 3. 发送登录日志给微信客户端 调用微信接口发送消息模板
        if (!StringUtils.isEmpty(openId)) {
            LoginTemplateDTO loginTemplateDto = new
                    LoginTemplateDTO(DesensitizationUtil.mobileEncrypt(mobile),
                    loginTime, loginIp, equipment, openId);
            weChatLoginTemplateFeign.sendLoginTemplate(loginTemplateDto);
        }
    }
}

测试效果:
在这里插入图片描述
注意:

  1. 如果多个feign接口冲突,加上配置
spring:
  main:
    allow-bean-definition-overriding: true
  1. 如果需要传递中文,先用URLEncode编码传参,接收到请求参数以后用UrlDecode解码。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值