假设的我们的服务号有这么一些功能,比如底部有按钮,点击会有一些复杂的功能,这时候可能就需要一个用户系统,有用户系统就经常想要做什么分享邀请新用户之类的,这时候就又有几种方式,1.直接一个连接,让其他用户点;2.有一个二维码,让离得近的朋友扫。
借着这个需求体会了下微信开发的两种不同类型(80非80端口的两种开发),以及python-social-auth的一些正确姿势。
而这个需求其实就对应了两种开发模式,比如有个需求可以在公众号内直接回复,或者在一个页面里面让用户提交表单等等。
首先,理解需求:用户在微信点击我们的邀请连接后,会引导用户做一个有绿色按钮的微信登录,用户登陆后成为我们的用户,并且跳转到某个页面。
关于用户微信登录的事情我们通过python-social-auth已经解决了(参考我的上一篇博客 微信公众号开发小记——3.接入三方登录 ),所以可以直接用django的login_required装饰器完成这种事情。
由于微信号的登录只有微信,所以 LOGIN_URL = '/login/weixinapp/'
class InviteUserView(View): '''邀请注册''' @method_decorator(login_required) def get(self, request): return HttpResponseRedirect(reverse('myauth:personal-center'))
上面的代码只是保证用户点解邀请链接会成为我们的用户,但是没有记录对应的邀请者信息等等,由于邀请这个事情其实是一个登录的流程,所以可以写在pipeline里面
def invite_user(backend, user, response, *args, **kwargs): is_new = kwargs['is_new'] if not is_new or not user: return # 二维码扫描 ... # 点击邀请链接 next_url = backend.strategy.session_get('next') if next_url: params = parse_url(next_url)['params'] inviter_id = params.get('inviter_id') if inviter_id and user: try: inviter = User.objects.get(id=inviter_id) UserInvite.invite_user(inviter_id, user, only_allow_invited_by_one_user=True) except: return user._inviter = inviter return {'inviter': inviter}
首先,扫码是一个服务号80端口的事件,所以代码添加在weixin_server/views.py 微信公众号开发小记——2.80端口上的服务
难点在于这里,微信扫码后是直接进入公众号的,如果你想要让用户进入公众号之后就变成我们的用户而不是让他在点一个东西这里是比较蛋疼的,因为你的服务器在这时候做302微信是不认得。这就导致了几个问题:
由于不引导用户登录,我这里没办法直接用python-social-auth里面的do_complete方法(因为拿不到用户的access_token),不过好处是使用微信服务器的access_token以及用户的openid我可以直接拿到这个用户的用户信息。这个问题就变成了python-social-auth的do_complete有用户response后执行pipeline的逻辑了。
然后我扒了下代码,用了几个小时从单测里面找到了这个逻辑,具体见 handle_invite_scan
, 这段代码才是这篇博客里面难度最大的东西
def weixin_handler_event_scan(self, request, parsed_wechat, *args, **kwargs): key = parsed_wechat.message.key # 对应生成二维码的key ticket = parsed_wechat.message.ticket if ticket: response = self.handle_invite_scan(request, parsed_wechat, key) if response: return response return self.weixin_handler_event( request, parsed_wechat, *args, **kwargs) def handle_invite_scan(self, request, parsed_wechat, scene_id): try: qrcode = QRCode.objects.get(scene_id=scene_id, action_type='invite_user') except QRCode.DoesNotExist: return openid = parsed_wechat.message.source user_info = parsed_wechat.get_user_info(openid) strategy = load_strategy(request) backend = WeixinOAuth2APP() backend.strategy = strategy idx, backend, xargs, xkwargs = strategy.partial_from_session( { 'next':0, 'backend': backend, 'args':[], 'kwargs':{'qrcode': qrcode}, } ) xkwargs.update({'response': user_info}) user = backend.continue_pipeline(pipeline_index=idx, *xargs, **xkwargs) if not user: return if user.is_new and hasattr(user, '_inviter'): content = u'感谢您的加入,邀请者是 {}'.format(user._inviter.username) response_xml = parsed_wechat.response_text(content=content) return HttpResponse(response_xml, content_type='application/xml')
然后就可以正常的执行了,由于二维码的机制跟url不同,所以需要单独的二维码处理逻辑
下面先把pipeline的那段代码贴过来,这里没什么特殊的
def invite_user(backend, user, response, *args, **kwargs): is_new = kwargs['is_new'] if not is_new or not user: return # 二维码扫描 qrcode = kwargs.get('qrcode') if qrcode and qrcode.userprofile_set.all().exists(): inviter = qrcode.userprofile_set.all()[0].user try: UserInvite.invite_user(inviter.id, user, only_allow_invited_by_one_user=True) except: return user._inviter = inviter return {'inviter': inviter} ....
二维码有两种大的类型,永久二维码、临时二维码,永久上线10万张,scenen_id为1~10万,然而他又有scenen_str这种字符串的形式,那肯定选第二种字符串了;临时二维码则scenen_id为1~2^10,这点需要注意,超过这个限制secen_id都是2^10-1,而且蛋疼的是,临时二维码会有过期时间需要维护这个二维码。为了方便我们的业务逻辑查询,我添加了一个 action_type
的字段,来做业务上的区别,方便查询。
class QRCode(models.Model): TEMP_QRCODE_UPDATE_DAYS = 7 QR_SCENE = 'QR_SCENE' QR_LIMIT_SCENE = 'QR_LIMIT_SCENE' QR_LIMIT_STR_SCENE = 'QR_LIMIT_STR_SCENE' ACTION_NAME_CHOICES = ( (QR_SCENE, QR_SCENE), (QR_LIMIT_SCENE, QR_LIMIT_SCENE), (QR_LIMIT_STR_SCENE, QR_LIMIT_STR_SCENE), ) url = models.URLField(blank=True, max_length=255, default='') # QR_SCENE时上限为2**32 scene_id = models.CharField(blank=True, max_length=255, db_index=True, default='') update_time = models.DateTimeField(blank=True, null=True) action_name = models.CharField(max_length=30, choices=ACTION_NAME_CHOICES, default=QR_SCENE, db_index=True) action_type = models.CharField(max_length=255, default='', db_index=True) @classmethod def get_qrcode(cls, action_name, scene_id, action_type=None): now = timezone.now() qrcode = None try: qrcode = cls.objects.get(action_name=action_name, scene_id=scene_id) # 临时二维码判断是否过期 if qrcode.action_name == cls.QR_SCENE: if qrcode.update_time and qrcode.url: _delta = now - qrcode.update_time if _delta.days < qrcode.TEMP_QRCODE_UPDATE_DAYS: return qrcode else: return qrcode except cls.DoesNotExist: pass if not qrcode: qrcode = cls( action_name=action_name, scene_id=scene_id, action_type=action_type) qrcode.update_time = now if action_name == cls.QR_SCENE: qrcode.url = create_temp_qrcode(scene_id) else: qrcode.url = create_permanent_qrcode(scene_id) qrcode.save() return qrcode @classmethod def generate_temp_scene_id(cls, obj_id): '''max id: 2 ** 32 = 4294967296''' return int('{}{}{}'.format(randint(1, 3), obj_id, uuid4().int)[:9]) @property def qrcode_url(self): if not self.action_name or not self.scene_id: raise Exception(u'qrcode object must have action_name and scene_id value') now = timezone.now() # 永久化的二维码不必更新 if self.action_name != self.QR_SCENE: if not self.url: self.update_time = now self.url = create_permanent_qrcode(self.scene_id) self.save() return self.url # 临时二维码判断是否过期 if self.update_time and self.url: _delta = now - self.update_time if _delta.days < self.TEMP_QRCODE_UPDATE_DAYS: return self.url self.update_time = now self.url = create_temp_qrcode(self.scene_id) self.save() return self.url