托管微信公众号&托管小程序

托管微信公众号&托管小程序

给公众号创建新小程序,利用新创建的小程序给公众号发送模板消息

前期准备工作

1:注册第三方平台账号,且经过开发者认证(得交钱)https://open.weixin.qq.com/
2:在第三方平台配置公众号&小程序权限,IP白名单
3:配置登录授权发起页域名(需要与本地访问域名相同)如果在本地开发调试需要进行内网穿透
4:绑定一个作为模板的小程序,作为一个母版,其他快速创建的小程序将复制这个小程序的所有源码
5:如果需要配置ext.json文件则需要在为创建小程序上传代码的时候进行ext.json填写

配置第三方平台

在这里插入图片描述注册账号完成之后创建第三方平台,选择平台型
在这里插入图片描述勾选权限完成之后:
在这里插入图片描述
这里非常重要,务必配置清晰,不然接收不到推送的回调
在这里插入图片描述

确认配置完成之后进行审核在这里插入图片描述
到了这里,第三方平台配置就完成了

公众号托管

大致的流程图如下
在这里插入图片描述

1.验证票据(component_verify_ticket),在第三方平台创建审核通过后,微信服务器会向其
“授权事件接收URL” 每隔 10 分钟以 POST 的方式推送 component_verify_ticket, component_verify_ticket 的有效时间为12 小时 每个 ticket 会发 6 次,然后更换新的 ticket

	def api_start_push_ticket(self, app_id=None, secret=None):
        """
        启动票据推送服务 —— 第三方平台调用凭证 /启动票据推送服务
        启动ticket推送服务, 推送 ticket
        https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/ticket-token/startPushTicket.html
        请求参数说明
        参数                        类型    必填     说明
        --------------------        ------  ------   ---------------------
        component_appid             string  是       平台型第三方平台的 app id
        component_secret            string  是       平台型第三方平台的 APP SECRET
        结果参数说明
        {"errcode": 0, "errmsg": "ok"}
        """
        params = dict(component_appid=app_id or self.app_id, component_secret=secret or self.secret)
        url = f'/cgi-bin/component/api_start_push_ticket'
        code, res = self.core.request(url, RequestType.POST, params, force_data=True)
        errcode = res.get('errcode')
        if errcode == 0:
            logger.debug('------启动ticket推送服务 成功')
        else:
            logger.warning(f'------启动ticket推送服务 失败。 res={res}')
    class WXBizMsgCrypt(object):
    # 构造函数
    # @param token: 公众平台上,开发者设置的Token
    # @param encoding_aes_key: 公众平台上,开发者设置的EncodingAESKey
    # @param appid: 企业号的AppId
	    def __init__(self, token, encoding_aes_key, appid):
	        try:
	            self.key = base64.b64decode(encoding_aes_key+"=")
	            assert len(self.key) == 32
	        except Exception as e:
	            print(e)
	            throw_exception("[error]: EncodingAESKey invalid !", FormatException)
	            # return error.WXBizMsgCrypt_IllegalAesKey)
	        self.token = token.encode()
	        self.appid = appid.encode()
	
	    def encrypt_msg(self, reply_msg, nonce, timestamp=None):
	        # 将公众号回复用户的消息加密打包
	        # @param reply_msg: 企业号待回复用户的消息,xml格式的字符串
	        # @param nonce: 随机串,可以自己生成,也可以用URL参数的nonce
	        # @param timestamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
	        # encrypt_msg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
	        # return:成功0,encrypt_msg,失败返回对应的错误码None
	        pc = ProcessCrypt(self.key)
	        ret, encrypt = pc.encrypt(reply_msg, self.appid)
	        if ret != 0:
	            return ret, None
	        if timestamp is None:
	            timestamp = str(int(time.time()))
	        # 生成安全签名
	        sha1 = SHA1()
	        ret, signature = sha1.get_sha1(self.token, timestamp, nonce, encrypt)
	        if ret != 0:
	            return ret, None
	        xml_parse = XMLParse()
	        return ret, xml_parse.generate(encrypt, signature, timestamp, nonce)
	
	    def decrypt_msg(self, post_data, msg_signature, timestamp, nonce):
	        # 检验消息的真实性,并且获取解密后的明文
	        # @param post_data: 密文,对应POST请求的数据
	        # @param msg_signature: 签名串,对应URL参数的msg_signature
	        # @param timestamp: 时间戳,对应URL参数的timestamp
	        # @param nonce: 随机串,对应URL参数的nonce
	        # @return: 成功0,失败返回对应的错误码
	        # xml_content: 解密后的原文,当return返回0时有效
	        # 验证安全签名
	        xml_parse = XMLParse()
	        ret, encrypt, to_user_name = xml_parse.extract(post_data)
	        if ret != 0:
	            return ret, None
	        sha1 = SHA1()
	        ret, signature = sha1.get_sha1(self.token, timestamp, nonce, encrypt)
	        if ret != 0:
	            return ret, None
	        if not signature == msg_signature:
	            return error.WXBizMsgCrypt_ValidateSignature_Error, None
	        pc = ProcessCrypt(self.key)
	        ret, xml_content = pc.decrypt(encrypt, self.appid)
	        return ret, xml_content
        
	def auth_info():
    """ 授权事件接收 URL, 用于接收微信服务器发送的相关通知。1,验证票据 2,授权变更通知推送 """
    logger.debug('接收授权事件')
    timestamp = request.args.get('timestamp')
    nonce = request.args.get('nonce')
    msg_sign = request.args.get('msg_signature')

    decrypt_test = WXBizMsgCrypt(开发者设置的Token, 第三方平台EncodingAESKey, 第三方平台APP_ID)
    ret, decrypt_xml = decrypt_test.decrypt_msg(request.data, msg_sign, timestamp, nonce)
    if not decrypt_xml:
        return 'success'
    data = WxPay.str_to_dict(decrypt_xml)
    info_type = data.get('InfoType')
    if info_type == 'component_verify_ticket':
        logger.debug('接收验证票据')
        x = int(time.time()) - int(timestamp)
        expires_in = 12 * 3600 - x
        cache.set(RD_COMPONENT_VERIFY_TICKET, data['ComponentVerifyTicket'], ex=expires_in)

        # ticket 有效时间不足一小时,启动ticket推送服务,更新 ticket
        if expires_in < 3600:
            jd_open.api_start_push_ticket()
    elif info_type == 'unauthorized':
        logger.debug('取消授权')
    elif info_type == 'updateauthorized':
        logger.debug('更新授权')
    elif info_type == 'authorized':
        logger.debug('授权成功')
    else:
        logger.debug(f'授权事件 未知类型,info_type: {info_type}')

    return 'success'

