假設的咱們的服務號有這麼一些功能,好比底部有按鈕,點擊會有一些複雜的功能,這時候可能就須要一個用戶系統,有用戶系統就常常想要作什麼分享邀請新用戶之類的,這時候就又有幾種方式,1.直接一個鏈接,讓其餘用戶點;2.有一個二維碼,讓離得近的朋友掃。python
藉着這個需求體會了下微信開發的兩種不一樣類型(80非80端口的兩種開發),以及python-social-auth的一些正確姿式。django
而這個需求其實就對應了兩種開發模式,好比有個需求能夠在公衆號內直接回復,或者在一個頁面裏面讓用戶提交表單等等。segmentfault
首先,理解需求:用戶在微信點擊咱們的邀請鏈接後,會引導用戶作一個有綠色按鈕的微信登陸,用戶登錄後成爲咱們的用戶,而且跳轉到某個頁面。服務器
關於用戶微信登陸的事情咱們經過python-social-auth已經解決了(參考個人上一篇博客微信公衆號開發小記——3.接入三方登陸),因此能夠直接用django的login_required裝飾器完成這種事情。微信
因爲微信號的登陸只有微信,因此LOGIN_URL = '/login/weixinapp/'
session
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端口上的服務app
難點在於這裏,微信掃碼後是直接進入公衆號的,若是你想要讓用戶進入公衆號以後就變成咱們的用戶而不是讓他在點一個東西這裏是比較蛋疼的,由於你的服務器在這時候作302微信是不認得。這就致使了幾個問題:ui
因爲不引導用戶登陸,我這裏沒辦法直接用python-social-auth裏面的do_complete方法(由於拿不到用戶的access_token),不過好處是使用微信服務器的access_token以及用戶的openid我能夠直接拿到這個用戶的用戶信息。這個問題就變成了python-social-auth的do_complete有用戶response後執行pipeline的邏輯了。url
而後我扒了下代碼,用了幾個小時從單測裏面找到了這個邏輯,具體見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