qrpa 1.1.79__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of qrpa might be problematic. Click here for more details.
- qrpa/RateLimitedSender.py +45 -0
- qrpa/__init__.py +31 -0
- qrpa/db_migrator.py +601 -0
- qrpa/feishu_bot_app.py +607 -0
- qrpa/feishu_client.py +410 -0
- qrpa/feishu_logic.py +1443 -0
- qrpa/fun_base.py +339 -0
- qrpa/fun_excel.py +3470 -0
- qrpa/fun_file.py +319 -0
- qrpa/fun_web.py +473 -0
- qrpa/fun_win.py +198 -0
- qrpa/mysql_module/__init__.py +0 -0
- qrpa/mysql_module/new_product_analysis_model.py +556 -0
- qrpa/mysql_module/shein_ledger_model.py +468 -0
- qrpa/mysql_module/shein_ledger_month_report_model.py +599 -0
- qrpa/mysql_module/shein_product_model.py +495 -0
- qrpa/mysql_module/shein_return_order_model.py +776 -0
- qrpa/mysql_module/shein_store_model.py +595 -0
- qrpa/mysql_module/shein_supplier_info_model.py +554 -0
- qrpa/mysql_module/shein_wallet_model.py +638 -0
- qrpa/shein_daily_report_model.py +375 -0
- qrpa/shein_excel.py +3809 -0
- qrpa/shein_lib.py +5780 -0
- qrpa/shein_mysql.py +106 -0
- qrpa/shein_sqlite.py +154 -0
- qrpa/shein_ziniao.py +531 -0
- qrpa/temu_chrome.py +56 -0
- qrpa/temu_excel.py +139 -0
- qrpa/temu_lib.py +156 -0
- qrpa/time_utils.py +882 -0
- qrpa/time_utils_example.py +243 -0
- qrpa/wxwork.py +318 -0
- qrpa-1.1.79.dist-info/METADATA +9 -0
- qrpa-1.1.79.dist-info/RECORD +36 -0
- qrpa-1.1.79.dist-info/WHEEL +5 -0
- qrpa-1.1.79.dist-info/top_level.txt +1 -0
qrpa/temu_excel.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from .fun_excel import *
|
|
2
|
+
from .fun_base import log
|
|
3
|
+
from .fun_file import read_dict_from_file, read_dict_from_file_ex, write_dict_to_file, write_dict_to_file_ex, delete_file
|
|
4
|
+
from .time_utils import TimeUtils
|
|
5
|
+
from .wxwork import WxWorkBot
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
class TemuExcel:
|
|
9
|
+
|
|
10
|
+
def __init__(self, config, bridge):
|
|
11
|
+
self.config = config
|
|
12
|
+
self.bridge = bridge
|
|
13
|
+
|
|
14
|
+
def write_funds(self):
|
|
15
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/funds_{TimeUtils.today_date()}.json'
|
|
16
|
+
dict = read_dict_from_file(cache_file)
|
|
17
|
+
data = []
|
|
18
|
+
for key, val in dict.items():
|
|
19
|
+
data.append(val)
|
|
20
|
+
|
|
21
|
+
excel_path = create_file_path(self.config.excel_temu_fund)
|
|
22
|
+
data.insert(0, ['汇总', '', '', '', ''])
|
|
23
|
+
data.insert(0, ['店铺名称', '总金额', '可用余额', '-', '导出时间'])
|
|
24
|
+
log(data)
|
|
25
|
+
# 删除第 4 列(索引为 3)
|
|
26
|
+
for row in data:
|
|
27
|
+
row.pop(3) # 删除每行中索引为 3 的元素
|
|
28
|
+
|
|
29
|
+
write_data(excel_path, 'Sheet1', data)
|
|
30
|
+
|
|
31
|
+
app, wb, sheet = open_excel(excel_path, 'Sheet1')
|
|
32
|
+
beautify_title(sheet)
|
|
33
|
+
format_to_money(sheet, ['金额', '余额'])
|
|
34
|
+
format_to_datetime(sheet, ['时间'])
|
|
35
|
+
add_sum_for_cell(sheet, ['总金额', '可用余额'])
|
|
36
|
+
add_borders(sheet)
|
|
37
|
+
close_excel(app, wb)
|
|
38
|
+
|
|
39
|
+
def format_purchase_advise_batch(self, sheet):
|
|
40
|
+
beautify_title(sheet)
|
|
41
|
+
format_to_datetime(sheet, ['时间'])
|
|
42
|
+
format_to_number(sheet, ['平均日销', '本地和采购可售天数', '建议采购'], 1)
|
|
43
|
+
add_borders(sheet)
|
|
44
|
+
add_formula_for_column(sheet, '平均日销', '=G2/7')
|
|
45
|
+
add_formula_for_column(sheet, '本地和采购可售天数', '=IF(H2>0,(E2+F2)/H2,0)')
|
|
46
|
+
add_formula_for_column(sheet, '建议采购', '=IF(J2>I2,H2*9,0)')
|
|
47
|
+
colorize_by_field(sheet, 'SKC')
|
|
48
|
+
autofit_column(sheet, ['店铺名称', '商品信息'])
|
|
49
|
+
column_to_left(sheet, ['商品信息'])
|
|
50
|
+
InsertImageV2(sheet, ['SKC图片', 'SKU图片'], 'temu', 120)
|
|
51
|
+
# if sheet.used_range.rows.count > 330:
|
|
52
|
+
# log('表格数据行数超过了330行,将删除SKC图片')
|
|
53
|
+
# remove_excel_columns(sheet, ['SKC图片'])
|
|
54
|
+
|
|
55
|
+
def write_purchase_advise(self, erp='mb'):
|
|
56
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/warehouse_list_{TimeUtils.today_date()}.json'
|
|
57
|
+
dict = read_dict_from_file(cache_file)
|
|
58
|
+
|
|
59
|
+
store_info = read_dict_from_file(self.config.temu_store_info)
|
|
60
|
+
|
|
61
|
+
header = ['店铺名称', 'SKC图片', 'SKU图片', '商品信息', '现有库存数量', '已采购数量', '近7日销量', '平均日销', '本地和采购可售天数', '生产天数', '建议采购', '产品起定量', '备货周期(天)', 'SKC', '导出时间']
|
|
62
|
+
new_excel_path_list = []
|
|
63
|
+
|
|
64
|
+
for mall_id, subOrderList in dict.items():
|
|
65
|
+
excel_data = []
|
|
66
|
+
mall_name = store_info.get(mall_id)[1]
|
|
67
|
+
|
|
68
|
+
for product in subOrderList:
|
|
69
|
+
spu = str(product['productId']) # temu平台 spu_id
|
|
70
|
+
skc = str(product['productSkcId']) # temu平台 skc_id
|
|
71
|
+
skcExtCode = product['skcExtCode'] # 商家 SKC货号
|
|
72
|
+
category = product['category'] # 叶子类目
|
|
73
|
+
onSalesDurationOffline = product['onSalesDurationOffline'] # 加入站点时长
|
|
74
|
+
|
|
75
|
+
for sku in product['skuQuantityDetailList']:
|
|
76
|
+
priceReviewStatus = sku['priceReviewStatus']
|
|
77
|
+
if priceReviewStatus == 3: # 过滤 开款价格状态 已作废的 2是已生效
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
mall_info = f'{mall_name}\n{mall_id}'
|
|
81
|
+
productSkcPicture = product['productSkcPicture'] # skc图片
|
|
82
|
+
skuExtCode = str(sku['skuExtCode']) # sku货号
|
|
83
|
+
sku_img = self.bridge.get_sku_img(skuExtCode, erp)
|
|
84
|
+
stock = self.bridge.get_sku_stock(skuExtCode, erp)
|
|
85
|
+
|
|
86
|
+
product_info = f"SPU: {spu}\nSKC: {skc}\nSKC货号: {skcExtCode}\nSKU货号: {skuExtCode}\n属性集: {sku['className']}\n类目: {category}\n加入站点时长: {onSalesDurationOffline}天\n"
|
|
87
|
+
|
|
88
|
+
row_item = []
|
|
89
|
+
row_item.append(mall_info)
|
|
90
|
+
row_item.append(productSkcPicture)
|
|
91
|
+
row_item.append(sku_img)
|
|
92
|
+
row_item.append(product_info)
|
|
93
|
+
row_item.append(stock)
|
|
94
|
+
row_item.append(0)
|
|
95
|
+
row_item.append(sku['lastSevenDaysSaleVolume'])
|
|
96
|
+
row_item.append(0)
|
|
97
|
+
row_item.append(0)
|
|
98
|
+
row_item.append(7)
|
|
99
|
+
row_item.append(0)
|
|
100
|
+
row_item.append(0)
|
|
101
|
+
row_item.append(0)
|
|
102
|
+
row_item.append(skc)
|
|
103
|
+
row_item.append(TimeUtils.current_datetime())
|
|
104
|
+
excel_data.append(row_item)
|
|
105
|
+
|
|
106
|
+
# 按近7日销量排序
|
|
107
|
+
excel_data = sort_by_column(excel_data, 6, 1)
|
|
108
|
+
|
|
109
|
+
# 计算需要多少个文件(每个文件最多320行数据,包含表头)
|
|
110
|
+
max_data_rows = 250 - 1 # 减去表头行
|
|
111
|
+
total_files = (len(excel_data) + max_data_rows - 1) // max_data_rows # 通过加(max_data_rows-1)实现向上取整
|
|
112
|
+
|
|
113
|
+
for file_index in range(total_files):
|
|
114
|
+
start_idx = file_index * max_data_rows
|
|
115
|
+
end_idx = min((file_index + 1) * max_data_rows, len(excel_data))
|
|
116
|
+
current_data = excel_data[start_idx:end_idx]
|
|
117
|
+
|
|
118
|
+
# 生成文件名,如果超过一个文件则添加序号
|
|
119
|
+
if total_files == 1:
|
|
120
|
+
new_excel_path = str(self.config.excel_purcase_advice_temu).replace('#store_name#', mall_name).replace(' ', '_')
|
|
121
|
+
else:
|
|
122
|
+
# 在文件名后添加 _2, _3 等序号
|
|
123
|
+
base_path = str(self.config.excel_purcase_advice_temu).replace('#store_name#', mall_name).replace(' ', '_')
|
|
124
|
+
file_name, file_ext = os.path.splitext(base_path)
|
|
125
|
+
new_excel_path = f"{file_name}_{file_index + 1}{file_ext}"
|
|
126
|
+
|
|
127
|
+
new_excel_path_list.append(new_excel_path)
|
|
128
|
+
sheet_name = 'Sheet1'
|
|
129
|
+
data = [header] + current_data
|
|
130
|
+
|
|
131
|
+
close_excel_file(new_excel_path)
|
|
132
|
+
log(f"创建文件: {new_excel_path}, 数据行数: {len(current_data)}")
|
|
133
|
+
|
|
134
|
+
batch_excel_operations(new_excel_path, [
|
|
135
|
+
(sheet_name, 'write', data, ['N']),
|
|
136
|
+
(sheet_name, 'format', self.format_purchase_advise_batch)
|
|
137
|
+
])
|
|
138
|
+
|
|
139
|
+
return new_excel_path_list
|
qrpa/temu_lib.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from .fun_file import read_dict_from_file, write_dict_to_file, read_dict_from_file_ex, write_dict_to_file_ex
|
|
2
|
+
from .fun_base import log, send_exception, md5_string, get_safe_value
|
|
3
|
+
from .time_utils import TimeUtils
|
|
4
|
+
|
|
5
|
+
import json, requests, time, math
|
|
6
|
+
|
|
7
|
+
class TemuLib:
|
|
8
|
+
def __init__(self, config, mobile, password, web_page):
|
|
9
|
+
self.config = config
|
|
10
|
+
self.web_page = web_page
|
|
11
|
+
self.mobile = mobile
|
|
12
|
+
|
|
13
|
+
self.dict_mall = {}
|
|
14
|
+
self.cookie = self.doLoginToTemu(mobile, password)
|
|
15
|
+
|
|
16
|
+
# 主账户登录 返回Cookie
|
|
17
|
+
def doLoginToTemu(self, username, password):
|
|
18
|
+
cache_cookie = f'{self.config.auto_dir}/temu/cookie/cookie_{username}.json'
|
|
19
|
+
dict_cookie = read_dict_from_file(cache_cookie, 1)
|
|
20
|
+
if len(dict_cookie) > 0:
|
|
21
|
+
self.cookie = dict_cookie.get('cookie')
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
log(f'登录Temu账号: {username}')
|
|
25
|
+
|
|
26
|
+
"""使用 XPath 登录网站"""
|
|
27
|
+
# 导航到登录页面
|
|
28
|
+
self.web_page.goto("https://seller.kuajingmaihuo.com/login")
|
|
29
|
+
# fixed
|
|
30
|
+
self.web_page.locator('//div[text()="账号登录"]').click()
|
|
31
|
+
# 输入用户名
|
|
32
|
+
self.web_page.locator('//input[@id="usernameId"]').fill("")
|
|
33
|
+
self.web_page.locator('//input[@id="usernameId"]').fill(username)
|
|
34
|
+
# 输入密码
|
|
35
|
+
self.web_page.locator('//input[@id="passwordId"]').fill("")
|
|
36
|
+
self.web_page.locator('//input[@id="passwordId"]').fill(password)
|
|
37
|
+
# 勾选隐私政策(checkbox)
|
|
38
|
+
self.web_page.locator('//input[@type="checkbox"]/following-sibling::div').click() # 直接check不了 换成点击
|
|
39
|
+
# 点击登录按钮
|
|
40
|
+
self.web_page.locator('//button[span[text()="登录"]]').click()
|
|
41
|
+
# 等待登录完成(根据页面加载情况调整等待策略)
|
|
42
|
+
self.web_page.wait_for_load_state("load")
|
|
43
|
+
|
|
44
|
+
while True:
|
|
45
|
+
log('等待卖家中心出现')
|
|
46
|
+
try:
|
|
47
|
+
if self.web_page.locator('//div[text()="Temu商家中心"]').count() == 1:
|
|
48
|
+
log('卖家中心已出现')
|
|
49
|
+
break
|
|
50
|
+
if self.web_page.locator('//div[text()="Seller Central"]').count() == 1:
|
|
51
|
+
log('卖家中心已出现')
|
|
52
|
+
break
|
|
53
|
+
except Exception as e:
|
|
54
|
+
log(f"❌{e}")
|
|
55
|
+
time.sleep(1.5)
|
|
56
|
+
|
|
57
|
+
log("✅ 登录成功")
|
|
58
|
+
|
|
59
|
+
self.web_page.wait_for_load_state("load")
|
|
60
|
+
self.web_page.wait_for_timeout(3000)
|
|
61
|
+
cookies = self.web_page.context.cookies()
|
|
62
|
+
cookies_list = [cookie for cookie in cookies if '.kuajingmaihuo.com' in cookie['domain']]
|
|
63
|
+
self.cookie = '; '.join([f"{cookie['name']}={cookie['value']}" for cookie in cookies_list])
|
|
64
|
+
log(f'已获取self.cookie:', self.cookie)
|
|
65
|
+
write_dict_to_file(cache_cookie, {'cookie': self.cookie})
|
|
66
|
+
return self.cookie
|
|
67
|
+
|
|
68
|
+
def post_json(self, str_url, payload, mall_id=None):
|
|
69
|
+
global response
|
|
70
|
+
try:
|
|
71
|
+
headers = {
|
|
72
|
+
'content-type': 'application/json',
|
|
73
|
+
'priority' : 'u=1, i',
|
|
74
|
+
'referer' : 'https://seller.kuajingmaihuo.com/settle/site-main',
|
|
75
|
+
'user-agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
76
|
+
'Cookie' : self.cookie,
|
|
77
|
+
}
|
|
78
|
+
if mall_id:
|
|
79
|
+
headers.update({'mallid': f"{mall_id}"})
|
|
80
|
+
response = requests.post(str_url, headers=headers, data=json.dumps(payload))
|
|
81
|
+
response.raise_for_status() # 如果响应不正常,会抛出异常
|
|
82
|
+
|
|
83
|
+
response_text = response.json()
|
|
84
|
+
error_code = response_text.get('error_code') or response_text.get('errorCode')
|
|
85
|
+
if error_code != 1000000:
|
|
86
|
+
raise send_exception(response_text)
|
|
87
|
+
return response.json() # 直接返回 JSON 格式的数据
|
|
88
|
+
except:
|
|
89
|
+
raise send_exception()
|
|
90
|
+
|
|
91
|
+
def get_shop_list(self):
|
|
92
|
+
log(f'获取店铺列表')
|
|
93
|
+
global DictMall
|
|
94
|
+
url = "https://seller.kuajingmaihuo.com/bg/quiet/api/mms/userInfo"
|
|
95
|
+
response_text = self.post_json(url, {})
|
|
96
|
+
|
|
97
|
+
company_list = response_text['result']['companyList']
|
|
98
|
+
mall_list = []
|
|
99
|
+
for company in company_list:
|
|
100
|
+
mallList = company['malInfoList']
|
|
101
|
+
# shop_list = [['店铺ID', '店铺名称', '主账号', '店铺类型']]
|
|
102
|
+
for mall in mallList:
|
|
103
|
+
mall_id = str(mall['mallId'])
|
|
104
|
+
mall_name = str(mall['mallName'])
|
|
105
|
+
shop_info = [mall_id, mall_name, self.mobile, '半托管' if mall['isSemiManagedMall'] else '全托管']
|
|
106
|
+
write_dict_to_file_ex(self.config.temu_store_info, {mall_id: shop_info}, [mall_id])
|
|
107
|
+
|
|
108
|
+
self.dict_mall[str(mall['mallId'])] = mall['mallName']
|
|
109
|
+
|
|
110
|
+
if not mall['isSemiManagedMall']:
|
|
111
|
+
mall_list.append([str(mall['mallId']), mall['mallName'], '', self.mobile])
|
|
112
|
+
|
|
113
|
+
return mall_list
|
|
114
|
+
|
|
115
|
+
def get_funds_info(self, mall_id):
|
|
116
|
+
log(f'获取 {self.dict_mall[mall_id]} 资金信息')
|
|
117
|
+
url = "https://seller.kuajingmaihuo.com/api/merchant/payment/account/amount/info"
|
|
118
|
+
response_text = self.post_json(url, {}, mall_id)
|
|
119
|
+
total_amount = response_text.get('result').get('totalAmount')
|
|
120
|
+
available_amount = response_text.get('result').get('availableBalance')
|
|
121
|
+
|
|
122
|
+
NotifyItem = [self.dict_mall[mall_id], total_amount, available_amount, '', TimeUtils.current_datetime()]
|
|
123
|
+
|
|
124
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/funds_{TimeUtils.today_date()}.json'
|
|
125
|
+
write_dict_to_file_ex(cache_file, {mall_id: NotifyItem}, [mall_id])
|
|
126
|
+
|
|
127
|
+
return NotifyItem
|
|
128
|
+
|
|
129
|
+
def list_warehouse(self, mall_id, mall_name):
|
|
130
|
+
log(f'获取店铺 {mall_name} 销售商品列表 第1页')
|
|
131
|
+
url = "https://seller.kuajingmaihuo.com/marvel-mms/cn/api/kiana/venom/sales/management/listWarehouse"
|
|
132
|
+
payload = {
|
|
133
|
+
"pageNo" : 1,
|
|
134
|
+
"pageSize" : 40,
|
|
135
|
+
"isLack" : 0,
|
|
136
|
+
"selectStatusList" : [12], # 12 是已加入站点
|
|
137
|
+
"priceAdjustRecentDays": 30 # 近30日价格调整
|
|
138
|
+
}
|
|
139
|
+
response_text = self.post_json(url, payload, mall_id)
|
|
140
|
+
|
|
141
|
+
total = response_text['result']['total']
|
|
142
|
+
subOrderListCount = len(response_text['result']['subOrderList'])
|
|
143
|
+
totalPage = math.ceil(total / subOrderListCount) if subOrderListCount else 0
|
|
144
|
+
subOrderList = response_text['result']['subOrderList']
|
|
145
|
+
|
|
146
|
+
for page in range(2, totalPage + 1):
|
|
147
|
+
log(f'获取店铺{mall_name}销售商品列表 第{page}/{totalPage}页')
|
|
148
|
+
payload['pageNo'] = page
|
|
149
|
+
response_text = self.post_json(url, payload, mall_id)
|
|
150
|
+
subOrderList += response_text['result']['subOrderList']
|
|
151
|
+
time.sleep(0.3)
|
|
152
|
+
|
|
153
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/warehouse_list_{TimeUtils.today_date()}.json'
|
|
154
|
+
write_dict_to_file_ex(cache_file, {mall_id: subOrderList}, [mall_id])
|
|
155
|
+
|
|
156
|
+
return subOrderList
|