2.获取接口调用凭据

    def get_component_access_token(cls):
        """
        获取 第三方平台接口 令牌。令牌(component_access_token)是第三方平台接口的调用凭据。
        """
        component_verify_ticket = r.get(component_verify_ticket)
        expires_in = r.ttl(component_verify_ticket)
        expires_in = expires_in if expires_in else 0
        if component_verify_ticket is None or expires_in < 3600:
            _open.api_start_push_ticket()

        res = _open.api_component_token(component_verify_ticket)
        return res

3.获取预授权码

def auth_url(company_id, token_web, status):
    """ 公众号/小程序 授权链接 不能直接使用 url 跳转,必须有点击事件
    参考页面:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/Authorization_Process_Technical_Description.html
    """
    pre_auth_code = WxThirdApi.pre_auth_code().get('pre_auth_code')
    redirect_uri = CYT_DOMAIN_NAME + f'/mp_api/auth_callback/{company_id}/{status}'

    auth_type = 3  # 要授权的帐号类型:1 则商户点击链接后,手机端仅展示公众号、2 表示仅展示小程序,3 表示公众号和小程序都展示。
    url = f'https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid={APP_ID_API}' \
        f'&pre_auth_code={pre_auth_code}&redirect_uri={redirect_uri}&auth_type={auth_type}'
    return redirect(url)
def pre_auth_code(cls):
    """ 获取预授权码
    预授权码(pre_auth_code)是第三方平台方实现授权托管的必备信息,每个预授权码有效期为 1800秒。
    获取预授权码是没有请求限制次数的,所以每一次进行网页授权的时候请求新的授权码。预授权码不要重复使用,
    每次授权码的过期时间是600s也就是说600s内一个授权码只能供一个用户进行公众号授权。

    结果参数说明
    参数                类型        说明
    ---------------     --------    -------------------
    pre_auth_code       string      预授权码
    expires_in          number      有效期,单位:秒
    """
    token = get_access_token_for_api()[0]
    data = dict(component_appid=微信第三方平台app_id, )
    url = f'https://api.weixin.qq.com/cgi-bin/component/api_create_preauthcode?component_access_token={token}'
    res = cls.__regular_post(url, data)
    return res

获取预授权码之后会有一个下面的界面,这里需要需要托管的公众号进行扫码授权
在这里插入图片描述

4.在手机上扫码授权完成之后会接收到一个回调用来获取被托管的公众号信息

def add_categories(cls, app_id):
    """ 新增类目 教育信息服务"""
    auth_token = AuthAccessToken()
    token = auth_token.get(app_id)

    if token is None:
        logger.debug(f'authorizer_access_token 不存在,app_id: {app_id}')
        return {}

    url = f'https://api.weixin.qq.com/cgi-bin/wxopen/addcategory?access_token={token}'
    data = dict(categories=[dict(first=8, second=1089, certicates=[])])  # 新增类目 教育信息服务 素质教育
    res = cls.__regular_post(url, data)
    return res
def create_templates(gzh_app_id, company_id):
    """ 批量添加 公众号 模板ID"""
    # 查询公众号所属行业
    res = WxThirdApi.template_get_industry(gzh_app_id)
    if res.get('primary_industry', {}).get('first_class') != '教育':  # 没有设置所属行业为 教育
        # 设置公众号所属行业
        res = WxThirdApi.template_set_industry(gzh_app_id)
        if res.get('errcode') != 0:
            logger.debug(f'设置公众号所属行业 失败:{res}')
            return
    obj = dict(company_list=[company_id])
    ret = WxThirdApi.templates_update(obj)
    return ret
