#!/bin/python #coding: utf-8 # +------------------------------------------------------------------- # | django-vue-lyadmin 专业版 # +------------------------------------------------------------------- # | Author: lybbn # +------------------------------------------------------------------- # | QQ: 1042594286 # +------------------------------------------------------------------- # | Date: 2023-10-21 # +------------------------------------------------------------------- # | version: 1.0 # +------------------------------------------------------------------- # 整理自:https://gitee.com/minibear2021/doudian/blob/master/doudian/core.py # ------------------------------ # 抖店开放平台接口 # ------------------------------ import json import requests import datetime import time from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.hashes import MD5, SHA256, Hash from cryptography.hazmat.primitives.hmac import HMAC from django.core.cache import cache import logging logger = logging.getLogger(__name__) class doudian: def __init__(self, app_key: str, app_secret: str, app_type="self", code=None, shop_id: str = None, token_file: str = None, proxy: str = None, test_mode=False): """ 1、初始化DouDian实例,自用型应用传入shop_id用于初始化access token(可选),工具型应用传入code换取access token(如初始化时未传入,可以在访问抖店API之前调用init_token(code)进行token的初始化。 2、精选联盟自研应用和普通电商后台自研应用换取token的方式不同(普通电商后台自研应用采用authorization_self形式),精选联盟自研应用使用授权code换取token,也即使用authorization_code授权形式 """ self._app_key = app_key # 应用key,长度19位数字字符串 self._app_secret = app_secret # 应用密钥 字符串 self._app_type = app_type # App类型,self=自用型应用, tool=工具型应用 # if self._app_type == "self" and not shop_id: # raise ValueError('shop_id is not assigned.') self._token_file = token_file # token缓存文件 self._shop_id = shop_id # 店铺ID,自用型应用必传。 self._code = code # 工具型应用需要传的授权码code self._proxy = proxy self._test_mode = test_mode if self._test_mode: self._gate_way = 'https://openapi-sandbox.jinritemai.com' else: self._gate_way = 'https://openapi-fxg.jinritemai.com' self._version = 2 self._token = None self._sign_method = 'hmac-sha256' if self._token_file: try: with open(self._token_file) as f: self._token = json.load(f) except Exception as e: logger.error('{}'.format(e)) # if self._app_type == "self": # self.init_token() # elif self._app_type == "tool" and code: # self.init_token(code) def getCurrentAppKey(self): return self._app_key def _sign(self, method: str, param_json: str, timestamp: str) -> str: param_pattern = 'app_key{}method{}param_json{}timestamp{}v{}'.format(self._app_key, method, param_json, timestamp, self._version) sign_pattern = '{}{}{}'.format(self._app_secret, param_pattern, self._app_secret) return self._hash_hmac(sign_pattern) def _hash_hmac(self, pattern: str) -> str: try: hmac = HMAC(key=self._app_secret.encode('UTF-8'), algorithm=SHA256(), backend=default_backend()) hmac.update(pattern.encode('UTF-8')) signature = hmac.finalize() return signature.hex() except Exception as e: return None def get_access_token(self): return self._access_token() def _access_token(self) -> str: cache_token = self.get_cache_access_token() if cache_token: return cache_token refresh_token = self.get_cache_refresh_token() if refresh_token: access_token = self.refresh_token() return access_token return None def init_token(self, code: str = '') -> bool: """初始化access token :param code: 工具型应用从授权url回调中获取到的code,自用型应用无需传入。 """ if self._app_type == "tool" and not code: raise ValueError('code is not assigned.') path = '/token/create' grant_type = 'authorization_self' if self._app_type == "self" else 'authorization_code' params = {} params.update({'code': code if code else ''}) params.update({'grant_type': grant_type}) if self._app_type == "self": if self._test_mode: params.update({'test_shop': '1'}) elif self._shop_id: params.update({'shop_id': self._shop_id}) # else: # raise ValueError('shop_id is not assigned.') result = self._request(path=path, params=params, token_request=True) if result and result.get('code') == 10000 and result.get('data'): logger.error("初始化token成功appkey=%s:返回内容:%s" % (self._app_key, result)) self._token = result.get('data') expires_in_int = result.get('data').get('expires_in') self._token.update({'expires_in': int(time.time()) + expires_in_int}) expires_times = expires_in_int - 3000 self.set_cache_access_token(access_token = result.get('data').get('access_token'),expires = expires_times) self.set_cache_buyin_id(buyin_id=result.get('data').get('authority_id'), expires=expires_times) #authority_id其实为百应ID self.set_cache_refresh_token(refresh_token = result.get('data').get('refresh_token')) if self._token_file: with open(self._token_file, mode='w') as f: f.write(json.dumps(self._token)) return True logger.error("初始化token失败appkey=%s:返回内容:%s"%(self._app_key,result)) return False # 获取机构各角色的信息,如百应ID、角色名称等 # https://buyin.jinritemai.com/dashboard/service-provider/doc-center?docId=2305 def getBuyinInstitutionInfo(self): path = '/buyin/institutionInfo' params = {} result = self.request(path=path, params=params) return result def set_cache_buyin_id(self,buyin_id="",expires = 7*86400): cache.set("doudian_buyin_id"+self._app_key, buyin_id,expires) def set_cache_access_token(self,access_token="",expires = 7*86400): cache.set("doudian_access_token"+self._app_key, access_token,expires) def set_cache_refresh_token(self,refresh_token="",expires = 14*86400): cache.set("doudian_refresh_token"+self._app_key, refresh_token,expires) def get_cache_access_token_ttl(self): expire =cache.ttl("doudian_access_token"+self._app_key) return expire def get_cache_refresh_access_token_ttl(self): expire =cache.ttl("doudian_refresh_token"+self._app_key) return expire def get_cache_buyin_id(self): buyin_id =cache.get("doudian_buyin_id"+self._app_key) if buyin_id: return buyin_id return None def get_cache_access_token(self): access_token =cache.get("doudian_access_token"+self._app_key) if access_token: return access_token return None def get_cache_refresh_token(self): refresh_token =cache.get("doudian_refresh_token"+self._app_key) if refresh_token: return refresh_token return None #刷新access_token和refresh_token def refresh_token(self) -> None: path = '/token/refresh' refresh_token = self.get_cache_refresh_token() if refresh_token: grant_type = 'refresh_token' params = {} params.update({'grant_type': grant_type}) params.update({'refresh_token': refresh_token}) result = self._request(path=path, params=params, token_request=True) if result and result.get('code') == 10000 and result.get('data'): new_access_token = result.get('data').get('access_token') new_refresh_token = result.get('data').get('refresh_token') self._token = result.get('data') expires_in_int = result.get('data').get('expires_in') self._token.update({'expires_in': int(time.time()) + expires_in_int}) expires_times = expires_in_int - 3000 self.set_cache_access_token(access_token=new_access_token, expires=expires_times) self.set_cache_buyin_id(buyin_id=result.get('data').get('authority_id'), expires=expires_times) #authority_id其实为百应ID self.set_cache_refresh_token(refresh_token=new_refresh_token) if self._token_file: with open(self._token_file, mode='w') as f: f.write(json.dumps(self._token)) # return result.get('data').get('access_token') return True logger.error("刷新token失败:appkey=%s,返回:%s"%(self._app_key,result)) return False return False # 判断token是否过期 def is_token_expire(self): if self._access_token(): return False return True def _request(self, path: str, params: dict, token_request: bool = False,c_access_token = None) -> json: """ c_access_token :用户侧access_token,默认使用系统的access_token """ try: headers = {} headers.update({'Content-Type': 'application/json'}) headers.update({'Accept': 'application/json'}) headers.update({'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36'}) timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') param_json = json.dumps(params, sort_keys=True, separators=(',', ':')) method = path[1:].replace('/', '.') sign = self._sign(method=method, param_json=param_json, timestamp=timestamp) if token_request: url = self._gate_way + '{}?app_key={}&method={}¶m_json={}×tamp={}&v={}&sign_method={}&sign={}'.format( path, self._app_key, method, param_json, timestamp, self._version, self._sign_method, sign) else: if c_access_token: access_token = c_access_token else: access_token = self._access_token() url = self._gate_way + '{}?app_key={}&method={}&access_token={}×tamp={}&v={}&sign_method={}&sign={}'.format( path, self._app_key, method, access_token, timestamp, self._version, self._sign_method, sign) response = requests.post(url=url, data=param_json, headers=headers, proxies=self._proxy) if response.status_code != 200: logger.error("抖店应用请求失败:请求地址:%s,错误内容:%s"%(path,response)) return None return json.loads(response.content) except Exception as e: logger.error("抖店应用请求失败:请求地址:%s,错误内容:%s"%(path,e)) return None def request(self, path: str, params: dict, access_token=None) -> json: """请求抖店API接口 :param path: 调用的API接口地址,示例:'/material/uploadImageSync' :param params: 业务参数字典,示例:{'folder_id':'123123123','url':'http://www.demo.com/demo.jpg','material_name':'demo.jpg'} """ return self._request(path=path, params=params,c_access_token=access_token) def callback(self, headers: dict, body: bytes) -> json: """验证处理消息推送服务收到信息 """ data: str = body.decode('UTF-8') if not data: return None if headers.get('app-id') != self._app_key: return None event_sign: str = headers.get('event-sign') if not event_sign: return None h = Hash(algorithm=MD5(), backend=default_backend()) h.update('{}{}{}'.format(self._app_key, data, self._app_secret).encode('UTF-8')) if h.finalize().hex() != event_sign: return None return json.loads(data) def build_auth_url(self,id="", state="state",type=1) -> str: """拼接授权URL,引导商家、机构/团长、抖客/达人点击完成授权 type:类型 1 商家、 2 机构/团长 3、抖客/达人 id: type 类型为 1 和2时 id为service_id , 为3时为app_id(未上架应用填app_key) """ if self._app_type == "tool": if type == 1: return 'https://fuwu.jinritemai.com/authorize?service_id={}&state={}'.format(id, state) if type == 2: return 'https://market.jinritemai.com/authorize?service_id={}&state={}'.format(id, state) if type == 3: if not id: id = self._app_key return 'https://buyin.jinritemai.com/dashboard/institution/through-power?app_id={}&state={}'.format(id, state) return 'https://fuwu.jinritemai.com/authorize?service_id={}&state={}'.format(id, state) else: return None