From 73125106d2dc678af4427664d8bee8ac6fc54f86 Mon Sep 17 00:00:00 2001 From: r350178982 <32759763+r350178982@users.noreply.github.com> Date: Wed, 7 Apr 2021 14:57:38 +0800 Subject: [PATCH 1/3] update --- examples/python/shopee_integration.py | 301 ++++++++++++++++++++++++++ manual/python/README.md | 1 + 2 files changed, 302 insertions(+) create mode 100644 examples/python/shopee_integration.py diff --git a/examples/python/shopee_integration.py b/examples/python/shopee_integration.py new file mode 100644 index 00000000..728b387c --- /dev/null +++ b/examples/python/shopee_integration.py @@ -0,0 +1,301 @@ +import hashlib +import requests +import hmac +import time +import json +from seatable_api import Base, context + +""" +该脚本用于跨境电商网站shopee向seatable进行授权店铺的销售数据等信息同步,通过shopee开发者平台 +的api以及seatable_api的结合交互, 实现数据同步。 +""" + +BASE_URL_LIVE = "https://partner.shopeemobile.com" # shopee 线上正式域名 +BASE_URL_TEST_NEW = "https://partner.test-stable.shopeemobile.com" # shopee 新版测试域名 +BASE_URL_TEST_OLD = "https://partner.uat.shopeemobile.com" # shopee 老版测试域名 + +# 平台开发者申请之后会有一个指定且唯一的 partner_id 和 partner_key, 用于店铺授权, api调用等 +PARTNER_ID = 0 +PARTNER_KEY = "" + +SHOP_LIMITS = 5 # 测试的店铺数量, 若要导入所有的授权店铺,设置为-1 +N_DAYS_BEFORE = 2 # 测试多少天之前的订单,最高设置为 半个月之前 设置为 15 +SHOP_ID_LIST = [] # 测试指定店铺数据 + +# base表格模版基本信息, 包括两个子表, 订单产品和订单 +ORDER_ITEM_TABLE = "订单产品" # 订单产品表格名称 +ORDER_TABLE = "订单" # 订单表格名称 +COL_LINK_ORDER = "订单链接" + +class Shoppy(object): + """ + 封装的shopee-api请求: 包括 + 1. 通过加密签名方式构造请求头 + 2. 构造url + 3. post + """ + + BASE_URL = BASE_URL_LIVE + + def __init__(self, partner_id, secret_key, shop_id=None): + + self.partner_id = partner_id + self.secret_key = secret_key + self.shop_id = shop_id + + @property + def _timestamp(self): + return int(time.time()) + + def _make_url(self, uri): + """ + The full url including base url and uri, uri is the sub-domain such as + "/api/v1/xxx/xxxx" + :param uri: + :return: + """ + return "%s/%s" % ( + self.BASE_URL.rstrip('/'), + uri.lstrip('/') + ) + + def _make_sign(self, uri, request_body): + """ + make authorization's token put in request header + :param uri: + :param request_body: + :return: + """ + url = self._make_url(uri) + base_string = url + "|" + json.dumps(request_body) + sign = hmac.new(self.secret_key.encode(), base_string.encode(), hashlib.sha256).hexdigest() + return sign + + def _generate_body(self, **kwargs): + params = { + "partner_id": self.partner_id, + "timestamp": self._timestamp + } + if kwargs: + params.update(kwargs) + return params + + + def _headers(self, uri, body): + return { + "Content-Type": "application/json", + "Authorization": self._make_sign(uri, body) + } + + def post(self, uri, **kwargs): + request_body = self._generate_body(**kwargs) + + url = self._make_url(uri) + request_headers = self._headers(uri, request_body) + resp = requests.post(url, json=request_body, headers=request_headers) + + res_dict = resp.json() + if res_dict.get('error'): + result = None + else: + result = res_dict + return result + +class ShopManager(object): + + ''' + 通过 partner_id, partner_key 获取所有的授权店铺的列表 + ''' + + def __init__(self, partner_id, partner_key): + self.api = Shoppy(partner_id, partner_key) + + def get_shops_by_partner(self): + return self.api.post('/api/v1/shop/get_partner_shop') + + + +class Shop(object): + + """ + 通过 partner_i, partner_key, shop_id获取一个店铺,从而可以进行: + 1. 店铺信息查看 + 2. 店铺订单查看 + 3. 店铺订单产品查看 + ... + """ + + def __init__(self, partner_id, partner_key, shop_id): + + self.api = Shoppy(partner_id, partner_key, shop_id) + self.shop_id = shop_id + + def _generate_timestamp(self, dt): + timeArray = time.strptime(dt, "%Y-%m-%d") + # 转换成时间戳 + timestamp = time.mktime(timeArray) + return timestamp + + + def get_shop_info(self): + return self.api.post('/api/v1/shop/get', shopid=self.shop_id) + + def get_shop_orders(self, n_days_before=15): + create_time_to = int(time.time()) + create_time_from = create_time_to - n_days_before * 24 * 3600 + return self.api.post('/api/v1/orders/basics', + shopid=self.shop_id, + create_time_from=create_time_from, + create_time_to=create_time_to) + + def get_order_detail(self, ordersn): + return self.api.post('/api/v1/orders/detail', + shopid=self.shop_id, + ordersn_list = [ordersn,]) + + def get_shop_order_details(self, n_days_before=14): + order_list = self.get_shop_orders(n_days_before).get('orders') + ordersn_list = order_list and [o.get("ordersn") for o in order_list] or [] + if not ordersn_list: + return None + return self.api.post('/api/v1/orders/detail', + shopid=self.shop_id, + ordersn_list=ordersn_list, + ) + + def get_item_detail(self, item_id): + return self.api.post('/api/v1/item/get', shopid=self.shop_id, item_id=item_id) + + + def get_escow_released_orders(self): + create_time_to = int(time.time()) + create_time_from = create_time_to - 30 * 24 * 3600 + return self.api.post('/api/v2/orders/get_escrow_detail' , shopid=self.shop_id, + release_time_from = create_time_from, + release_time_to = create_time_to + ) + + + +def _get_authed_shop_id(limits=-1): + """ + 获取合作授权的店铺id列表 + :return: + """ + shop_manager = ShopManager(test_partner_id, test_api_key) + resps = shop_manager.get_shops_by_partner() + shop_id_list = [m.get('shopid') for m in resps.get('authed_shops')[:limits]] + return shop_id_list + +def timestamp2str(timest): + time_obj = time.localtime(timest) + time_str = time.strftime("%Y-%m-%d %H:%M", time_obj) + return time_str + +def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_list = None): + """ + 数据同步至seatable中指定的base + """ + + # 按照授权店铺-->具体店铺-->店铺订单-->订单项商品的不同level层级深入 + row_infos = [(row.get('订单编号'), row.get('下单时间')) for row in base.list_rows(ORDER_TABLE)] + # level 0: 授权店铺 + if shop_id_list: + shop_ids = shop_id_list + else: + shop_ids = _get_authed_shop_id(shop_limits) + for shop_id in shop_ids: + shop = Shop(test_partner_id, test_api_key, shop_id) + shop_info = shop.get_shop_info() + #根据excel表构造字段 + if not shop_info: + continue + + # level 2: 店铺中订单信息, 包括订单编号, 币种, 下单时间 + orders = shop.get_shop_order_details(n_days_before=n_days_before) + if not orders: + continue + order_details = orders.get('orders') + for order_detail in order_details: + order_sn = order_detail.get('ordersn') + currency = order_detail.get('currency') + time_order = timestamp2str(order_detail.get('create_time')) + if (order_sn, time_order) in row_infos: + #如果订单已经存在, 跳过 + continue + # 构造订单链接 + row_data_order = { + # shop 信息 + '店铺编号': shop_info.get('shop_id'), + '店铺名称': shop_info.get('shop_name'), + '站点': shop_info.get('country'), + + # 订单信息 + '订单编号': order_sn, # 订单编号 + '下单时间': time_order, # 生成时间, 时间戳 int + '币种': currency, # 交易货币, + '实收金额': order_detail.get('total_amount'), # 该订单的交易价格 + } + + row_order = base.append_row(ORDER_TABLE, row_data_order) + row_id_order = row_order.get('_id') + + + order_items = order_detail.get('items') + if not order_items: + continue + item_image_list = [] + for order_item_detail in order_items: + order_item_id = order_item_detail.get('item_id') + item_detail = shop.get_item_detail(order_item_id) + if not item_detail: + continue + order_item_detail_item = item_detail.get('item') + image_url = order_item_detail_item.get('images')[0] + item_image_list.append(image_url) + row_data_order_item = { + # 商品信息 + '商品标题': order_item_detail.get('item_name'), # 商品名称 + '商品规格': order_item_detail.get('variation_name'), # 商品细化标题 + '商品折扣价': order_item_detail.get('variation_discounted_price'), # 商品价格,折后 + '商品单价': order_item_detail.get('variation_original_price'), # 商品价格, 原价 + '购买数量': order_item_detail.get('variation_quantity_purchased'), # 商品购买数量 + + '商品图片': [image_url], + '商品链接': "https://%(country_abbr)s.xiapibuy.com/product/%(shop_id)s/%(item_id)s" % ({ + 'country_abbr': shop_info.get('country').lower(), + 'shop_id':shop_info.get('shop_id'), + 'item_id': order_item_detail.get('item_id') + }) + + } + row_order_item = base.append_row(ORDER_ITEM_TABLE, row_data_order_item) + row_id_order_item = row_order_item.get('_id') + row_order_item_link_id = base.get_column_link_id(ORDER_ITEM_TABLE, COL_LINK_ORDER) + base.add_link(row_order_item_link_id, + ORDER_ITEM_TABLE, + ORDER_TABLE, + row_id_order_item, + row_id_order + ) + base.update_row(ORDER_TABLE, row_id_order, { + "图片": item_image_list + }) + +if __name__ == '__main__': + test_partner_id = PARTNER_ID + test_api_key = PARTNER_KEY + + # 测试用的shop_id限制 + shop_limits = SHOP_LIMITS + n_days_before = N_DAYS_BEFORE + shop_id_list = SHOP_ID_LIST + + api_token = context.api_token or "5b260eca2bb1ca2cab7787c148bb09ba343c9082" + server_url = context.server_url or "http://127.0.0.1:8000/" + + + base = Base(api_token, server_url) + base.auth() + + insert_valid_infos_to_table(base, shop_limits, n_days_before, shop_id_list) diff --git a/manual/python/README.md b/manual/python/README.md index 9b398bcb..6290e824 100644 --- a/manual/python/README.md +++ b/manual/python/README.md @@ -88,4 +88,5 @@ SeaTable API 库介绍: * [update_certification_expiration.py](https://github.com/seatable/seatable-scripts-cn/tree/master/examples/python/update_certification_expiration.py): 更新网站证书过期时间 * [send_wechat_msg.py](https://github.com/seatable/seatable-scripts-cn/tree/master/examples/python/send_wechat_msg.py): 通过企业微信群机器人推送群消息 * [sync_stock_price.py](https://github.com/seatable/seatable-scripts-cn/tree/master/examples/python/sync_stock_price.py): 更新股票价格信息 +* [shopee_integration.py](https://github.com/seatable/seatable-scripts-cn/tree/master/examples/python/shopee_integration.py): 电商网站销售数据同步 From b171403e1d2951258031f7adc5b23b650598b282 Mon Sep 17 00:00:00 2001 From: r350178982 <32759763+r350178982@users.noreply.github.com> Date: Thu, 8 Apr 2021 09:48:35 +0800 Subject: [PATCH 2/3] currency-exchanger --- examples/python/shopee_integration.py | 59 ++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/examples/python/shopee_integration.py b/examples/python/shopee_integration.py index 728b387c..a2e6d510 100644 --- a/examples/python/shopee_integration.py +++ b/examples/python/shopee_integration.py @@ -175,7 +175,52 @@ def get_escow_released_orders(self): release_time_to = create_time_to ) +class MoneyExchange(object): + """ + 汇率换算功能类,从 https://currencylayer.com/ 中注册获取的免费接口 + 人民币与其他货币的汇率换算通过美元作为中介实现 + 目前该免费接口只能获取实时数据 + """ + access_key = "b8ce91d3168c6d3588576d073f580cfd" + + def __init__(self): + self.data_init() + + def data_init(self): + self.get_exchange_rates_to_CNY() + + def get_exchange_rates_to_USD(self): + # 获取世界各国货币同美元的比率 + url = "http://api.currencylayer.com/live?access_key=%s" % self.access_key + resp = requests.get(url) + resp_dict = resp.json() + return resp_dict.get('quotes') + + def get_exchange_rates_to_CNY(self): + # 通过美元的比率换算世界各国货币同中国RMB的比率字典 + exchange_rates_to_USD = self.get_exchange_rates_to_USD() + USD_TO_CNY = exchange_rates_to_USD.get('USDCNY') + d = {} + for k, v in exchange_rates_to_USD.items(): + d[k] = v / USD_TO_CNY + self.pool = d + return d + + def get_exchange_rates_to_CNY_by_cur(self, currency='THB'): + # 获取中国人民币RMB和其他国家货币的比率 + exchange_rates = self.pool + + return exchange_rates.get("USD%s" % currency) + + def exchange_to_CNY(self, money, currency='THB'): + # 转换currency的money数值为RMB数值 + exchange_rates = self.get_exchange_rates_to_CNY_by_cur(currency) + money_exchange = money / exchange_rates + return float("%.2f" % money_exchange) + + def exchange_to_CNY_history(self, money, currency='THB', timestamp=None): + pass def _get_authed_shop_id(limits=-1): """ @@ -192,7 +237,7 @@ def timestamp2str(timest): time_str = time.strftime("%Y-%m-%d %H:%M", time_obj) return time_str -def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_list = None): +def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_list = None, currency_exchanger=None): """ 数据同步至seatable中指定的base """ @@ -220,6 +265,7 @@ def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_li order_sn = order_detail.get('ordersn') currency = order_detail.get('currency') time_order = timestamp2str(order_detail.get('create_time')) + total_amount = order_detail.get('total_amount') if (order_sn, time_order) in row_infos: #如果订单已经存在, 跳过 continue @@ -234,7 +280,8 @@ def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_li '订单编号': order_sn, # 订单编号 '下单时间': time_order, # 生成时间, 时间戳 int '币种': currency, # 交易货币, - '实收金额': order_detail.get('total_amount'), # 该订单的交易价格 + '实收金额': total_amount, # 该订单的交易价格 + '实收金额RMB': currency.exchange_to_CNY(float(total_amount), currency), # 该订单的交易价格RMB } row_order = base.append_row(ORDER_TABLE, row_data_order) @@ -253,12 +300,14 @@ def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_li order_item_detail_item = item_detail.get('item') image_url = order_item_detail_item.get('images')[0] item_image_list.append(image_url) + original_price = order_item_detail.get('variation_original_price') row_data_order_item = { # 商品信息 '商品标题': order_item_detail.get('item_name'), # 商品名称 '商品规格': order_item_detail.get('variation_name'), # 商品细化标题 '商品折扣价': order_item_detail.get('variation_discounted_price'), # 商品价格,折后 - '商品单价': order_item_detail.get('variation_original_price'), # 商品价格, 原价 + '商品单价': original_price, # 商品价格, 原价 + '商品单价RMB': currency_exchanger.exchange_to_CNY(float(original_price), currency), # 商品价格RMB, 原价 '购买数量': order_item_detail.get('variation_quantity_purchased'), # 商品购买数量 '商品图片': [image_url], @@ -297,5 +346,5 @@ def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_li base = Base(api_token, server_url) base.auth() - - insert_valid_infos_to_table(base, shop_limits, n_days_before, shop_id_list) + me = MoneyExchange() + insert_valid_infos_to_table(base, shop_limits, n_days_before, shop_id_list, currency_exchanger=me) From b78b0ed9ce0b0b0b00a34ee7c22c6a24cafe1856 Mon Sep 17 00:00:00 2001 From: r350178982 <32759763+r350178982@users.noreply.github.com> Date: Sat, 10 Apr 2021 13:04:38 +0800 Subject: [PATCH 3/3] Update shopee_integration.py --- examples/python/shopee_integration.py | 414 ++++++++++++++++++-------- 1 file changed, 287 insertions(+), 127 deletions(-) diff --git a/examples/python/shopee_integration.py b/examples/python/shopee_integration.py index a2e6d510..1748f1ec 100644 --- a/examples/python/shopee_integration.py +++ b/examples/python/shopee_integration.py @@ -3,37 +3,25 @@ import hmac import time import json +import re from seatable_api import Base, context +BASE_URL_LIVE = "https://partner.shopeemobile.com" +BASE_URL_TEST_NEW = "https://partner.test-stable.shopeemobile.com" +BASE_URL_TEST_OLD = "https://partner.uat.shopeemobile.com" -""" -该脚本用于跨境电商网站shopee向seatable进行授权店铺的销售数据等信息同步,通过shopee开发者平台 -的api以及seatable_api的结合交互, 实现数据同步。 -""" +PARTNER_ID = 843359 +PARTNER_KEY = "004a04cf2f55f0f9a0dd22ca79a12ca69327011f2d3159125f75943e7f126b41" -BASE_URL_LIVE = "https://partner.shopeemobile.com" # shopee 线上正式域名 -BASE_URL_TEST_NEW = "https://partner.test-stable.shopeemobile.com" # shopee 新版测试域名 -BASE_URL_TEST_OLD = "https://partner.uat.shopeemobile.com" # shopee 老版测试域名 - -# 平台开发者申请之后会有一个指定且唯一的 partner_id 和 partner_key, 用于店铺授权, api调用等 -PARTNER_ID = 0 -PARTNER_KEY = "" - -SHOP_LIMITS = 5 # 测试的店铺数量, 若要导入所有的授权店铺,设置为-1 -N_DAYS_BEFORE = 2 # 测试多少天之前的订单,最高设置为 半个月之前 设置为 15 +SPLIT_PATERN = ",|," +SHOP_LIMITS = 30 # 测试的店铺数量, 若要导入所有的授权店铺,设置为-1 +N_DAYS_BEFORE = 7 # 测试多少天之前的订单,最高设置为 半个月之前 设置为 15 SHOP_ID_LIST = [] # 测试指定店铺数据 -# base表格模版基本信息, 包括两个子表, 订单产品和订单 ORDER_ITEM_TABLE = "订单产品" # 订单产品表格名称 ORDER_TABLE = "订单" # 订单表格名称 COL_LINK_ORDER = "订单链接" class Shoppy(object): - """ - 封装的shopee-api请求: 包括 - 1. 通过加密签名方式构造请求头 - 2. 构造url - 3. post - """ BASE_URL = BASE_URL_LIVE @@ -96,6 +84,7 @@ def post(self, uri, **kwargs): res_dict = resp.json() if res_dict.get('error'): + print("[ERROR: %s]" % res_dict) result = None else: result = res_dict @@ -103,10 +92,6 @@ def post(self, uri, **kwargs): class ShopManager(object): - ''' - 通过 partner_id, partner_key 获取所有的授权店铺的列表 - ''' - def __init__(self, partner_id, partner_key): self.api = Shoppy(partner_id, partner_key) @@ -117,13 +102,7 @@ def get_shops_by_partner(self): class Shop(object): - """ - 通过 partner_i, partner_key, shop_id获取一个店铺,从而可以进行: - 1. 店铺信息查看 - 2. 店铺订单查看 - 3. 店铺订单产品查看 - ... - """ + def __init__(self, partner_id, partner_key, shop_id): @@ -143,10 +122,39 @@ def get_shop_info(self): def get_shop_orders(self, n_days_before=15): create_time_to = int(time.time()) create_time_from = create_time_to - n_days_before * 24 * 3600 - return self.api.post('/api/v1/orders/basics', + order_basic_info_list = [] + for create_time_from, create_time_to in self.generate_time_interval(n_days_before): + order_basic_info = self.api.post('/api/v1/orders/basics', shopid=self.shop_id, create_time_from=create_time_from, create_time_to=create_time_to) + order_basic_info_list.append(order_basic_info) + return order_basic_info_list + + def generate_time_interval(self, n_days_before): + # 处理一次请求只能间隔15天的情况 + time_now = int(time.time()) + time_interval_list = [] + one_day_timest = 24 * 3600 + n_days_before_tmp = n_days_before + while n_days_before_tmp > 15: + time_f = time_now - n_days_before_tmp * one_day_timest + time_t = time_now - (n_days_before_tmp - 15) * one_day_timest + time_interval_list.append((time_f, time_t)), + n_days_before_tmp -= 15 + time_interval_list.append((time_now - n_days_before_tmp * 24 * 3600, time_now)) + return time_interval_list + + def generate_order_interval(self, order_sn_list): + # 处理一次请求只能请求50个订单的情况 + # 数据结构, [[xxx,xxx,xxx...], [xxxx,xxx,xxx,....]] + interval = 49 + tmp = 0 + order_sn_interval_list = [] + while tmp < len(order_sn_list): + order_sn_interval_list.append(order_sn_list[tmp: tmp + interval]) + tmp += interval + return order_sn_interval_list def get_order_detail(self, ordersn): return self.api.post('/api/v1/orders/detail', @@ -154,14 +162,17 @@ def get_order_detail(self, ordersn): ordersn_list = [ordersn,]) def get_shop_order_details(self, n_days_before=14): - order_list = self.get_shop_orders(n_days_before).get('orders') - ordersn_list = order_list and [o.get("ordersn") for o in order_list] or [] + order_list = [order_basic.get('orders') for order_basic in self.get_shop_orders(n_days_before)] + ordersn_list = order_list and [o.get("ordersn") for o in sum(order_list, [])] or [] if not ordersn_list: return None - return self.api.post('/api/v1/orders/detail', + order_details_list = [] + for ordersn_interval in self.generate_order_interval(ordersn_list): + order_details_list.append(self.api.post('/api/v1/orders/detail', shopid=self.shop_id, - ordersn_list=ordersn_list, - ) + ordersn_list=ordersn_interval, + )) + return order_details_list def get_item_detail(self, item_id): return self.api.post('/api/v1/item/get', shopid=self.shop_id, item_id=item_id) @@ -171,17 +182,12 @@ def get_escow_released_orders(self): create_time_to = int(time.time()) create_time_from = create_time_to - 30 * 24 * 3600 return self.api.post('/api/v2/orders/get_escrow_detail' , shopid=self.shop_id, - release_time_from = create_time_from, - release_time_to = create_time_to + # release_time_from = create_time_from, + # release_time_to = create_time_to ) class MoneyExchange(object): - """ - 汇率换算功能类,从 https://currencylayer.com/ 中注册获取的免费接口 - 人民币与其他货币的汇率换算通过美元作为中介实现 - 目前该免费接口只能获取实时数据 - """ access_key = "b8ce91d3168c6d3588576d073f580cfd" def __init__(self): @@ -191,14 +197,12 @@ def data_init(self): self.get_exchange_rates_to_CNY() def get_exchange_rates_to_USD(self): - # 获取世界各国货币同美元的比率 url = "http://api.currencylayer.com/live?access_key=%s" % self.access_key resp = requests.get(url) resp_dict = resp.json() return resp_dict.get('quotes') def get_exchange_rates_to_CNY(self): - # 通过美元的比率换算世界各国货币同中国RMB的比率字典 exchange_rates_to_USD = self.get_exchange_rates_to_USD() USD_TO_CNY = exchange_rates_to_USD.get('USDCNY') d = {} @@ -208,19 +212,15 @@ def get_exchange_rates_to_CNY(self): return d def get_exchange_rates_to_CNY_by_cur(self, currency='THB'): - # 获取中国人民币RMB和其他国家货币的比率 exchange_rates = self.pool return exchange_rates.get("USD%s" % currency) - def exchange_to_CNY(self, money, currency='THB'): - # 转换currency的money数值为RMB数值 + def exchange_to_CNY(self, money, currency='CNY'): exchange_rates = self.get_exchange_rates_to_CNY_by_cur(currency) money_exchange = money / exchange_rates return float("%.2f" % money_exchange) - def exchange_to_CNY_history(self, money, currency='THB', timestamp=None): - pass def _get_authed_shop_id(limits=-1): """ @@ -237,19 +237,114 @@ def timestamp2str(timest): time_str = time.strftime("%Y-%m-%d %H:%M", time_obj) return time_str -def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_list = None, currency_exchanger=None): + +def format_order_escow_infos(order_escow_infos): + """ + {":ordersn":":releasetime"} + :param orderinfos: + :return: + """ + return {info.get('ordersn'): info.get('escow_release_time') for info in order_escow_infos} + + +def currency_exchange_rate(escow_release_timestamp, from_c=None, to_c='RMB'): + pass + +def parse_order_status(order_status): + # 将订单状态转换为中文 + "ALL/UNPAID/READY_TO_SHIP/COMPLETED/IN_CANCEL/CANCELLED/TO_RETURN" + return { + "UNPAID": '待付款', + "READY_TO_SHIP": "待发运", + "SHIPPED": "已发运", + "COMPLETED": "已完成", + "IN_CANCEL": "取消中", + "CANCELLED": "已取消", + "TO_RETURN": "退款 / 退货", + "RETRY_SHIP":"再次发货", + "TO_CONFIRM_RECEIVE": "待确认收货" + }.get(order_status) + +def order_id_row_id_map(base): + # 构造订单编号和行号的映射, 用于更新表格 """ - 数据同步至seatable中指定的base + { + ": order_id": ":row_id" + ": order_id2": ":row_id2" + ": order_id3": ":row_id3" + } """ + return { + row.get('订单编号') : row.get('_id') for row in base.list_rows(ORDER_TABLE) + } + +def order_key_list(base, operation="row_create"): + # 通过摘取订单表中的一些信息来做唯一的索引 + # 构造索引数据结构和上述唯一索引做比较, 如果不匹配, 则执行创建行或更新行的逻辑 + if operation not in ["row_create", "row_update"]: + raise ValueError('Row operation invalid, please select row_create or row update') + + # 创建行, 使用订单编号, 创建时间来做唯一索引 + if operation == 'row_create': + return [( + row.get('订单编号'), + row.get('店铺编号'), + ) + for row in base.list_rows(ORDER_TABLE) + ] + + # 更新行, 使用编号, 时间, 回款金额,订单状态来做唯一索引 + if operation == 'row_update': + return [( + row.get('订单编号'), + row.get('店铺编号'), + str(row.get('回款金额')), + row.get('订单状态') + + ) for row in base.list_rows(ORDER_TABLE)] + +def get_domain_by_country(country): + """ + TW:https://seller.xiapi.shopee.cn/account/signin + MY:https://seller.my.shopee.cn/account/signin + SG:https://seller.sg.shopee.cn/account/signin + ID:https://seller.id.shopee.cn/account/signin + TH:https://seller.th.shopee.cn/account/signin + VN: https://seller.vn.shopee.cn/account/signin + PH:https://seller.ph.shopee.cn/account/signin + BR: https://seller.br.shopee.cn/account/signin + :param country: + :return: + """ + return { + "TW": "xiapi", + "MY": "my", + "SG": "sg", + "ID": "id", + "TH": "th", + "VN": "vn", + "PH": "ph", + "BR": "br" + }.get(country) + + +# 添加行API +def sync_infos_from_shopee(base, shop_limits=2, n_days_before=2, shop_id_list = None, money_exchanger=None, ship_cost_default=3): + valid_info_list = [] # 按照授权店铺-->具体店铺-->店铺订单-->订单项商品的不同level层级深入 - row_infos = [(row.get('订单编号'), row.get('下单时间')) for row in base.list_rows(ORDER_TABLE)] + order_key_list_create = order_key_list(base, operation='row_create') + order_key_list_update = order_key_list(base, operation='row_update') + order_row_map = order_id_row_id_map(base) + # level 0: 授权店铺 if shop_id_list: shop_ids = shop_id_list else: shop_ids = _get_authed_shop_id(shop_limits) + order_create_count, order_update_count = 0, 0 for shop_id in shop_ids: + shop = Shop(test_partner_id, test_api_key, shop_id) shop_info = shop.get_shop_info() #根据excel表构造字段 @@ -257,94 +352,159 @@ def insert_valid_infos_to_table(base, shop_limits=2, n_days_before=2, shop_id_li continue # level 2: 店铺中订单信息, 包括订单编号, 币种, 下单时间 - orders = shop.get_shop_order_details(n_days_before=n_days_before) - if not orders: + orders_detail_list = shop.get_shop_order_details(n_days_before=n_days_before) + if not orders_detail_list: continue - order_details = orders.get('orders') - for order_detail in order_details: + order_details = [orders.get('orders') for orders in orders_detail_list] + for order_detail in sum(order_details, []): order_sn = order_detail.get('ordersn') currency = order_detail.get('currency') time_order = timestamp2str(order_detail.get('create_time')) total_amount = order_detail.get('total_amount') - if (order_sn, time_order) in row_infos: - #如果订单已经存在, 跳过 - continue - # 构造订单链接 - row_data_order = { - # shop 信息 - '店铺编号': shop_info.get('shop_id'), - '店铺名称': shop_info.get('shop_name'), - '站点': shop_info.get('country'), - - # 订单信息 - '订单编号': order_sn, # 订单编号 - '下单时间': time_order, # 生成时间, 时间戳 int - '币种': currency, # 交易货币, - '实收金额': total_amount, # 该订单的交易价格 - '实收金额RMB': currency.exchange_to_CNY(float(total_amount), currency), # 该订单的交易价格RMB - } - - row_order = base.append_row(ORDER_TABLE, row_data_order) - row_id_order = row_order.get('_id') - - - order_items = order_detail.get('items') - if not order_items: - continue - item_image_list = [] - for order_item_detail in order_items: - order_item_id = order_item_detail.get('item_id') - item_detail = shop.get_item_detail(order_item_id) - if not item_detail: + escow_amount = order_detail.get('escrow_amount') + order_status = parse_order_status(order_detail.get('order_status')) + if (order_sn, shop_id) in order_key_list_create: + #如果订单已经存在, 看订单是否需要更新 + if (order_sn, shop_id, escow_amount, order_status) in order_key_list_update: + # 如果字段都匹配, 跳过 + print("[PASS] order_sn %s exists" % order_sn) continue - order_item_detail_item = item_detail.get('item') - image_url = order_item_detail_item.get('images')[0] - item_image_list.append(image_url) - original_price = order_item_detail.get('variation_original_price') - row_data_order_item = { - # 商品信息 - '商品标题': order_item_detail.get('item_name'), # 商品名称 - '商品规格': order_item_detail.get('variation_name'), # 商品细化标题 - '商品折扣价': order_item_detail.get('variation_discounted_price'), # 商品价格,折后 - '商品单价': original_price, # 商品价格, 原价 - '商品单价RMB': currency_exchanger.exchange_to_CNY(float(original_price), currency), # 商品价格RMB, 原价 - '购买数量': order_item_detail.get('variation_quantity_purchased'), # 商品购买数量 - - '商品图片': [image_url], - '商品链接': "https://%(country_abbr)s.xiapibuy.com/product/%(shop_id)s/%(item_id)s" % ({ - 'country_abbr': shop_info.get('country').lower(), - 'shop_id':shop_info.get('shop_id'), - 'item_id': order_item_detail.get('item_id') - }) - + else: + # 执行订单更新逻辑 + row_data_order_update = { + "订单状态": order_status, + "回款金额": escow_amount + } + row_id = order_row_map.get(order_sn) + base.update_row(ORDER_TABLE, row_id, row_data_order_update) + order_update_count += 1 + print("[ORDER UPDATED][%s]: 状态: %s / 回款金额: %s" % (order_sn, order_status, escow_amount)) + + else: + # 构造订单链接 + row_data_order = { + # shop 信息 + '店铺编号': shop_info.get('shop_id'), + '店铺名称': shop_info.get('shop_name'), + '站点': shop_info.get('country'), + + # 订单信息 + '订单编号': order_sn, # 订单编号 + '下单时间': time_order, # 生成时间, 时间戳 int + '币种': currency, # 交易货币, + '实收金额': total_amount, # 该订单的交易价格 + '实收金额RMB': money_exchanger.exchange_to_CNY(float(total_amount),currency), # 该订单的交易价格 + '回款金额': escow_amount, # 订单的回款金额, 实收金额-佣金-平台手续费 + '回款金额RMB':money_exchanger.exchange_to_CNY(float(total_amount),currency), # 该订单的回款金额 + '订单状态': order_status, + '货代运费RMB': ship_cost_default, # 货代运费, 给一个初始默认值 } - row_order_item = base.append_row(ORDER_ITEM_TABLE, row_data_order_item) - row_id_order_item = row_order_item.get('_id') - row_order_item_link_id = base.get_column_link_id(ORDER_ITEM_TABLE, COL_LINK_ORDER) - base.add_link(row_order_item_link_id, - ORDER_ITEM_TABLE, - ORDER_TABLE, - row_id_order_item, - row_id_order - ) - base.update_row(ORDER_TABLE, row_id_order, { - "图片": item_image_list - }) + + row_order = base.append_row(ORDER_TABLE, row_data_order) + row_id_order = row_order.get('_id') + order_create_count += 1 + print("[ORDER CREATED][%s]" % order_sn) + + order_items = order_detail.get('items') + if not order_items: + continue + item_image_list = [] + for order_item_detail in order_items: + order_item_id = order_item_detail.get('item_id') + item_detail = shop.get_item_detail(order_item_id) + if not item_detail: + continue + order_item_detail_item = item_detail.get('item') + image_url = order_item_detail_item.get('images')[0] + item_image_list.append(image_url) + original_price = order_item_detail.get('variation_original_price') + row_data_order_item = { + # 商品信息 + '商品标题': order_item_detail.get('item_name'), # 商品名称 + '商品规格': order_item_detail.get('variation_name'), # 商品细化标题 + '商品折扣价': order_item_detail.get('variation_discounted_price'), # 商品价格,折后 + '商品单价': original_price, # 商品价格, 原价 + '商品单价RMB': money_exchanger.exchange_to_CNY(float(original_price), currency), # 商品价格, 原价 + '购买数量': order_item_detail.get('variation_quantity_purchased'), # 商品购买数量 + + '商品图片': [image_url], + '商品链接': "https://%(country_abbr)s.xiapibuy.com/product/%(shop_id)s/%(item_id)s" % ({ + 'country_abbr': get_domain_by_country(shop_info.get('country')), + 'shop_id':shop_info.get('shop_id'), + 'item_id': order_item_detail.get('item_id') + }) + + } + row_order_item = base.append_row(ORDER_ITEM_TABLE, row_data_order_item) + row_id_order_item = row_order_item.get('_id') + row_order_item_link_id = base.get_column_link_id(ORDER_ITEM_TABLE, COL_LINK_ORDER) + base.add_link(row_order_item_link_id, + ORDER_ITEM_TABLE, + ORDER_TABLE, + row_id_order_item, + row_id_order + ) + base.update_row(ORDER_TABLE, row_id_order, { + "图片": item_image_list + }) + + sync_info_msg = "新增订单个数: %s / 更新订单个数: %s" % (order_create_count, order_update_count) + return sync_info_msg + if __name__ == '__main__': test_partner_id = PARTNER_ID test_api_key = PARTNER_KEY + test_shop_id = 250963379 # 测试用的shop_id限制 shop_limits = SHOP_LIMITS n_days_before = N_DAYS_BEFORE shop_id_list = SHOP_ID_LIST + me = MoneyExchange() - api_token = context.api_token or "5b260eca2bb1ca2cab7787c148bb09ba343c9082" - server_url = context.server_url or "http://127.0.0.1:8000/" + # 1. 获取客户信息表 + customer_info_table_api_token = "624ab6fe4e1fabbf7d7f2a975875065420083be3" + cloud_server_url = "https://cloud.seatable.cn/" + + base_customer_info = Base(customer_info_table_api_token, cloud_server_url) + base_customer_info.auth() + for row in base_customer_info.list_rows('客户信息'): + shop_ids = row.get('店铺ID') + + if not shop_ids: + shop_id_list = [] + else: + shop_id_list = [int(shop_id_str.strip()) for shop_id_str in re.split(SPLIT_PATERN, shop_ids)] + base_token = row.get('Base Token(管理员生成)') + if not base_token: + continue + try: + n_days_before = int(row.get('数据导入天数(<15)')) + except: + n_days_before = 7 + try: + ship_cost_default = int(row.get('货代运费RMB默认')) + except: + ship_cost_default = 3 + + customer_name = row.get('用户名') + try: + customer_base = Base(base_token, cloud_server_url) + customer_base.auth() + except Exception as e: + print("[ERROR] Base %s auth faild" % base_token) + continue + + sysn_info_msg = sync_infos_from_shopee( + customer_base, # 客户base + shop_limits, # 测试shop的数量, 正式使用时设置为-1 + n_days_before, # 倒入n天前订单 + shop_id_list, # 客户的shop_id, 为空导入所有订单, 用于管理员 + money_exchanger=me, # 汇率转换器 + ship_cost_default=ship_cost_default, # 货代运费默认值 + ) + + print("客户数据同步结果: [%s] %s" % (customer_name, sysn_info_msg)) - base = Base(api_token, server_url) - base.auth() - me = MoneyExchange() - insert_valid_infos_to_table(base, shop_limits, n_days_before, shop_id_list, currency_exchanger=me)