def templates_update(obj):
   """ 批量添加,修改,删除 公众号 模板ID"""
   logger.debug(f'templates_update, 输入参数:{obj}')
   company_ids = obj.get('company_list')  # 公司ID
   if not company_ids:
       return {'code': ERR_PARAM_WANTED, 'msg': ERR_PARAM_WANTED_MSG % '公司ID'}
   ou_info = WxOfficeUser.query.filter(WxOfficeUser.company_id.in_(company_ids))\
       .filter_by(gzh_authorized=1, mp_authorized=1) \
       .join(DanceCompany, DanceCompany.id == WxOfficeUser.company_id) \
       .add_columns(DanceCompany.company_name).all()

   errors = []
   for ou in ou_info:
       a = ou[0]
       ret = WxThirdApi.template_get(a.gzh_app_id)  # 查询该公众号下拥有的模板消息列表
       logger.debug(f"查询公众号模板返回:{ret}")
       if 'template_list' not in ret:
           errors.append(f"[{ou.company_name}][获取模板列表失败!]")
           logger.warning(f"获取模板列表失败!!! [{ou.company_name}]")
           continue
       gzh_template_ids = [k['template_id'] for k in ret['template_list']]  # 该公众号实际拥有模板ID
       logger.debug(f'{ou.company_name} 机构公众号实际拥有消息模板:{gzh_template_ids}')

       db_template = WxTemplate.all(company_id=a.company_id)  # 已创建的 模板类型
       logger.debug(f'{ou.company_name} 机构公众号数据库拥有消息模板:{db_template}')
       db_temp_ids = [k.template_id for k in db_template]  # 数据库内模板ID

       # 删除模板信息
       to_del = list(set(db_temp_ids).difference(set(gzh_template_ids)))  # 公众号未拥有  数据库内存在的模板
       to_del_tx = list(set(gzh_template_ids).difference(set(db_temp_ids)))  # 公众号已拥有  数据库内不存在的模板

       logger.debug(f'{ou.company_name} 需要删除的【数据库】模板信息:{to_del}')
       logger.debug(f'{ou.company_name} 需要删除的【公众号】模板信息:{to_del_tx}')

       for d in to_del_tx:
           ret = WxThirdApi.template_delete(a.gzh_app_id, d)  # 删除单个模板
           if ret["errcode"] != 0:
               errors.append(f"[{ou.company_name}][模板:{d}]")
               logger.debug(f'{ou.company_name}模板删除 失败:{ret}')
       cnt = WxTemplate.query.filter_by(company_id=a.company_id)\
           .filter(WxTemplate.template_id.in_(to_del)).delete(synchronize_session=False)
       logger.debug(f"[ {cnt} ] 条模板记录 待删除! [{ou.company_name}]")

       # 新增模板信息
       cur_template = WxTemplate.all(company_id=a.company_id)  # 已创建的 模板类型
       template_types = [te.type for te in cur_template]
       logger.debug(f'{ou.company_name}机构创建的模板类型:{template_types}')
       
       for ty, val in template_dict.items():  # template_dict 财艺通公众号消息模板
           template_id_short = val[0]  # template id: (编号, 模板ID, 标题)
           if ty in template_types or ty == WxTemplate.T_GROUP_PROCESS:    # 财艺招小程序模板不能添加到公众号
               continue
           # 公众号新增模板
           ret = WxThirdApi.template_add(a.gzh_app_id, template_id_short)  # 公众号ID  模板消息编号
           if 'template_id' in ret:
               data = dict(company_id=a.company_id, type=ty,
                           template_no=template_id_short, template_id=ret['template_id'])
               tp = WxTemplate.first(type=ty, company_id=a.company_id)
               if tp:
                   tp.update(data)
                   db.session.add(tp)
               else:
                   db.session.add(WxTemplate(data))
               logger.debug(f'{ou.company_name}模板新增 成功:{ret}')
           else:
               errors.append(f"[{ou.company_name}][模板类型:{ty}, 编号:{template_id_short}]")
               logger.debug(f'{ou.company_name}模板新增 失败!!!:{ret}')
   msg = f"【{''.join(errors)}】" if errors else ""
   
   db.session.commit()
   return {'code': ERR_SUCCESS, 'msg': MSG_WX_TEMPLATE_UPDATE_SUCCESS % msg}
def create_templates(gzh_app_id, company_id):
    """ 批量添加 公众号 模板ID"""
    # 查询公众号所属行业
    res = WxThirdApi.template_get_industry(gzh_app_id)
    if res.get('primary_industry', {}).get('first_class') != '教育':  # 没有设置所属行业为 教育
        # 设置公众号所属行业
        res = WxThirdApi.template_set_industry(gzh_app_id)
        if res.get('errcode') != 0:
            logger.debug(f'设置公众号所属行业 失败:{res}')
            return
    obj = dict(company_list=[company_id])
    ret = WxThirdApi.templates_update(obj)
    return ret
def delete_menu(cls, gzh_app_id):
    """ 公众号菜单取消托管 删除创建菜单  """
    token = AuthAccessToken().get(gzh_app_id)
    url = f'https://api.weixin.qq.com/cgi-bin/menu/delete?access_token={token}'
    response = requests.get(url=url)
    res = json.loads(response.content)
    return res
