007:扫码关注&定义消息模版推送
1 扫码实现关注效果的演示
今日课程任务
- 微服务接口扫码登录实现原理
- 如何生成二维码链接与前端定时ajax刷新
- 微信模板接口如何实现发送消息
- 登录接口成功,实现发送微信消息模板推送
2 扫码实现关注登录原理的分析
扫码关注的原理
- 判断该用户是否有关注每特学院公众号
数据库表中有wx_open_id字段,如果为空表示没有关注,不为空表示已经关注。 - 如果没有关注每特学院公众号,调用微信接口生成二维码链接地址,传递参数userId
如果查询数据库中wx_open_id字段为空,生成微信二维码
如何关联:
当用户扫描该二维码的时候,会将该userId传递到微信开发者回调的接口;传递过程中核心两个参数openId、userId,判断用户如果没有关联openId的情况下,则关联。
- 先根据该openId查询是否有关注公众号,如果有关注,不做操作;
- 如果没有关注,获取userId,对应修改openId
openId表示开放的唯一userId。
3 微信生成临时二维码原理分析
微信服务提供接口:根据userId生成带参数链接的二维码
Vue调用会员服务接口传递token,获取带参数链接的二维码
- 根据token获取userId(直接传递userId不安全)
- 调用微信服务提供接口生成带参数链接的二维码
如果该用户没有关注微信公众号的情况下,扫二维码链接的时候,不会推送扫码事件,改为推送新用户关注事件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);
}
}
}
测试效果:
注意:
- 如果多个feign接口冲突,加上配置
spring:
main:
allow-bean-definition-overriding: true
- 如果需要传递中文,先用URLEncode编码传参,接收到请求参数以后用UrlDecode解码。