qrpa 1.0.23__py3-none-any.whl → 1.0.24__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/__init__.py +5 -1
- qrpa/fun_base.py +38 -0
- qrpa/fun_excel.py +37 -10
- qrpa/shein_excel.py +199 -13
- qrpa/shein_lib.py +155 -0
- qrpa/temu_chrome.py +56 -0
- qrpa/temu_excel.py +109 -0
- qrpa/temu_lib.py +154 -0
- qrpa/wxwork.py +6 -0
- {qrpa-1.0.23.dist-info → qrpa-1.0.24.dist-info}/METADATA +1 -1
- qrpa-1.0.24.dist-info/RECORD +23 -0
- qrpa-1.0.23.dist-info/RECORD +0 -20
- {qrpa-1.0.23.dist-info → qrpa-1.0.24.dist-info}/WHEEL +0 -0
- {qrpa-1.0.23.dist-info → qrpa-1.0.24.dist-info}/top_level.txt +0 -0
qrpa/__init__.py
CHANGED
|
@@ -3,7 +3,7 @@ from .db_migrator import DatabaseMigrator, DatabaseConfig, RemoteConfig, create_
|
|
|
3
3
|
|
|
4
4
|
from .shein_ziniao import ZiniaoRunner
|
|
5
5
|
|
|
6
|
-
from .fun_base import log, send_exception, md5_string, hostname, get_safe_value, sanitize_filename
|
|
6
|
+
from .fun_base import log, send_exception, md5_string, hostname, get_safe_value, sanitize_filename, get_file_size, calculate_star_symbols
|
|
7
7
|
|
|
8
8
|
from .time_utils import TimeUtils
|
|
9
9
|
|
|
@@ -18,3 +18,7 @@ from .shein_excel import SheinExcel
|
|
|
18
18
|
from .shein_lib import SheinLib
|
|
19
19
|
|
|
20
20
|
from .fun_excel import InsertImageV2
|
|
21
|
+
|
|
22
|
+
from .temu_lib import TemuLib
|
|
23
|
+
from .temu_excel import TemuExcel
|
|
24
|
+
from .temu_chrome import temu_chrome_excute
|
qrpa/fun_base.py
CHANGED
|
@@ -105,3 +105,41 @@ def copy_file(source, destination):
|
|
|
105
105
|
print(f"错误:没有权限复制到 '{destination}'")
|
|
106
106
|
except Exception as e:
|
|
107
107
|
print(f"错误:发生未知错误 - {e}")
|
|
108
|
+
|
|
109
|
+
def get_file_size(file_path, human_readable=False):
|
|
110
|
+
"""
|
|
111
|
+
获取文件大小
|
|
112
|
+
|
|
113
|
+
:param file_path: 文件路径
|
|
114
|
+
:param human_readable: 是否返回可读格式(KB, MB, GB)
|
|
115
|
+
:return: 文件大小(字节数或可读格式)
|
|
116
|
+
"""
|
|
117
|
+
if not os.path.isfile(file_path):
|
|
118
|
+
raise FileNotFoundError(f"文件不存在: {file_path}")
|
|
119
|
+
|
|
120
|
+
size_bytes = os.path.getsize(file_path)
|
|
121
|
+
|
|
122
|
+
if not human_readable:
|
|
123
|
+
return size_bytes
|
|
124
|
+
|
|
125
|
+
# 转换为可读单位
|
|
126
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
127
|
+
size = float(size_bytes)
|
|
128
|
+
for unit in units:
|
|
129
|
+
if size < 1024:
|
|
130
|
+
return f"{size:.2f} {unit}"
|
|
131
|
+
size /= 1024
|
|
132
|
+
|
|
133
|
+
def calculate_star_symbols(rating):
|
|
134
|
+
"""
|
|
135
|
+
计算星级对应的符号组合(独立评分逻辑函数)
|
|
136
|
+
参数:
|
|
137
|
+
rating (int): 标准化评分(0-5)
|
|
138
|
+
返回:
|
|
139
|
+
str: 星级符号字符串(如★★★⭐☆)
|
|
140
|
+
"""
|
|
141
|
+
full_stars = int(rating)
|
|
142
|
+
empty_stars = 5 - full_stars
|
|
143
|
+
star_string = '★' * full_stars
|
|
144
|
+
star_string += '☆' * empty_stars
|
|
145
|
+
return star_string
|
qrpa/fun_excel.py
CHANGED
|
@@ -12,6 +12,7 @@ import concurrent.futures
|
|
|
12
12
|
from collections import defaultdict
|
|
13
13
|
import threading
|
|
14
14
|
from playwright.sync_api import sync_playwright
|
|
15
|
+
import psutil
|
|
15
16
|
|
|
16
17
|
from .fun_base import log, sanitize_filename, create_file_path, copy_file, add_https, send_exception
|
|
17
18
|
|
|
@@ -89,13 +90,13 @@ def set_cell_prefix_red(cell, n, color_name):
|
|
|
89
90
|
except Exception as e:
|
|
90
91
|
print(f"设置字体颜色失败: {e}")
|
|
91
92
|
|
|
92
|
-
def sort_by_column(data, col_index,
|
|
93
|
-
if not data or
|
|
93
|
+
def sort_by_column(data, col_index, header_rows=2, reverse=True):
|
|
94
|
+
if not data or header_rows >= len(data):
|
|
94
95
|
return data
|
|
95
96
|
|
|
96
97
|
try:
|
|
97
|
-
header = data[:
|
|
98
|
-
new_data_sorted = data[
|
|
98
|
+
header = data[:header_rows]
|
|
99
|
+
new_data_sorted = data[header_rows:]
|
|
99
100
|
|
|
100
101
|
def get_key(row):
|
|
101
102
|
value = row[col_index]
|
|
@@ -541,11 +542,11 @@ def insert_fixed_scale_image(sheet, cell, image_path, scale=1.0):
|
|
|
541
542
|
|
|
542
543
|
return None
|
|
543
544
|
|
|
544
|
-
def InsertImageV2(
|
|
545
|
+
def InsertImageV2(sheet, columns=None, platform='shein', img_width=150, img_save_key=None, dir_name=None, cell_height_with_img=False):
|
|
545
546
|
if not columns:
|
|
546
547
|
return
|
|
547
548
|
|
|
548
|
-
minimize(app)
|
|
549
|
+
minimize(sheet.book.app)
|
|
549
550
|
|
|
550
551
|
# 清空所有图片
|
|
551
552
|
clear_all_pictures(sheet)
|
|
@@ -1162,6 +1163,9 @@ def check_data(data):
|
|
|
1162
1163
|
log(len(row), row)
|
|
1163
1164
|
|
|
1164
1165
|
def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
|
|
1166
|
+
log('write_data入参:', excel_path, sheet_name, 'data', format_to_text_colunm)
|
|
1167
|
+
close_excel_file(excel_path)
|
|
1168
|
+
|
|
1165
1169
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
1166
1170
|
# 清空工作表中的所有数据
|
|
1167
1171
|
sheet.clear()
|
|
@@ -1174,8 +1178,8 @@ def write_data(excel_path, sheet_name, data, format_to_text_colunm=None):
|
|
|
1174
1178
|
wb.save()
|
|
1175
1179
|
close_excel(app, wb)
|
|
1176
1180
|
|
|
1177
|
-
def colorize_by_field(
|
|
1178
|
-
minimize(app)
|
|
1181
|
+
def colorize_by_field(sheet, field):
|
|
1182
|
+
minimize(sheet.book.app)
|
|
1179
1183
|
# 读取数据
|
|
1180
1184
|
field_column = find_column_by_data(sheet, 1, field) # 假设 SPU 在 C 列
|
|
1181
1185
|
if field_column is None:
|
|
@@ -2405,6 +2409,7 @@ def format_excel_with_lock(excel_path, sheet_name, format_func, *args, **kwargs)
|
|
|
2405
2409
|
log(f"格式化失败: {e}")
|
|
2406
2410
|
return False
|
|
2407
2411
|
|
|
2412
|
+
# 经过观察 fortmat时 传入函数需要为类函数且第二个参数必须是 sheet
|
|
2408
2413
|
def batch_excel_operations(excel_path, operations):
|
|
2409
2414
|
"""
|
|
2410
2415
|
批量 Excel 操作函数,一次性打开 Excel 执行多个操作
|
|
@@ -2439,17 +2444,19 @@ def batch_excel_operations(excel_path, operations):
|
|
|
2439
2444
|
sheet.activate()
|
|
2440
2445
|
|
|
2441
2446
|
if operation_type == 'write':
|
|
2442
|
-
data, format_to_text_colunm = args[:
|
|
2447
|
+
data, format_to_text_colunm = args[0], args[1:] if len(args) > 1 else None
|
|
2443
2448
|
# 清空工作表
|
|
2444
2449
|
sheet.clear()
|
|
2445
2450
|
# 格式化文本列
|
|
2446
|
-
|
|
2451
|
+
if format_to_text_colunm:
|
|
2452
|
+
format_to_text_v2(sheet, format_to_text_colunm)
|
|
2447
2453
|
# 写入数据
|
|
2448
2454
|
sheet.range('A1').value = data
|
|
2449
2455
|
log(f"批量操作:写入数据到 {sheet_name}")
|
|
2450
2456
|
|
|
2451
2457
|
elif operation_type == 'format':
|
|
2452
2458
|
format_func, format_args = args[0], args[1:] if len(args) > 1 else ()
|
|
2459
|
+
log('格式化入参', *format_args)
|
|
2453
2460
|
# 执行格式化
|
|
2454
2461
|
format_func(sheet, *format_args)
|
|
2455
2462
|
log(f"批量操作:格式化工作表 {sheet_name}")
|
|
@@ -2478,6 +2485,26 @@ def batch_excel_operations(excel_path, operations):
|
|
|
2478
2485
|
finally:
|
|
2479
2486
|
# 释放锁但不关闭 Excel(保持复用)
|
|
2480
2487
|
excel_lock_manager.release_excel_lock(excel_path)
|
|
2488
|
+
close_excel_with_lock(excel_path, app, wb, True)
|
|
2489
|
+
|
|
2490
|
+
def close_excel_file(file_path):
|
|
2491
|
+
file_path = os.path.abspath(file_path).lower()
|
|
2492
|
+
|
|
2493
|
+
for proc in psutil.process_iter(['pid', 'name']):
|
|
2494
|
+
if proc.info['name'] and proc.info['name'].lower() in ['excel.exe', 'wps.exe']: # 只找 Excel
|
|
2495
|
+
try:
|
|
2496
|
+
for f in proc.open_files():
|
|
2497
|
+
if os.path.abspath(f.path).lower() == file_path:
|
|
2498
|
+
print(f"文件被 Excel 占用 (PID: {proc.pid}),正在关闭进程...")
|
|
2499
|
+
proc.terminate()
|
|
2500
|
+
proc.wait(timeout=3)
|
|
2501
|
+
print("已关闭。")
|
|
2502
|
+
return True
|
|
2503
|
+
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
|
2504
|
+
continue
|
|
2505
|
+
|
|
2506
|
+
print("文件没有被 Excel 占用。")
|
|
2507
|
+
return False
|
|
2481
2508
|
|
|
2482
2509
|
def force_close_excel_file(excel_path):
|
|
2483
2510
|
"""
|
qrpa/shein_excel.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from .fun_excel import *
|
|
2
|
-
from .fun_base import log
|
|
2
|
+
from .fun_base import log, calculate_star_symbols
|
|
3
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
4
|
from .time_utils import TimeUtils
|
|
5
5
|
from .wxwork import WxWorkBot
|
|
@@ -11,6 +11,192 @@ class SheinExcel:
|
|
|
11
11
|
self.config = config
|
|
12
12
|
pass
|
|
13
13
|
|
|
14
|
+
def format_funds(self, sheet):
|
|
15
|
+
beautify_title(sheet)
|
|
16
|
+
column_to_right(sheet, ['金额', '汇总'])
|
|
17
|
+
format_to_money(sheet, ['金额', '汇总'])
|
|
18
|
+
add_sum_for_cell(sheet, ['在途商品金额', '在仓商品金额', '待结算金额', '可提现金额', '销售出库金额', '汇总'])
|
|
19
|
+
add_formula_for_column(sheet, '汇总', '=SUM(D3:G3)', 3)
|
|
20
|
+
sheet.autofit()
|
|
21
|
+
|
|
22
|
+
def format_bad_comment(self, sheet):
|
|
23
|
+
beautify_title(sheet)
|
|
24
|
+
column_to_left(sheet, ['商品信息'])
|
|
25
|
+
autofit_column(sheet, ['买家评价', '时间信息', '标签关键词'])
|
|
26
|
+
specify_column_width(sheet, ['买家评价', '商品信息'], 150 / 6)
|
|
27
|
+
color_for_column(sheet, ['买家评分'], '红色')
|
|
28
|
+
colorize_by_field(sheet, 'skc')
|
|
29
|
+
add_borders(sheet)
|
|
30
|
+
InsertImageV2(sheet, ['商品图片', '图1', '图2', '图3', '图4', '图5'])
|
|
31
|
+
|
|
32
|
+
def write_bad_comment(self):
|
|
33
|
+
excel_path = create_file_path(self.config.excel_bad_comment)
|
|
34
|
+
header = ['评价ID', '商品图片', '商品信息', '买家评分', '买家评价', '标签关键词', '区域', '时间信息', '有图', '图1',
|
|
35
|
+
'图2', '图3', '图4', '图5', 'skc']
|
|
36
|
+
summary_excel_data = [header]
|
|
37
|
+
|
|
38
|
+
cache_file = f'{self.config.auto_dir}/shein/dict/comment_list_{TimeUtils.today_date()}.json'
|
|
39
|
+
dict = read_dict_from_file(cache_file)
|
|
40
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
41
|
+
|
|
42
|
+
for store_username, comment_list in dict.items():
|
|
43
|
+
store_name = dict_store.get(store_username)
|
|
44
|
+
sheet_name = store_name
|
|
45
|
+
|
|
46
|
+
store_excel_data = [header]
|
|
47
|
+
for comment in comment_list:
|
|
48
|
+
row_item = []
|
|
49
|
+
row_item.append(comment['commentId'])
|
|
50
|
+
row_item.append(comment['goodsThumb'])
|
|
51
|
+
product_info = f'属性:{comment["goodsAttribute"]}\n货号:{comment["goodSn"]}\nSPU:{comment["spu"]}\nSKC:{comment["skc"]}\nSKU:{comment["sku"]}'
|
|
52
|
+
row_item.append(product_info)
|
|
53
|
+
row_item.append(calculate_star_symbols(comment['goodsCommentStar']))
|
|
54
|
+
row_item.append(comment['goodsCommentContent'])
|
|
55
|
+
qualityLabel = '存在质量问题\n' if comment['isQualityLabel'] == 1 else ''
|
|
56
|
+
bad_comment_label = qualityLabel + '\n'.join([item['labelName'] for item in comment['badCommentLabelList']])
|
|
57
|
+
|
|
58
|
+
row_item.append(bad_comment_label)
|
|
59
|
+
row_item.append(comment['dataCenterName'])
|
|
60
|
+
time_info = f'下单时间:{comment["orderTime"]}\n评论时间:{comment["commentTime"]}'
|
|
61
|
+
row_item.append(time_info)
|
|
62
|
+
|
|
63
|
+
# 获取图片数量
|
|
64
|
+
image_num = len(comment.get('goodsCommentImages', []))
|
|
65
|
+
# 设置imgFlag值(如果comment中没有imgFlag字段,默认设为0)
|
|
66
|
+
imgFlag = image_num if comment.get('imgFlag') == 1 else 0
|
|
67
|
+
row_item.append(imgFlag)
|
|
68
|
+
|
|
69
|
+
images = comment.get('goodsCommentImages', [])
|
|
70
|
+
for i in range(5):
|
|
71
|
+
row_item.append(images[i] if i < len(images) else '')
|
|
72
|
+
|
|
73
|
+
row_item.append(comment['skc'])
|
|
74
|
+
|
|
75
|
+
store_excel_data.append(row_item)
|
|
76
|
+
summary_excel_data.append(row_item)
|
|
77
|
+
|
|
78
|
+
# write_data(excel_path, sheet_name, store_excel_data)
|
|
79
|
+
# format_bad_comment(excel_path, sheet_name)
|
|
80
|
+
|
|
81
|
+
sheet_name = 'Sheet1'
|
|
82
|
+
|
|
83
|
+
batch_excel_operations(excel_path, [
|
|
84
|
+
(sheet_name, 'write', summary_excel_data),
|
|
85
|
+
(sheet_name, 'format', self.format_bad_comment),
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
def write_funds(self):
|
|
89
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/stat_fund_{TimeUtils.today_date()}.json'
|
|
90
|
+
dict = read_dict_from_file(cache_file)
|
|
91
|
+
data = []
|
|
92
|
+
for key, val in dict.items():
|
|
93
|
+
data.append(val)
|
|
94
|
+
|
|
95
|
+
excel_path = create_file_path(self.config.excel_shein_fund)
|
|
96
|
+
sheet_name = 'Sheet1'
|
|
97
|
+
data.insert(0, ['汇总', '', '', '', '', '', '', '', '', ''])
|
|
98
|
+
data.insert(0, ['店铺名称', '店铺账号', '店长', '在途商品金额', '在仓商品金额', '待结算金额', '可提现金额',
|
|
99
|
+
'销售出库金额', '汇总', '导出时间'])
|
|
100
|
+
batch_excel_operations(excel_path, [
|
|
101
|
+
('Sheet1', 'write', sort_by_column(data, 7, 2)),
|
|
102
|
+
('Sheet1', 'format', self.format_funds),
|
|
103
|
+
])
|
|
104
|
+
WxWorkBot('b30aaa8d-1a1f-4378-841a-8b0f8295f2d9').send_file(excel_path)
|
|
105
|
+
|
|
106
|
+
def format_skc_quality(self, sheet):
|
|
107
|
+
beautify_title(sheet)
|
|
108
|
+
colorize_by_field(sheet, 'skc')
|
|
109
|
+
add_borders(sheet)
|
|
110
|
+
InsertImageV2(sheet, ['商品图片'])
|
|
111
|
+
|
|
112
|
+
def sort_site_desc_by_sale_cnt_14d(self, data, reverse=True):
|
|
113
|
+
"""
|
|
114
|
+
对data中的site_desc_vo_list按照skc_site_sale_cnt_14d进行排序
|
|
115
|
+
|
|
116
|
+
参数:
|
|
117
|
+
data: 包含site_desc_vo_list的字典
|
|
118
|
+
reverse: 是否倒序排序,默认为True(从大到小)
|
|
119
|
+
|
|
120
|
+
返回:
|
|
121
|
+
排序后的data(原数据会被修改)
|
|
122
|
+
"""
|
|
123
|
+
if 'site_desc_vo_list' in data and isinstance(data['site_desc_vo_list'], list):
|
|
124
|
+
# 处理None值,将它们放在排序结果的最后
|
|
125
|
+
data['site_desc_vo_list'].sort(
|
|
126
|
+
key=lambda x: float('-inf') if x.get('skc_site_sale_cnt_14d') is None else x['skc_site_sale_cnt_14d'],
|
|
127
|
+
reverse=reverse
|
|
128
|
+
)
|
|
129
|
+
return data
|
|
130
|
+
|
|
131
|
+
def write_skc_quality_estimate(self):
|
|
132
|
+
excel_path = create_file_path(self.config.excel_skc_quality_estimate)
|
|
133
|
+
header = ['店铺信息', '商品图片', '统计日期', '国家', '当日销量', '14日销量', '14日销量占比', '质量等级',
|
|
134
|
+
'客评数/客评分', '差评数/差评率', '退货数/退货率', 'skc', 'skc当日销量', 'skc14日销量', 'skc14日销量占比']
|
|
135
|
+
summary_excel_data = [header]
|
|
136
|
+
|
|
137
|
+
stat_date = TimeUtils.before_yesterday()
|
|
138
|
+
cache_file = f'{self.config.auto_dir}/shein/dict/googs_estimate_{stat_date}.json'
|
|
139
|
+
dict = read_dict_from_file(cache_file)
|
|
140
|
+
if len(dict) == 0:
|
|
141
|
+
log('昨日质量评估数据不存在')
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
145
|
+
|
|
146
|
+
operations = []
|
|
147
|
+
for store_username, skc_list in dict.items():
|
|
148
|
+
store_name = dict_store.get(store_username)
|
|
149
|
+
sheet_name = store_name
|
|
150
|
+
|
|
151
|
+
store_excel_data = [header]
|
|
152
|
+
for skc_item in skc_list:
|
|
153
|
+
sorted_skc_item = self.sort_site_desc_by_sale_cnt_14d(skc_item, True)
|
|
154
|
+
# for site in sorted_skc_item['site_desc_vo_list']:
|
|
155
|
+
# print(f"{site['country_site']}: {site['skc_site_sale_cnt_14d']}")
|
|
156
|
+
# continue
|
|
157
|
+
store_info = f'{store_name}'
|
|
158
|
+
skc = sorted_skc_item['skc']
|
|
159
|
+
sites = sorted_skc_item['site_desc_vo_list']
|
|
160
|
+
skc_sale_cnt = sorted_skc_item['skc_sale_cnt']
|
|
161
|
+
skc_sale_cnt_14d = sorted_skc_item['skc_sale_cnt_14d']
|
|
162
|
+
skc_sale_rate_14d = sorted_skc_item['skc_sale_rate_14d']
|
|
163
|
+
for site in sites:
|
|
164
|
+
row_item = []
|
|
165
|
+
row_item.append(store_info)
|
|
166
|
+
row_item.append(skc_item['goods_image'])
|
|
167
|
+
row_item.append(stat_date)
|
|
168
|
+
row_item.append(site['country_site'])
|
|
169
|
+
row_item.append(site['skc_site_sale_cnt'])
|
|
170
|
+
cnt_14d = site['skc_site_sale_cnt_14d']
|
|
171
|
+
if cnt_14d is None or cnt_14d <= 0:
|
|
172
|
+
continue
|
|
173
|
+
row_item.append(cnt_14d)
|
|
174
|
+
row_item.append(site['skc_site_sale_rate_14d'])
|
|
175
|
+
row_item.append(site['quality_level'])
|
|
176
|
+
customer_info = f'{site["customer_evaluate_num"]}/{site["customer_evaluate_score"][:-1]}'
|
|
177
|
+
row_item.append(customer_info)
|
|
178
|
+
negative_info = f'{site["negative_quantity"]}/{site["negative_percent"]}'
|
|
179
|
+
row_item.append(negative_info)
|
|
180
|
+
return_info = f'{site["goods_return_quantity"]}/{site["goods_return_percent"]}'
|
|
181
|
+
row_item.append(return_info)
|
|
182
|
+
row_item.append(skc)
|
|
183
|
+
row_item.append(skc_sale_cnt)
|
|
184
|
+
row_item.append(skc_sale_cnt_14d)
|
|
185
|
+
row_item.append(skc_sale_rate_14d)
|
|
186
|
+
store_excel_data.append(row_item)
|
|
187
|
+
summary_excel_data.append(row_item)
|
|
188
|
+
|
|
189
|
+
operations.append((
|
|
190
|
+
sheet_name, 'write', store_excel_data
|
|
191
|
+
))
|
|
192
|
+
operations.append((
|
|
193
|
+
sheet_name, 'format', self.format_skc_quality
|
|
194
|
+
))
|
|
195
|
+
operations.append((
|
|
196
|
+
'Sheet1', 'delete'
|
|
197
|
+
))
|
|
198
|
+
batch_excel_operations(excel_path, operations)
|
|
199
|
+
|
|
14
200
|
def write_sales_data(self):
|
|
15
201
|
yesterday = TimeUtils.get_yesterday()
|
|
16
202
|
model = SheinStoreSalesDetailManager(self.config.database_url)
|
|
@@ -333,9 +519,9 @@ class SheinExcel:
|
|
|
333
519
|
add_formula_for_column(sheet, '本地和采购可售天数', '=IF(H2>0, (F2+G2)/H2,0)')
|
|
334
520
|
add_formula_for_column(sheet, '建议采购', '=IF(I2 > J2,0,E2)')
|
|
335
521
|
|
|
336
|
-
colorize_by_field(
|
|
522
|
+
colorize_by_field(sheet, 'SKC')
|
|
337
523
|
specify_column_width(sheet, ['商品信息'], 180 / 6)
|
|
338
|
-
InsertImageV2(
|
|
524
|
+
InsertImageV2(sheet, ['SKC图片', 'SKU图片'])
|
|
339
525
|
wb.save()
|
|
340
526
|
close_excel(app, wb)
|
|
341
527
|
if mode == 4:
|
|
@@ -372,11 +558,11 @@ class SheinExcel:
|
|
|
372
558
|
new_excel_path_list.append(new_excel_path)
|
|
373
559
|
sheet_name = 'Sheet1'
|
|
374
560
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
561
|
+
log(new_excel_path)
|
|
562
|
+
if mode in [2]:
|
|
563
|
+
excel_data = sort_by_column(excel_data, 4, 1)
|
|
564
|
+
write_data(new_excel_path, sheet_name, excel_data)
|
|
565
|
+
self.format_bak_advice(new_excel_path, sheet_name, mode)
|
|
380
566
|
|
|
381
567
|
# 是否合并表格数据
|
|
382
568
|
if mode in [1, 3]:
|
|
@@ -411,7 +597,7 @@ class SheinExcel:
|
|
|
411
597
|
beautify_title(sheet)
|
|
412
598
|
add_borders(sheet)
|
|
413
599
|
column_to_left(sheet, ['活动信息'])
|
|
414
|
-
colorize_by_field(
|
|
600
|
+
colorize_by_field(sheet, '店铺名称')
|
|
415
601
|
autofit_column(sheet, ['店铺名称', '活动信息'])
|
|
416
602
|
wb.save()
|
|
417
603
|
close_excel(app, wb)
|
|
@@ -462,11 +648,11 @@ class SheinExcel:
|
|
|
462
648
|
app, wb, sheet = open_excel(excel_path, sheet_name)
|
|
463
649
|
beautify_title(sheet)
|
|
464
650
|
add_borders(sheet)
|
|
465
|
-
colorize_by_field(
|
|
651
|
+
colorize_by_field(sheet, 'SKC')
|
|
466
652
|
column_to_left(sheet, ["商品信息", "近7天SKU销量/SKC销量/SKC曝光", "SKC点击率/SKC转化率", "自主参与活动"])
|
|
467
653
|
autofit_column(sheet,
|
|
468
654
|
['店铺名称', '商品信息', "近7天SKU销量/SKC销量/SKC曝光", "SKC点击率/SKC转化率", "自主参与活动"])
|
|
469
|
-
InsertImageV2(
|
|
655
|
+
InsertImageV2(sheet, ['SKC图片', 'SKU图片'])
|
|
470
656
|
wb.save()
|
|
471
657
|
close_excel(app, wb)
|
|
472
658
|
WxWorkBot('b30aaa8d-1a1f-4378-841a-8b0f8295f2d9').send_file(excel_path)
|
|
@@ -498,12 +684,12 @@ class SheinExcel:
|
|
|
498
684
|
format_to_money(sheet, ['申报价', '成本价', '毛利润', '利润'])
|
|
499
685
|
format_to_percent(sheet, ['支付率', '点击率', '毛利率'])
|
|
500
686
|
self.dealFormula(sheet) # 有空再封装优化
|
|
501
|
-
colorize_by_field(
|
|
687
|
+
colorize_by_field(sheet, 'SPU')
|
|
502
688
|
autofit_column(sheet, ['商品信息', '店铺名称', 'SKC点击率/SKC转化率', '自主参与活动'])
|
|
503
689
|
column_to_left(sheet, ['店铺名称', 'SKC点击率/SKC转化率', '自主参与活动'])
|
|
504
690
|
specify_column_width(sheet, ['商品标题'], 150 / 6)
|
|
505
691
|
add_borders(sheet)
|
|
506
|
-
InsertImageV2(
|
|
692
|
+
InsertImageV2(sheet, ['SKC图片', 'SKU图片'], 'shein', 120, None, None, True)
|
|
507
693
|
wb.save()
|
|
508
694
|
close_excel(app, wb)
|
|
509
695
|
|
qrpa/shein_lib.py
CHANGED
|
@@ -22,6 +22,7 @@ class SheinLib:
|
|
|
22
22
|
self.store_name = store_name
|
|
23
23
|
self.web_page = web_page
|
|
24
24
|
self.dt = None
|
|
25
|
+
self.DictQueryTime = {}
|
|
25
26
|
|
|
26
27
|
self.deal_auth()
|
|
27
28
|
|
|
@@ -82,6 +83,160 @@ class SheinLib:
|
|
|
82
83
|
# web_page.goto('https://sso.geiwohuo.com')
|
|
83
84
|
log('鉴权处理结束')
|
|
84
85
|
|
|
86
|
+
def get_comment_list(self):
|
|
87
|
+
cache_file = f'{self.config.auto_dir}/shein/dict/comment_list_{TimeUtils.today_date()}.json'
|
|
88
|
+
comment_list = read_dict_from_file_ex(cache_file, self.store_username, 3600)
|
|
89
|
+
if len(comment_list) > 0:
|
|
90
|
+
return comment_list
|
|
91
|
+
|
|
92
|
+
page_num = 1
|
|
93
|
+
page_size = 50
|
|
94
|
+
|
|
95
|
+
yesterday = TimeUtils.get_yesterday()
|
|
96
|
+
|
|
97
|
+
url = f"https://sso.geiwohuo.com/gsp/goods/comment/list"
|
|
98
|
+
payload = {
|
|
99
|
+
"page" : page_num,
|
|
100
|
+
"perPage" : page_size,
|
|
101
|
+
"startCommentTime": f"{yesterday} 00:00:00",
|
|
102
|
+
"commentEndTime" : f"{yesterday} 23:59:59",
|
|
103
|
+
"commentStarList" : ["3", "2", "1"]
|
|
104
|
+
}
|
|
105
|
+
response_text = fetch(self.web_page, url, payload)
|
|
106
|
+
error_code = response_text.get('code')
|
|
107
|
+
if str(error_code) != '0':
|
|
108
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
109
|
+
|
|
110
|
+
comment_list = response_text['info']['data']
|
|
111
|
+
total = response_text['info']['meta']['count']
|
|
112
|
+
totalPage = math.ceil(total / page_size)
|
|
113
|
+
|
|
114
|
+
for page in range(2, totalPage + 1):
|
|
115
|
+
log(f'获取评价列表 第{page}/{totalPage}页')
|
|
116
|
+
page_num = page
|
|
117
|
+
payload['page'] = page_num
|
|
118
|
+
response_text = fetch(self.web_page, url, payload)
|
|
119
|
+
comment_list = response_text['info']['data']
|
|
120
|
+
time.sleep(0.1)
|
|
121
|
+
|
|
122
|
+
write_dict_to_file_ex(cache_file, {self.store_username: comment_list}, [self.store_username])
|
|
123
|
+
return comment_list
|
|
124
|
+
|
|
125
|
+
def get_last_month_outbound_amount(self):
|
|
126
|
+
url = "https://sso.geiwohuo.com/mils/report/month/list"
|
|
127
|
+
start, end = TimeUtils.get_current_year_range()
|
|
128
|
+
payload = {
|
|
129
|
+
"reportDateStart": start, "reportDateEnd": end, "pageNumber": 1, "pageSize": 50
|
|
130
|
+
}
|
|
131
|
+
response_text = fetch(self.web_page, url, payload)
|
|
132
|
+
error_code = response_text.get('code')
|
|
133
|
+
if str(error_code) != '0':
|
|
134
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
135
|
+
info = response_text.get('info')
|
|
136
|
+
lst = info.get('data', {}).get('list', [])
|
|
137
|
+
if not lst:
|
|
138
|
+
log(f'⚠️ {self.store_name} 最近一个月无出库记录,金额为0')
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
last_item = lst[-1]
|
|
142
|
+
log(f'正在获取 {self.store_name} 最近一个月出库金额: {last_item["totalCustomerAmount"]}')
|
|
143
|
+
return last_item['totalCustomerAmount']
|
|
144
|
+
|
|
145
|
+
def get_funds_data(self):
|
|
146
|
+
log(f'正在获取 {self.store_name} 财务数据')
|
|
147
|
+
url = "https://sso.geiwohuo.com/sso/homePage/dataOverview/v2/detail"
|
|
148
|
+
payload = {
|
|
149
|
+
"metaIndexIds": [
|
|
150
|
+
298,
|
|
151
|
+
67,
|
|
152
|
+
70,
|
|
153
|
+
72
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
response_text = fetch(self.web_page, url, payload)
|
|
157
|
+
error_code = response_text.get('code')
|
|
158
|
+
if str(error_code) != '0':
|
|
159
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
160
|
+
info = response_text.get('info')
|
|
161
|
+
num298 = 0 # 在途商品金额
|
|
162
|
+
num67 = 0 # 在仓商品金额
|
|
163
|
+
num70 = 0 # 待结算金额
|
|
164
|
+
num72 = 0 # 可提现金额
|
|
165
|
+
for item in info['list']:
|
|
166
|
+
if item['metaIndexId'] == 298:
|
|
167
|
+
num298 = item['count']
|
|
168
|
+
if item['metaIndexId'] == 67:
|
|
169
|
+
num67 = item['count']
|
|
170
|
+
if item['metaIndexId'] == 70:
|
|
171
|
+
num70 = item['count']
|
|
172
|
+
if item['metaIndexId'] == 72:
|
|
173
|
+
num72 = item['count']
|
|
174
|
+
|
|
175
|
+
outAmount = self.get_last_month_outbound_amount()
|
|
176
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
177
|
+
store_manager = dict_store.get(str(self.store_username).lower())
|
|
178
|
+
NotifyItem = [f'{self.store_name}', self.store_username, store_manager, num298, num67, num70, num72, outAmount, '',
|
|
179
|
+
TimeUtils.current_datetime()]
|
|
180
|
+
|
|
181
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/stat_fund_{TimeUtils.today_date()}.json'
|
|
182
|
+
write_dict_to_file_ex(cache_file, {self.store_username: NotifyItem}, [self.store_username])
|
|
183
|
+
return NotifyItem
|
|
184
|
+
|
|
185
|
+
def getQueryDate(self):
|
|
186
|
+
query_time = self.DictQueryTime.get(self.store_username, None)
|
|
187
|
+
if query_time is not None:
|
|
188
|
+
log(f'从字典获取query_time: {query_time}')
|
|
189
|
+
return query_time
|
|
190
|
+
log('获取日期范围')
|
|
191
|
+
url = "https://sso.geiwohuo.com/mgs-api-prefix/estimate/queryDateRange"
|
|
192
|
+
payload = {}
|
|
193
|
+
response_text = fetch(self.web_page, url, payload)
|
|
194
|
+
error_code = response_text.get('code')
|
|
195
|
+
if str(error_code) != '0':
|
|
196
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
197
|
+
query_time = response_text.get('info').get('quality_goods_query_time')
|
|
198
|
+
self.DictQueryTime.update({self.store_username: query_time})
|
|
199
|
+
log(f'query_time: {query_time}')
|
|
200
|
+
return query_time
|
|
201
|
+
|
|
202
|
+
def get_goods_quality_estimate_list(self, query_date):
|
|
203
|
+
cache_file = f'{self.config.auto_dir}/shein/dict/googs_estimate_{query_date}.json'
|
|
204
|
+
estimate_list = read_dict_from_file_ex(cache_file, self.store_username, 3600 * 8)
|
|
205
|
+
if len(estimate_list) > 0:
|
|
206
|
+
return estimate_list
|
|
207
|
+
|
|
208
|
+
page_num = 1
|
|
209
|
+
page_size = 100
|
|
210
|
+
|
|
211
|
+
url = f"https://sso.geiwohuo.com/mgs-api-prefix/estimate/queryNewQualityGoodsList"
|
|
212
|
+
payload = {
|
|
213
|
+
"page_no" : page_num,
|
|
214
|
+
"page_size" : page_size,
|
|
215
|
+
"start_date": query_date,
|
|
216
|
+
"end_date" : query_date,
|
|
217
|
+
"order_col" : "skc_sale_cnt_14d",
|
|
218
|
+
"order_type": "desc"
|
|
219
|
+
}
|
|
220
|
+
response_text = fetch(self.web_page, url, payload, {'lan': 'CN'})
|
|
221
|
+
error_code = response_text.get('code')
|
|
222
|
+
if str(error_code) != '0':
|
|
223
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
224
|
+
|
|
225
|
+
estimate_list = response_text['info']['data']
|
|
226
|
+
total = response_text['info']['meta']['count']
|
|
227
|
+
totalPage = math.ceil(total / page_size)
|
|
228
|
+
|
|
229
|
+
for page in range(2, totalPage + 1):
|
|
230
|
+
log(f'获取质量评估列表 第{page}/{totalPage}页')
|
|
231
|
+
page_num = page
|
|
232
|
+
payload['page'] = page_num
|
|
233
|
+
response_text = fetch(self.web_page, url, payload)
|
|
234
|
+
estimate_list = response_text['info']['data']
|
|
235
|
+
time.sleep(0.1)
|
|
236
|
+
|
|
237
|
+
write_dict_to_file_ex(cache_file, {self.store_username: estimate_list}, [self.store_username])
|
|
238
|
+
return estimate_list
|
|
239
|
+
|
|
85
240
|
# 已上架备货款A数量
|
|
86
241
|
def get_product_bak_A_count(self):
|
|
87
242
|
url = "https://sso.geiwohuo.com/idms/goods-skc/list"
|
qrpa/temu_chrome.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from playwright.sync_api import sync_playwright, Page
|
|
2
|
+
from qrpa import get_progress_json_ex, done_progress_json_ex, send_exception
|
|
3
|
+
from qrpa import TemuLib, get_chrome_page_v3, log
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Optional, Callable, List, Dict, Any
|
|
6
|
+
|
|
7
|
+
"""不要test开头命名文件 否则会用pytest运行这个程序"""
|
|
8
|
+
|
|
9
|
+
def temu_chrome_excute(settings, run_prepare: Optional[Callable] = None, run: Optional[Callable] = None, run_summary: Optional[Callable] = None, run_notify: Optional[Callable] = None, key_id: Optional[str] = None, just_usernames: Optional[List] = None, just_mall_ids: Optional[List] = None):
|
|
10
|
+
run_prepare()
|
|
11
|
+
with sync_playwright() as p:
|
|
12
|
+
count = 0
|
|
13
|
+
while True:
|
|
14
|
+
try:
|
|
15
|
+
count += 1
|
|
16
|
+
with get_chrome_page_v3(p) as (browser, context, web_page):
|
|
17
|
+
web_page: Page # 显式注解
|
|
18
|
+
|
|
19
|
+
for account in settings.temu_account_list:
|
|
20
|
+
username = account[0]
|
|
21
|
+
password = account[1]
|
|
22
|
+
|
|
23
|
+
if just_usernames and username not in just_usernames:
|
|
24
|
+
continue
|
|
25
|
+
|
|
26
|
+
if get_progress_json_ex(settings, key_id, username):
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
temu_client = TemuLib(settings, username, password, web_page)
|
|
30
|
+
shop_list = temu_client.get_shop_list()
|
|
31
|
+
# 增加每个店铺的处理进度
|
|
32
|
+
for shop in shop_list:
|
|
33
|
+
mall_id = shop[0]
|
|
34
|
+
mall_name = shop[1]
|
|
35
|
+
if just_mall_ids and mall_id not in just_mall_ids:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
store_name = f'{mall_id}_{mall_name}'
|
|
39
|
+
if not get_progress_json_ex(settings, key_id, store_name):
|
|
40
|
+
log(f"正在处理店铺: {mall_name},{mall_id},{username}")
|
|
41
|
+
run(temu_client, web_page, mall_id, mall_name)
|
|
42
|
+
done_progress_json_ex(settings, key_id, store_name)
|
|
43
|
+
|
|
44
|
+
done_progress_json_ex(settings, key_id, username)
|
|
45
|
+
|
|
46
|
+
if not get_progress_json_ex(settings, key_id, 'run_summary'):
|
|
47
|
+
run_summary()
|
|
48
|
+
done_progress_json_ex(settings, key_id, 'run_summary')
|
|
49
|
+
if not get_progress_json_ex(settings, key_id, 'run_notify'):
|
|
50
|
+
run_notify()
|
|
51
|
+
done_progress_json_ex(settings, key_id, 'run_notify')
|
|
52
|
+
break
|
|
53
|
+
except:
|
|
54
|
+
send_exception()
|
|
55
|
+
if count > 1:
|
|
56
|
+
break
|
qrpa/temu_excel.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
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
|
+
|
|
7
|
+
class TemuExcel:
|
|
8
|
+
|
|
9
|
+
def __init__(self, config, bridge):
|
|
10
|
+
self.config = config
|
|
11
|
+
self.bridge = bridge
|
|
12
|
+
|
|
13
|
+
def write_funds(self):
|
|
14
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/funds_{TimeUtils.today_date()}.json'
|
|
15
|
+
dict = read_dict_from_file(cache_file)
|
|
16
|
+
data = []
|
|
17
|
+
for key, val in dict.items():
|
|
18
|
+
data.append(val)
|
|
19
|
+
|
|
20
|
+
excel_path = create_file_path(self.config.excel_temu_fund)
|
|
21
|
+
data.insert(0, ['汇总', '', '', '', ''])
|
|
22
|
+
data.insert(0, ['店铺名称', '总金额', '可用余额', '-', '导出时间'])
|
|
23
|
+
log(data)
|
|
24
|
+
# 删除第 4 列(索引为 3)
|
|
25
|
+
for row in data:
|
|
26
|
+
row.pop(3) # 删除每行中索引为 3 的元素
|
|
27
|
+
|
|
28
|
+
write_data(excel_path, 'Sheet1', data)
|
|
29
|
+
|
|
30
|
+
app, wb, sheet = open_excel(excel_path, 'Sheet1')
|
|
31
|
+
beautify_title(sheet)
|
|
32
|
+
format_to_money(sheet, ['金额', '余额'])
|
|
33
|
+
format_to_datetime(sheet, ['时间'])
|
|
34
|
+
add_sum_for_cell(sheet, ['总金额', '可用余额'])
|
|
35
|
+
add_borders(sheet)
|
|
36
|
+
close_excel(app, wb)
|
|
37
|
+
|
|
38
|
+
def format_purchase_advise_batch(self, sheet):
|
|
39
|
+
beautify_title(sheet)
|
|
40
|
+
format_to_datetime(sheet, ['时间'])
|
|
41
|
+
format_to_number(sheet, ['平均日销', '本地和采购可售天数', '建议采购'], 1)
|
|
42
|
+
add_borders(sheet)
|
|
43
|
+
add_formula_for_column(sheet, '平均日销', '=G2/7')
|
|
44
|
+
add_formula_for_column(sheet, '本地和采购可售天数', '=IF(H2>0,(E2+F2)/H2,0)')
|
|
45
|
+
add_formula_for_column(sheet, '建议采购', '=IF(J2>I2,H2*9,0)')
|
|
46
|
+
colorize_by_field(sheet, 'SKC')
|
|
47
|
+
autofit_column(sheet, ['店铺名称', '商品信息'])
|
|
48
|
+
column_to_left(sheet, ['商品信息'])
|
|
49
|
+
InsertImageV2(sheet, ['SKC图片', 'SKU图片'], 'temu', 120)
|
|
50
|
+
|
|
51
|
+
def write_purchase_advise(self, erp='mb'):
|
|
52
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/warehouse_list_{TimeUtils.today_date()}.json'
|
|
53
|
+
dict = read_dict_from_file(cache_file)
|
|
54
|
+
|
|
55
|
+
store_info = read_dict_from_file(self.config.temu_store_info)
|
|
56
|
+
|
|
57
|
+
header = ['店铺名称', 'SKC图片', 'SKU图片', '商品信息', '现有库存数量', '已采购数量', '近7日销量', '平均日销', '本地和采购可售天数', '生产天数', '建议采购', '产品起定量', '备货周期(天)', 'SKC', '导出时间']
|
|
58
|
+
new_excel_path_list = []
|
|
59
|
+
for mall_id, subOrderList in dict.items():
|
|
60
|
+
excel_data = []
|
|
61
|
+
mall_name = store_info.get(mall_id)[1]
|
|
62
|
+
for product in subOrderList:
|
|
63
|
+
spu = str(product['productId']) # temu平台 spu_id
|
|
64
|
+
skc = str(product['productSkcId']) # temu平台 skc_id
|
|
65
|
+
skcExtCode = product['skcExtCode'] # 商家 SKC货号
|
|
66
|
+
category = product['category'] # 叶子类目
|
|
67
|
+
onSalesDurationOffline = product['onSalesDurationOffline'] # 加入站点时长
|
|
68
|
+
for sku in product['skuQuantityDetailList']:
|
|
69
|
+
priceReviewStatus = sku['priceReviewStatus']
|
|
70
|
+
if priceReviewStatus == 3: # 过滤 开款价格状态 已作废的 2是已生效
|
|
71
|
+
continue
|
|
72
|
+
mall_info = f'{mall_name}\n{mall_id}'
|
|
73
|
+
productSkcPicture = product['productSkcPicture'] # skc图片
|
|
74
|
+
skuExtCode = str(sku['skuExtCode']) # sku货号
|
|
75
|
+
sku_img = self.bridge.get_sku_img(skuExtCode, erp)
|
|
76
|
+
stock = self.bridge.get_sku_stock(skuExtCode, erp)
|
|
77
|
+
|
|
78
|
+
product_info = f"SPU: {spu}\nSKC: {skc}\nSKC货号: {skcExtCode}\nSKU货号: {skuExtCode}\n属性集: {sku['className']}\n类目: {category}\n加入站点时长: {onSalesDurationOffline}天\n"
|
|
79
|
+
|
|
80
|
+
row_item = []
|
|
81
|
+
row_item.append(mall_info)
|
|
82
|
+
row_item.append(productSkcPicture)
|
|
83
|
+
row_item.append(sku_img)
|
|
84
|
+
row_item.append(product_info)
|
|
85
|
+
row_item.append(stock)
|
|
86
|
+
row_item.append(0)
|
|
87
|
+
row_item.append(sku['lastSevenDaysSaleVolume'])
|
|
88
|
+
row_item.append(0)
|
|
89
|
+
row_item.append(0)
|
|
90
|
+
row_item.append(7)
|
|
91
|
+
row_item.append(0)
|
|
92
|
+
row_item.append(0)
|
|
93
|
+
row_item.append(0)
|
|
94
|
+
row_item.append(skc)
|
|
95
|
+
row_item.append(TimeUtils.current_datetime())
|
|
96
|
+
excel_data.append(row_item)
|
|
97
|
+
|
|
98
|
+
new_excel_path = str(self.config.excel_purcase_advice_temu).replace('#store_name#', mall_name).replace(' ', '_')
|
|
99
|
+
new_excel_path_list.append(new_excel_path)
|
|
100
|
+
sheet_name = 'Sheet1'
|
|
101
|
+
data = [header] + excel_data
|
|
102
|
+
close_excel_file(new_excel_path)
|
|
103
|
+
log(new_excel_path)
|
|
104
|
+
batch_excel_operations(new_excel_path, [
|
|
105
|
+
(sheet_name, 'write', sort_by_column(data, 6, 1), ['N']),
|
|
106
|
+
(sheet_name, 'format', self.format_purchase_advise_batch)
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
return new_excel_path_list
|
qrpa/temu_lib.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
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
|
+
# 输入用户名
|
|
30
|
+
self.web_page.locator('//input[@id="usernameId"]').fill("")
|
|
31
|
+
self.web_page.locator('//input[@id="usernameId"]').fill(username)
|
|
32
|
+
# 输入密码
|
|
33
|
+
self.web_page.locator('//input[@id="passwordId"]').fill("")
|
|
34
|
+
self.web_page.locator('//input[@id="passwordId"]').fill(password)
|
|
35
|
+
# 勾选隐私政策(checkbox)
|
|
36
|
+
self.web_page.locator('//input[@type="checkbox"]/following-sibling::div').click() # 直接check不了 换成点击
|
|
37
|
+
# 点击登录按钮
|
|
38
|
+
self.web_page.locator('//button[span[text()="登录"]]').click()
|
|
39
|
+
# 等待登录完成(根据页面加载情况调整等待策略)
|
|
40
|
+
self.web_page.wait_for_load_state("load")
|
|
41
|
+
|
|
42
|
+
while True:
|
|
43
|
+
log('等待卖家中心出现')
|
|
44
|
+
try:
|
|
45
|
+
if self.web_page.locator('//div[text()="Temu商家中心"]').count() == 1:
|
|
46
|
+
log('卖家中心已出现')
|
|
47
|
+
break
|
|
48
|
+
if self.web_page.locator('//div[text()="Seller Central"]').count() == 1:
|
|
49
|
+
log('卖家中心已出现')
|
|
50
|
+
break
|
|
51
|
+
except Exception as e:
|
|
52
|
+
log(f"❌{e}")
|
|
53
|
+
time.sleep(1.5)
|
|
54
|
+
|
|
55
|
+
log("✅ 登录成功")
|
|
56
|
+
|
|
57
|
+
self.web_page.wait_for_load_state("load")
|
|
58
|
+
self.web_page.wait_for_timeout(3000)
|
|
59
|
+
cookies = self.web_page.context.cookies()
|
|
60
|
+
cookies_list = [cookie for cookie in cookies if '.kuajingmaihuo.com' in cookie['domain']]
|
|
61
|
+
self.cookie = '; '.join([f"{cookie['name']}={cookie['value']}" for cookie in cookies_list])
|
|
62
|
+
log(f'已获取self.cookie:', self.cookie)
|
|
63
|
+
write_dict_to_file(cache_cookie, {'cookie': self.cookie})
|
|
64
|
+
return self.cookie
|
|
65
|
+
|
|
66
|
+
def post_json(self, str_url, payload, mall_id=None):
|
|
67
|
+
global response
|
|
68
|
+
try:
|
|
69
|
+
headers = {
|
|
70
|
+
'content-type': 'application/json',
|
|
71
|
+
'priority' : 'u=1, i',
|
|
72
|
+
'referer' : 'https://seller.kuajingmaihuo.com/settle/site-main',
|
|
73
|
+
'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',
|
|
74
|
+
'Cookie' : self.cookie,
|
|
75
|
+
}
|
|
76
|
+
if mall_id:
|
|
77
|
+
headers.update({'mallid': f"{mall_id}"})
|
|
78
|
+
response = requests.post(str_url, headers=headers, data=json.dumps(payload))
|
|
79
|
+
response.raise_for_status() # 如果响应不正常,会抛出异常
|
|
80
|
+
|
|
81
|
+
response_text = response.json()
|
|
82
|
+
error_code = response_text.get('error_code') or response_text.get('errorCode')
|
|
83
|
+
if error_code != 1000000:
|
|
84
|
+
raise send_exception(response_text)
|
|
85
|
+
return response.json() # 直接返回 JSON 格式的数据
|
|
86
|
+
except:
|
|
87
|
+
raise send_exception()
|
|
88
|
+
|
|
89
|
+
def get_shop_list(self):
|
|
90
|
+
log(f'获取店铺列表')
|
|
91
|
+
global DictMall
|
|
92
|
+
url = "https://seller.kuajingmaihuo.com/bg/quiet/api/mms/userInfo"
|
|
93
|
+
response_text = self.post_json(url, {})
|
|
94
|
+
|
|
95
|
+
company_list = response_text['result']['companyList']
|
|
96
|
+
mall_list = []
|
|
97
|
+
for company in company_list:
|
|
98
|
+
mallList = company['malInfoList']
|
|
99
|
+
# shop_list = [['店铺ID', '店铺名称', '主账号', '店铺类型']]
|
|
100
|
+
for mall in mallList:
|
|
101
|
+
mall_id = str(mall['mallId'])
|
|
102
|
+
mall_name = str(mall['mallName'])
|
|
103
|
+
shop_info = [mall_id, mall_name, self.mobile, '半托管' if mall['isSemiManagedMall'] else '全托管']
|
|
104
|
+
write_dict_to_file_ex(self.config.temu_store_info, {mall_id: shop_info}, [mall_id])
|
|
105
|
+
|
|
106
|
+
self.dict_mall[str(mall['mallId'])] = mall['mallName']
|
|
107
|
+
|
|
108
|
+
if not mall['isSemiManagedMall']:
|
|
109
|
+
mall_list.append([str(mall['mallId']), mall['mallName'], '', self.mobile])
|
|
110
|
+
|
|
111
|
+
return mall_list
|
|
112
|
+
|
|
113
|
+
def get_funds_info(self, mall_id):
|
|
114
|
+
log(f'获取 {self.dict_mall[mall_id]} 资金信息')
|
|
115
|
+
url = "https://seller.kuajingmaihuo.com/api/merchant/payment/account/amount/info"
|
|
116
|
+
response_text = self.post_json(url, {}, mall_id)
|
|
117
|
+
total_amount = response_text.get('result').get('totalAmount')
|
|
118
|
+
available_amount = response_text.get('result').get('availableBalance')
|
|
119
|
+
|
|
120
|
+
NotifyItem = [self.dict_mall[mall_id], total_amount, available_amount, '', TimeUtils.current_datetime()]
|
|
121
|
+
|
|
122
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/funds_{TimeUtils.today_date()}.json'
|
|
123
|
+
write_dict_to_file_ex(cache_file, {mall_id: NotifyItem}, [mall_id])
|
|
124
|
+
|
|
125
|
+
return NotifyItem
|
|
126
|
+
|
|
127
|
+
def list_warehouse(self, mall_id, mall_name):
|
|
128
|
+
log(f'获取店铺 {mall_name} 销售商品列表 第1页')
|
|
129
|
+
url = "https://seller.kuajingmaihuo.com/marvel-mms/cn/api/kiana/venom/sales/management/listWarehouse"
|
|
130
|
+
payload = {
|
|
131
|
+
"pageNo" : 1,
|
|
132
|
+
"pageSize" : 40,
|
|
133
|
+
"isLack" : 0,
|
|
134
|
+
"selectStatusList" : [12], # 12 是已加入站点
|
|
135
|
+
"priceAdjustRecentDays": 30 # 近30日价格调整
|
|
136
|
+
}
|
|
137
|
+
response_text = self.post_json(url, payload, mall_id)
|
|
138
|
+
|
|
139
|
+
total = response_text['result']['total']
|
|
140
|
+
subOrderListCount = len(response_text['result']['subOrderList'])
|
|
141
|
+
totalPage = math.ceil(total / subOrderListCount) if subOrderListCount else 0
|
|
142
|
+
subOrderList = response_text['result']['subOrderList']
|
|
143
|
+
|
|
144
|
+
for page in range(2, totalPage + 1):
|
|
145
|
+
log(f'获取店铺{mall_name}销售商品列表 第{page}/{totalPage}页')
|
|
146
|
+
payload['pageNo'] = page
|
|
147
|
+
response_text = self.post_json(url, payload, mall_id)
|
|
148
|
+
subOrderList += response_text['result']['subOrderList']
|
|
149
|
+
time.sleep(0.3)
|
|
150
|
+
|
|
151
|
+
cache_file = f'{self.config.auto_dir}/temu/cache/warehouse_list_{TimeUtils.today_date()}.json'
|
|
152
|
+
write_dict_to_file_ex(cache_file, {mall_id: subOrderList}, [mall_id])
|
|
153
|
+
|
|
154
|
+
return subOrderList
|
qrpa/wxwork.py
CHANGED
|
@@ -41,6 +41,9 @@ class WxWorkBot:
|
|
|
41
41
|
response = requests.post(
|
|
42
42
|
f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={self.key}&type=file',
|
|
43
43
|
headers=headers, files=files)
|
|
44
|
+
response_text = json.loads(response.text)
|
|
45
|
+
if str(response_text.get('errcode')) != '0':
|
|
46
|
+
raise Exception(response_text)
|
|
44
47
|
if response.status_code == 200:
|
|
45
48
|
result = json.loads(response.text)
|
|
46
49
|
return result['media_id']
|
|
@@ -190,6 +193,9 @@ class WxWorkBot:
|
|
|
190
193
|
"Content-Type": "application/json"
|
|
191
194
|
}
|
|
192
195
|
response = requests.post(f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.key}", headers=header, data=json.dumps(data))
|
|
196
|
+
response_text = json.loads(response.text)
|
|
197
|
+
if str(response_text.get('errcode')) != '0':
|
|
198
|
+
raise Exception(response_text)
|
|
193
199
|
if response.status_code == 200:
|
|
194
200
|
result = json.loads(response.text)
|
|
195
201
|
return result
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
qrpa/RateLimitedSender.py,sha256=hqvb1qspDFaW4RsLuVufylOrefkMgixANKeBaGEqYb4,1421
|
|
2
|
+
qrpa/__init__.py,sha256=-7KlqEQj8qwtMs6xAHh3JB2B-5xDBz273BeZiBQ5Fe4,953
|
|
3
|
+
qrpa/db_migrator.py,sha256=2VmhzcMsU0MKpl-mNCwKyV8tLTqyEysSpP27-S_rQZ8,21862
|
|
4
|
+
qrpa/fun_base.py,sha256=j3sbYInaltsRItUEqt1dhqsijdjvkePV4X4aN34madg,4410
|
|
5
|
+
qrpa/fun_excel.py,sha256=o6Tb_lp3uFc0MpS6NG6N6CTrgUwRABTTHyVY2eMTYDA,105676
|
|
6
|
+
qrpa/fun_file.py,sha256=yzjDV16WL5vRys7J4uQcNzIFkX4D5MAlSCwxcD-mwQo,11966
|
|
7
|
+
qrpa/fun_web.py,sha256=5QLQorAhRzMOGMRh4eCJ2UH8ZhVHvxkHwobWhmgU5qM,6286
|
|
8
|
+
qrpa/fun_win.py,sha256=-LnTeocdTt72NVH6VgLdpAT9_C5oV9okeudXG6CftMA,8034
|
|
9
|
+
qrpa/shein_daily_report_model.py,sha256=H8oZmIN5Pyqe306W1_xuz87lOqLQ_LI5RjXbaxDkIzE,12589
|
|
10
|
+
qrpa/shein_excel.py,sha256=7gFAh7etQB9ChoXZkqPD3fEQA0Y3sm_Ja6UtD_DkfSo,38472
|
|
11
|
+
qrpa/shein_lib.py,sha256=ub0jv9P4Nt8S_DQ77uIQBCULGDWYrkzjwsmPhlUkKq8,85266
|
|
12
|
+
qrpa/shein_sqlite.py,sha256=ZQwD0Gz81q9WY7tY2HMEYvSF9r3N_G_Aur3bYfST9WY,5707
|
|
13
|
+
qrpa/shein_ziniao.py,sha256=nSqqcEPh4nVQtUxUnIRzeZfTLyXywGPjPZn5pP-w57U,18309
|
|
14
|
+
qrpa/temu_chrome.py,sha256=CbtFy1QPan9xJdJcNZj-EsVGhUvv3ZTEPVDEA4-im40,2803
|
|
15
|
+
qrpa/temu_excel.py,sha256=pmXKuCZ1H6A-F7dj0Kg5JjENYPEJO2dr572Q-GkepuI,5447
|
|
16
|
+
qrpa/temu_lib.py,sha256=hYB59zsLS3m3NTic_duTwPMOTSxlHyQVa8OhHnHm-1g,7199
|
|
17
|
+
qrpa/time_utils.py,sha256=ef0hhbN_6b-gcnz5ETIVOoxemIMvcxGVGGIRnHnGaBo,29564
|
|
18
|
+
qrpa/time_utils_example.py,sha256=shHOXKKF3QSzb0SHsNc34M61wEkkLuM30U9X1THKNS8,8053
|
|
19
|
+
qrpa/wxwork.py,sha256=Vy8PGEtlTWt4-1laVhuqpJUGCFH2JymgbjvH00aaBog,10946
|
|
20
|
+
qrpa-1.0.24.dist-info/METADATA,sha256=IUf28BbfVg7_crICn1PvAHv6u_xeOQljmoDtKk_sccg,231
|
|
21
|
+
qrpa-1.0.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
qrpa-1.0.24.dist-info/top_level.txt,sha256=F6T5igi0fhXDucPPUbmmSC0qFCDEsH5eVijfVF48OFU,5
|
|
23
|
+
qrpa-1.0.24.dist-info/RECORD,,
|
qrpa-1.0.23.dist-info/RECORD
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
qrpa/RateLimitedSender.py,sha256=hqvb1qspDFaW4RsLuVufylOrefkMgixANKeBaGEqYb4,1421
|
|
2
|
-
qrpa/__init__.py,sha256=EysO3QHdHWPWuVmtbekAZBSKSlCIAHnLzg2O6e26nNs,801
|
|
3
|
-
qrpa/db_migrator.py,sha256=2VmhzcMsU0MKpl-mNCwKyV8tLTqyEysSpP27-S_rQZ8,21862
|
|
4
|
-
qrpa/fun_base.py,sha256=W_owEa8-yuGG18n9kX3remm9YTzym69ztQjtYCNMTw4,3308
|
|
5
|
-
qrpa/fun_excel.py,sha256=I6EX0MGFZS82W6KRtzoYbmPX6TpsMywVtUg9i5sFuhg,104430
|
|
6
|
-
qrpa/fun_file.py,sha256=yzjDV16WL5vRys7J4uQcNzIFkX4D5MAlSCwxcD-mwQo,11966
|
|
7
|
-
qrpa/fun_web.py,sha256=5QLQorAhRzMOGMRh4eCJ2UH8ZhVHvxkHwobWhmgU5qM,6286
|
|
8
|
-
qrpa/fun_win.py,sha256=-LnTeocdTt72NVH6VgLdpAT9_C5oV9okeudXG6CftMA,8034
|
|
9
|
-
qrpa/shein_daily_report_model.py,sha256=H8oZmIN5Pyqe306W1_xuz87lOqLQ_LI5RjXbaxDkIzE,12589
|
|
10
|
-
qrpa/shein_excel.py,sha256=TDmcxVe2AIJ6Bjl59viCcRbcufgwu_XPooPe2S7_U9k,29333
|
|
11
|
-
qrpa/shein_lib.py,sha256=IurTKa-K5Ay6DCOSv-cxiz1N6uvFKrxpxvREkua78FE,78696
|
|
12
|
-
qrpa/shein_sqlite.py,sha256=ZQwD0Gz81q9WY7tY2HMEYvSF9r3N_G_Aur3bYfST9WY,5707
|
|
13
|
-
qrpa/shein_ziniao.py,sha256=nSqqcEPh4nVQtUxUnIRzeZfTLyXywGPjPZn5pP-w57U,18309
|
|
14
|
-
qrpa/time_utils.py,sha256=ef0hhbN_6b-gcnz5ETIVOoxemIMvcxGVGGIRnHnGaBo,29564
|
|
15
|
-
qrpa/time_utils_example.py,sha256=shHOXKKF3QSzb0SHsNc34M61wEkkLuM30U9X1THKNS8,8053
|
|
16
|
-
qrpa/wxwork.py,sha256=zu6eQZHJEpwe4Q6vvwmBJaU31qm0bixII_XC9uJIrJc,10618
|
|
17
|
-
qrpa-1.0.23.dist-info/METADATA,sha256=UnMd4JfT9YVhH3fBLxjTLH-nViAOR7YWrQHu9e-Jdf0,231
|
|
18
|
-
qrpa-1.0.23.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
-
qrpa-1.0.23.dist-info/top_level.txt,sha256=F6T5igi0fhXDucPPUbmmSC0qFCDEsH5eVijfVF48OFU,5
|
|
20
|
-
qrpa-1.0.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|