def menu_create(cls, gzh_app_id, mp_app_id=None):
    """ 创建 公众号 自定义菜单 """
    token = cls.__get_access_token(gzh_app_id)
    if token is None:
        logger.warning(f'authorizer_access_token 不存在,app_id: {gzh_app_id}')
        return {}

    # 第一个菜单
    button_first = dict(type='view', name='我的主页', url='https://caiyitong1.com/api/wx/index')

    # 公众号关联的小程序
    if mp_app_id:
        button_first.update(dict(type='miniprogram', name='我的主页', appid=mp_app_id,
                                 pagepath='pages/tabbar/index'))

    data = {'button': [  # 一级菜单
        button_first
    ]}
    oa = OfficialAccount()
    res = oa.menu.menu_create(data, token)
    return res
def authorizer_info(cls, app_id):
    """ 
    获取授权方的帐号基本信息 公众号和小程序的接口返回结果不一样
    """
    token = get_access_token_for_api()[0]
    if token is None:
        logger.warning(f'第三方平台 component_access_token 不存在')
        return {}

    data = dict(component_appid=APP_ID_API, authorizer_appid=app_id)
    url = f'https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_info?component_access_token={token}'
    res = cls.__regular_post(url, data)

    return res
def api_query_auth(cls, auth_code):
	 """ 使用授权码获取授权信息
	 当用户在第三方平台授权页中完成授权流程后,第三方平台开发者可以在回调 URI 中通过 URL 参数获取授权码
	 。使用以下接口可以换取公众号/小程序的授权信息。
	 https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/ThirdParty/token/authorization_info.html
	 """
	 token = get_access_token_for_api()[0]
	 data = dict(component_appid=APP_ID_API, authorization_code=auth_code)
	 url = f'https://api.weixin.qq.com/cgi-bin/component/api_query_auth?component_access_token={token}'
	 res = cls.__regular_post(url, data)
	 return res
def auth_callback(company_id, status):
    """ 公众号管理员扫码授权回调URL """
    logger.debug(f'公众号管理员扫码授权回调, company_id:  {company_id}')
    auth_code = request.args.get('auth_code')

    success_url = '/html/platform_3rd/bind_success.html?code=%s&msg=%s&name=%s'
    error_url = '/html/platform_3rd/bind_error.html?code=%s&msg=%s'

    # 获取扫码后的授权信息
    res = WxThirdApi.api_query_auth(auth_code)

    if 'authorization_info' not in res:
        return redirect(error_url % (ERR_WX_THIRD_GET_INFO, ERR_WX_THIRD_GET_INFO_MSG))
    else:
        authorization_info = res['authorization_info']
        logger.debug(f'authorization_info={authorization_info}')

        app_id = authorization_info.get('authorizer_appid')
        ret = WxThirdApi.authorizer_info(app_id)

        # 获取 app_id 对应的帐号信息
        authorizer_info = ret.get('authorizer_info')
        if not authorizer_info:
            return redirect(error_url % (ERR_WX_THIRD_GET_INFO, ERR_WX_THIRD_GET_INFO_MSG))

        # 判断 app_id 是否为公众号
        if 'MiniProgramInfo' in authorizer_info:  # 小程序
            return redirect(error_url % (ERR_WX_THIRD_NOT_MP, ERR_WX_THIRD_NOT_MP_MSG))

        # 判断是否通过微信认证
        verify_type_id = authorizer_info['verify_type_info']['id']
        if verify_type_id != 0:
            return redirect(error_url % (ERR_WX_THIRD_UNAUTHORIZED, ERR_WX_THIRD_UNAUTHORIZED_MSG))

        # 保存 access_token
        auth_token = AuthAccessToken()
        auth_token.save_access_token_from_auth_info(authorization_info, app_id)

        # 公众号不能重复授权
        gzh = WxOfficeUser.first(gzh_app_id=app_id)
        if gzh and gzh.gzh_authorized:
            msg = '公众号已绑定机构,无需重复授权'
            return redirect(success_url % (ERR_SUCCESS, msg, authorizer_info.get('nick_name')))

        if status == 'true':
            res = WxThirdApi.menu_create(app_id, mp_app_id)  # 创建菜单  ======================
            logger.debug(f'创建 公众号 自定义菜单,res:{res}')
        else:
            res = WxThirdApi.delete_menu(app_id)  # 取消菜单托管删除创建菜单  ======================
            logger.debug(f'删除创建的公众号菜单,res:{res}')

        res = WxThirdApi.create_templates(app_id, company_id)  # 添加消息模板  ======================
        logger.debug(f'添加消息模板,res:{res}')
        
    db.session.commit()
    return redirect(success_url % (ERR_SUCCESS, MSG_WX_AUTHORIZATION_SUCCESS, authorizer_info.get('nick_name')))

5.公众号托管完成

使用公众号快速创建小程序

注意!!!!!!!!!!!!!每个月最多创建五个,创建完成就没了,啥办法也没有新增,谨慎创建
大致流程图如下
在这里插入图片描述
1.复用公众号主体快速注册小程序

def fast_register_mp(company_id, token_web):
   """ 复用公众号主体快速注册小程序 """
   redirect_uri = CYT_DOMAIN_NAME + f'/mp_api/register_mp_callback/{gzh_app_id}'
   copy_wx_verify = 1  # 是否复用公众号的资质进行微信认证(1:申请复用资质进行微信 认证 0:不申请)
   params = dict(appid=gzh_app_id, component_appid=APP_ID_API, copy_wx_verify=copy_wx_verify,
                 redirect_uri=redirect_uri)
   url = 'https://mp.weixin.qq.com/cgi-bin/fastregisterauth?' + urllib.parse.urlencode(params)
   return redirect(url)

用户扫码授权确认快速创建小程序

2.回调快速创建小程序

def register_mp_callback(gzh_app_id):
    """ 公众号管理员扫码确认授权注册小程序,并跳转回第三方平台 """
    logger.debug('复用公众号主体快速注册小程序 授权回调')
    logger.debug(f'args: {request.args}')
    ticket = request.args.get('ticket')
    error_url = '/html/platform_3rd/bind_error.html?code=%s&msg=%s'
    success_url = '/html/platform_3rd/bind_success.html?code=%s&msg=%s&name=%s'
    resp = WxThirdApi.mp_linked(gzh_app_id)  # 查询公众号关联的所有小程序
    logger.debug(f'【查询公众号关联的所有小程序】: {resp}')
    mp_app_id = None
    mp_origin_id = None
    if resp['errcode'] == 0:
        items = resp.get('wxopens', {}).get('items') or []
        logger.debug(f'公众号关联的小程序:res={items}')
        for i in items:
            # 没有上线的小程序 昵称和原始名称是一样的
            if i.get('nickname') == i.get('username') and i.get('released') == 0:
                mp_app_id = i.get('appid')
    if mp_app_id is None:  # 公众号未创建过新的小程序
        res = WxThirdApi.mp_register(gzh_app_id, ticket)  # 跳转至第三方平台,第三方平台调用快速注册 API 完成注册
        if res['errcode'] == 0:
            logger.debug(f'【第三方平台注册】:res={res}')

            verify_success = res.get('is_wx_verify_succ')
            if verify_success is not True and verify_success != 'true':
                msg = '复用公众号微信认证小程序失败!'
                logger.debug(f'{msg}, ret={res}')
                return redirect(error_url % (ERR_WX_THIRD_MP_FOUND, msg))

            link_success = res.get('is_link_succ')
            if link_success is not True and link_success != 'true':
                msg = '注册的小程序和公众号关联失败!'
                logger.debug(f'{msg}, ret={res}')
                return redirect(error_url % (ERR_WX_THIRD_MP_FOUND, msg))
            mp_app_id = res['appid']
            # 获取并存储 access_token
            auth_code = res['authorization_code']
            ret = WxThirdApi.api_query_auth(auth_code)
            logger.debug(f'创建小程序成功,使用授权码获取信息: {ret}')
        elif res['errcode'] == 61058:  # 腾讯服务器返回超时
            resp = WxThirdApi.mp_linked(gzh_app_id)
            items = resp.get('wxopens', {}).get('items') or []
            logger.debug(f'公众号关联的小程序:res={items}')
            for i in items:
                if i.get('nickname') == i.get('username') and i.get('released') == 0:
                    mp_app_id = i.get('appid')
        else:
            msg = '复用公众号主体快速注册小程序 失败'
            logger.debug(f'{msg}, ret={res}')
            return redirect(error_url % (ERR_WX_THIRD_MP_FOUND, msg))

    ret = WxThirdApi.authorizer_info(mp_app_id)  # 获取小程序基本信息
    logger.debug(f'小程序基本信息: {ret}')
    if 'authorization_info' not in ret:
        msg = '获取 小程序授权信息 失败'
        logger.debug(f'{msg}, ret={ret}')
    else:
        mp_origin_id = ret['authorizer_info']['user_name']  # 小程序原始名称
        auth_token = AuthAccessToken()
        auth_token.get(mp_app_id)  # 获取接口调用令牌
    gzh = WxOfficeUser.first(gzh_app_id=gzh_app_id, gzh_authorized=1)
    if gzh is None:
        msg = '注册小程序的公众号已解绑'
        logger.debug(f'{msg}')
        return redirect(error_url % (ERR_WX_THIRD_MP_FOUND, msg))

    menu_status = gzh.menu_status
    gzh.mp_app_id = mp_app_id
    gzh.mp_origin_id = mp_origin_id
    db.session.add(gzh)

    # 设置小程序类目
    ret = WxThirdApi.add_categories(mp_app_id)
    if ret['errcode'] != 0:
        return redirect(error_url % (ERR_WX_THIRD_MP_FOUND, ret))
    logger.debug('设置小程序类目成功')
    WxThirdApi.mp_modify_domain_directly(mp_app_id)
    logger.debug('设置小程序域名成功')
    if menu_status == 1:
        res = WxThirdApi.menu_create(gzh_app_id, mp_app_id)  # 更新公众号菜单
        logger.debug(f'更新 公众号 自定义菜单,res:{res}')

    """ 使用 公众号 创建开放平台帐号,然后绑定小程序"""
    # 查询是否绑定开放平台账号
    res = WxThirdApi.get_open_account(gzh.gzh_app_id)
    if res['errcode'] != 0:
        # 没有开放平台账号,创建开放平台帐号并绑定公众号
        res = WxThirdApi.open_create(gzh_app_id=gzh.gzh_app_id)
        logger.debug(f'创建开放平台帐号 成功 {res}')
    gzh.open_app_id = res['open_appid']  # 联合ID
    gzh.trusteeship_time = datetime.datetime.now()  # 授权时间
    gzh.mp_authorized = 1  # 小程序状态改为授权
    db.session.add(gzh)

    # 将小程序绑定到开放平台帐号下
    ret = WxThirdApi.open_bind(gzh.mp_app_id, gzh.open_app_id)
    if ret.get('errcode') == 0:
        logger.debug('小程序绑定到开放平台帐号 成功')

    # 公众号托管成功后,获取用户信息。 要放到 创建开放平台之后,以便能获取 union id
    jd_thread_pool.add_job(WxThirdApi.get_gzh_users, gzh_app_id)

    db.session.commit()
    return redirect(success_url % (ERR_SUCCESS, MSG_MP_FOUND_SUCCESS, gzh.nick_name))
def mp_register(cls, gzh_app_id, ticket):
    """ 跳转至第三方平台,第三方平台调用快速注册 API 完成注册

    返回参数:
    参数                说明
    -----------         ----------------------------
    errcode             错误码
    errmsg              错误信息
    appid               新创建小程序的 appid
    authorization_code  新创建小程序的授权码
    is_wx_verify_succ   复用公众号微信认证小程序是否成功
    is_link_succ        小程序是否和公众号关联成功

    示例:
    {
      "errcode": 0,
      "errmsg": "",
      "appid": "wxe5f52902cf4de896",
      "authorization_code": "****",
      "is_wx_verify_succ": "true",
      "is_link_succ": "true"
    }
    """
    token = cls.__get_access_token(gzh_app_id)
    data = dict(ticket=ticket, )
    url = f'https://api.weixin.qq.com/cgi-bin/account/fastregister?access_token={token}'
    res = cls.__regular_post(url, data)
    return res
def mp_linked(cls, gzh_app_id):
    """ 获取公众号关联的小程序 """
    token = cls.__get_access_token(gzh_app_id)
    data = dict()
    url = f'https://api.weixin.qq.com/cgi-bin/wxopen/wxamplinkget?access_token={token}'
    res = cls.__regular_post(url, data)
    return res
def mp_modify_domain_directly(cls, mp_app_id):
    """ 快速设置小程序服务器域名  业务域名 """
    token = cls.__get_access_token(mp_app_id)
    data = dict(
              action='add',
              requestdomain=['**************自己的域名'],
              wsrequestdomain=['**************自己的域名'],
              uploaddomain=['**************自己的域名'],
              downloaddomain=['**************自己的域名'],
              udpdomain=[],
              tcpdomain=[]
            )
    url = f'https://api.weixin.qq.com/wxa/modify_domain_directly?access_token={token}'
    res = cls.__regular_post(url, data)
    if res['errcode'] != 0:
        return res
    #  新增效验文件
    url = f'https://api.weixin.qq.com/wxa/get_webviewdomain_confirmfile?access_token={token}'
    data = dict()
    res = cls.__regular_post(url, data)
    file_name = res['file_name']
    file_content = res['file_content']
    new_path = os.path.join(basedir, 'app/static/' + file_name)
    if os.path.isfile(new_path):  # 文件存在时删除,不存在时忽略
        os.remove(new_path)
    files = open(new_path, 'w')
    files.write(file_content)

    data = dict(
              action='add',
              webviewdomain=['**************自己的域名']
            )
    url = f'https://api.weixin.qq.com/wxa/setwebviewdomain_directly?access_token={token}'
    res = cls.__regular_post(url, data)
    return res
    def authorizer_list(cls):
        """ 拉取所有已授权的帐号列表 """
        token = get_access_token_for_api()[0]
        if token is None:
            logger.warning(f'第三方平台 component_access_token 不存在')
            return {}

        data = dict(component_appid=APP_ID_API, offset=0, count=500)  # 每次最多拉取 500 个
        url = f'https://api.weixin.qq.com/cgi-bin/component/api_get_authorizer_list?component_access_token={token}'
        res = cls.__regular_post(url, data)
        return res

    def open_create(cls, gzh_app_id):
        """ 创建开放平台帐号并绑定公众号 """
        token = cls.__get_access_token(gzh_app_id)
        if token is None:
            logger.warning(f'authorizer_access_token 不存在,app_id: {gzh_app_id}')
            return {}

        data = dict(appid=gzh_app_id, )
        url = f'https://api.weixin.qq.com/cgi-bin/open/create?access_token={token}'
        res = cls.__regular_post(url, data)
        return res

    def open_bind(cls, mp_app_id, open_app_id):
        """ 将小程序绑定到开放平台帐号下 """
        token = cls.__get_access_token(mp_app_id)
        if token is None:
            logger.warning(f'authorizer_access_token 不存在,app_id: {mp_app_id}')
            return {}

        data = dict(appid=mp_app_id, open_appid=open_app_id, )
        url = f'https://api.weixin.qq.com/cgi-bin/open/bind?access_token={token}'
        res = cls.__regular_post(url, data)
        return res

3.已经成功创建了一个新的小程序,可以通过登录小程序助手查看,新创建的小程序没有名称,头像,简介,需要设置才能上传代码,审核上线

小程序信息设置

这里将会设置小程序名称,头像,简介,以及代码上传
在这里插入图片描述

def modify_mp_info():
    """ 小程序信息设置 """
    obj = get_request_obj_ex()
    com = DanceCompany.query.filter_by(id=obj.get('company_id')).first()
    if not com:
        return jsonify({'code': ERR_PUB_NOT_EXIST, 'msg': ERR_PUB_NOT_EXIST_MSG % f"公司ID={obj.get('company_id')}"})
    company_id = com.id
    
    rec = WxOfficeUser.query.filter_by(company_id=company_id).first()
    if not rec:
        return jsonify({'code': ERR_WX_THIRD_MP_NAME, 'msg': ERR_WX_THIRD_MP_NAME_MSG})
    ret = JdCheck.verify(
        obj, int=dict(gzh_status='公众号状态, d2, in[0,1,2]', mp_status='小程序状态, d2, in[0,1,2]',
                      mp_examine_status='小程序审核状态, d0, in[0,1,2,3]'),
        str=dict(nickname='小程序名称, 1-15', content='小程序简介, 2-60', media_id_img='小程序logo, 1-200',
                 media_id_name='小程序营业执照, 1-200')
    )
    if ret['code'] != 0:
        return jsonify(ret)
    mp_app_id = rec.mp_app_id
    media_id_img = obj['media_id_img']  # 小程序logo
    media_id_name = obj['media_id_name']  # 营业执照图片
    nickname = obj['nickname']  # 小程序名称
    content = obj['content']  # 小程序简介

    av = Avatar(M_MP_LOGO, company_id)
    filepath = av.file_path(media_id_img)
    # 判断文件是否存在
    if not os.path.exists(filepath):
        return jsonify({'code': ERR_LOGO_NEED_FILE, 'msg': ERR_LOGO_NEED_FILE_MSG})
    # 小程序logo图片大小不能超过2M
    if os.path.getsize(filepath) > 2 * 1024 * 1024:
        return jsonify({'code': ERR_LOGO_WRONG_SIZE, 'msg': ERR_LOGO_WRONG_SIZE_MSG})

    av = Avatar(M_LICENSE, company_id)
    filepath = av.file_path(media_id_name)
    # 判断文件是否存在
    if not os.path.exists(filepath):
        return jsonify({'code': ERR_LOGO_NEED_FILE, 'msg': ERR_LOGO_NEED_FILE_MSG})
    # 营业执照图片大小不能超过2M
    if os.path.getsize(filepath) > 2 * 1024 * 1024:
        return jsonify({'code': ERR_LOGO_WRONG_SIZE, 'msg': ERR_LOGO_WRONG_SIZE_MSG})

    ret = WxThirdApi.mp_authentication_name(mp_app_id, nickname)  # 小程序名称认证
    if ret['errcode'] != 0:
        return jsonify({'code': ret['errcode'], 'msg': f'小程序名称认证错误{ret}'})

    if not com.company_mp_logo or com.company_mp_logo != media_id_img:
        ret = WxThirdApi.mp_set_head_image(mp_app_id, media_id_img)  # 修改头像
        # 小程序头像上传成功后进行存储
        if ret['errcode'] == 0:
            com.update(dict(company_mp_logo=media_id_img))
            db.session.add(com)
            db.session.commit()     # 实时更新 头像
        elif ret['errcode'] == 53202:
            logger.debug(f"头像修改超过次数================{ret}")
            pass    # 头像修改超过次数
        else:
            return jsonify({'code': ret['errcode'], 'msg': f'小程序修改头像错误{ret}'})

    if not com.company_mp_name or com.company_mp_name != nickname:
        ret = WxThirdApi.mp_set_name(mp_app_id, nickname, media_id_name)  # 修改名称
        # 小程序名称设置成功后进行存储
        if ret['errcode'] == 0:
            com.update(dict(company_mp_name=nickname))
            db.session.add(com)
            db.session.commit()
        else:
            return jsonify({'code': ret['errcode'], 'msg': f'小程序修改名称错误{ret}'})

    if not com.mp_content or com.mp_content != content:
        res = WxThirdApi.mp_set_signature(mp_app_id, content)  # 修改简介
        # 小程序简介设置成功后进行存储
        if res['errcode'] == 0:
            com.update(dict(mp_content=content))
            db.session.add(com)
            db.session.commit()
        else:
            return jsonify({'code': res['errcode'], 'msg': f'小程序修改简介错误{res}'})

    if 'audit_id' in ret:
        return jsonify({'code': ERR_SUCCESS, 'msg': '名称审核单ID%s' % ret['audit_id']})

    # 查询最新版本号
    res = WxThirdApi.mp_template_get()
    template_list = res.get('template_list')
    if not template_list:
        logger.warning(f"托管公众号时,模板库没有最新模板!!!")
    else:
        # 最新版本号
        last_template_id = max(list(map(int, [i['template_id'] for i in template_list])))
        logger.debug(f'获取版本列表:{template_list}, 最新模板版本号:{last_template_id}')
    
        for template in template_list:
            if template['template_id'] == last_template_id:
                template_id = template['template_id']
                user_version = template['user_version']
                user_desc = template['user_desc']
                # 小程序信息修改完成之后应用最新代码模板
                res = WxThirdApi.mp_template_add(mp_app_id, template_id, user_version, user_desc, company_id)
                if res.get('errcode') == 0:  # 上传成功
                    rec.update(dict(template_status=WxOfficeUser.S_UPLOAD, new_version=user_version))
                else:
                    logger.warning(f'上传小程序代码 失败:res={res}')
        db.session.add(rec)
        
    db.session.commit()
    return jsonify({'code': ERR_SUCCESS, 'msg': MSG_MP_INFO_SET_SUCCESS})
    def mp_set_name(cls, mp_app_id, nickname, img):
        """ 小程序 设置名称
        返回参数:
        参数                说明
        -----------         ----------------------------
        errcode             返回码
        errmsg              错误信息
        wording             材料说明
        audit_id            审核单 id,通过用于查询改名审核状态

        若接口未返回 audit_id,说明名称已直接设置成功,无需审核;
        """
        # 需要 组织机构代码证或营业执照 图片素材
        token = cls.__get_access_token(mp_app_id)
        ret = cls.mp_media_upload(mp_app_id, img)
        re = json.loads(ret.text)
        if 'media_id' not in re:
            logger.warning(f'小程序 营业执照 上传失败: {ret}')
            return None
        data = dict(nick_name=nickname, license=re['media_id'],)
        url = f'https://api.weixin.qq.com/wxa/setnickname?access_token={token}'
        res = cls.__regular_post(url, data)
        return res


    def mp_set_head_image(cls, mp_app_id, img):
        """ 小程序 修改头像 """
        token = cls.__get_access_token(mp_app_id)

        ret = cls.mp_media_upload(mp_app_id, img)
        res = json.loads(ret.text)
        if 'media_id' not in res:
            logger.warning(f'小程序 logo 上传失败: {res}')
            return None

        data = dict(head_img_media_id=res['media_id'], x1=0, y1=0, x2=1, y2=1)
        url = f'https://api.weixin.qq.com/cgi-bin/account/modifyheadimage?access_token={token}'
        res = cls.__regular_post(url, data)
        return res


    def mp_set_signature(cls, mp_app_id, content):
        """ 小程序 修改简介 """
        token = cls.__get_access_token(mp_app_id)

        data = dict(signature=content)
        url = f'https://api.weixin.qq.com/cgi-bin/account/modifysignature?access_token={token}'
        res = cls.__regular_post(url, data)
        # 小程序简介设置成功后进行存储
        if res['errcode'] == 0:
            info = WxOfficeUser.first(mp_app_id=mp_app_id)
            com = DanceCompany.query.filter_by(id=info.company_id).first()
            com.update(dict(mp_content=content))
            db.session.add(com)
            db.session.commit()
        return res

小程序代码审核及发布新版本


def mp_template_submit_func():
    """ 小程序代码提交审核 """

    ret = WxThirdApi.get_code_privacy_info(s.mp_app_id)
    if ret['errcode'] != 0:  # 检测未成功
        continue
    WxThirdApi.set_privacy_setting(s.mp_app_id)  # 小程序 配置小程序用户隐私保护
    res = WxThirdApi.mp_template_submit(s.mp_app_id)
   	return
   
def mp_template_submit(cls, mp_app_id):
    """ 在调用上传代码接口为小程序上传代码后,可以调用本接口,将上传的代码提交审核。"""
    token = cls.__get_access_token(mp_app_id)
    data = dict(item_list=[
            {
                'first_class': '****根据需要自行配置',
                'second_class': '****根据需要自行配置',
                'first_id': ***根据需要自行配置,
                'second_id': ***根据需要自行配置,
            }])
    url = f'https://api.weixin.qq.com/wxa/submit_audit?access_token={token}'
    res = cls.__regular_post(url, data)
    return res

    def code_release(self, access_token=None):
        """
        调用本接口可以发布最后一个审核通过的小程序代码版本。

        注意,post的 data 为空,不等于不需要传data,否则会报错【errcode: 44002 "errmsg": "empty post data"】
        https://developers.weixin.qq.com/doc/oplatform/openApi/OpenApiDoc/miniprogram-management/code-management/release.html
        请求参数:
        参数                    类型            必填        说明
        -----------------       ----------      -------     -------------------
        access_token	        string	        是	        接口调用凭证,该参数为 URL 参数,非 Body 参数。使用 authorizer_access_token

        返回参数:{"errcode": 0, "errmsg": "ok"}
        """
        params = dict()
        path = f'/wxa/release?access_token={access_token or self._p.authorizer_access_token}'
        code, res = self.core.request(path, RequestType.POST, params)
        return res

这里已经完成公众号托管及小程序快速创建

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值