qrpa 1.0.34__py3-none-any.whl → 1.1.50__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.
qrpa/shein_lib.py CHANGED
@@ -1,6 +1,6 @@
1
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 .fun_web import fetch, full_screen_shot
2
+ from .fun_base import log, send_exception, md5_string, get_safe_value, NetWorkIdleTimeout
3
+ from .fun_web import fetch, fetch_get, full_screen_shot, safe_goto
4
4
  from .time_utils import TimeUtils
5
5
  from .wxwork import WxWorkBot
6
6
 
@@ -8,7 +8,7 @@ from .shein_sqlite import insert_sales, get_last_week_sales, get_near_week_sales
8
8
 
9
9
  import math
10
10
  import time
11
- import json
11
+ import json, traceback
12
12
  from datetime import datetime
13
13
  from playwright.sync_api import Page
14
14
 
@@ -23,14 +23,54 @@ class SheinLib:
23
23
  self.store_name = store_name
24
24
  self.web_page = web_page
25
25
  self.dt = None
26
+ self.dt_goods = None
26
27
  self.DictQueryTime = {}
27
28
 
28
29
  self.deal_auth()
30
+ self.get_user()
29
31
 
30
32
  # 处理鉴权
31
33
  def deal_auth(self):
32
34
  web_page = self.web_page
33
35
 
36
+ # 等待页面稳定并处理导航
37
+ for attempt in range(3):
38
+ try:
39
+ current_url = web_page.url
40
+ log(f"尝试获取页面信息 - URL: {current_url}", self.store_username, self.store_name)
41
+
42
+ # 检查是否在认证页面,如果是则直接跳转到目标页面
43
+ if '/auth/SSLS' in current_url:
44
+ log("检测到SSLS认证页面,直接跳转到首页", self.store_username, self.store_name)
45
+ web_page.goto('https://sso.geiwohuo.com/#/home', wait_until='domcontentloaded', timeout=15000)
46
+ web_page.wait_for_timeout(3000)
47
+ current_url = web_page.url
48
+ log(f"跳转后URL: {current_url}", self.store_username, self.store_name)
49
+
50
+ # 等待导航完成
51
+ web_page.wait_for_load_state("domcontentloaded", timeout=6000)
52
+
53
+ final_url = web_page.url
54
+ final_title = web_page.title()
55
+ log(f"页面稳定 - URL: {final_url}, 标题: {final_title}", self.store_username, self.store_name)
56
+ break
57
+
58
+ except Exception as e:
59
+ log(f"第{attempt + 1}次等待页面稳定失败: {e}", self.store_username, self.store_name)
60
+ if "crashed" in str(e) or "Target" in str(e):
61
+ log("页面稳定检查时崩溃,直接继续", self.store_username, self.store_name)
62
+ break
63
+ elif "destroyed" in str(e) or "navigation" in str(e):
64
+ log("检测到导航中断,等待导航完成", self.store_username, self.store_name)
65
+ web_page.wait_for_timeout(4000)
66
+ continue
67
+ elif attempt == 2:
68
+ log("页面稳定等待最终失败,继续执行", self.store_username, self.store_name)
69
+ break
70
+ web_page.wait_for_timeout(2000)
71
+
72
+ web_page.wait_for_timeout(2000)
73
+
34
74
  # 定义最大重试次数
35
75
  MAX_RETRIES = 5
36
76
  retries = 0
@@ -41,16 +81,35 @@ class SheinLib:
41
81
 
42
82
  while retries < MAX_RETRIES:
43
83
  try:
44
-
45
84
  retries += 1
46
85
 
47
86
  while not web_page.locator('//div[contains(text(),"商家后台")]').nth(1).is_visible():
87
+ try:
88
+ current_url = web_page.url
89
+ current_title = web_page.title()
90
+ log(f"循环检查 - URL: {current_url}, 标题: {current_title}", self.store_username, self.store_name)
91
+
92
+ # 如果在认证页面且出现问题,直接跳转
93
+ if '/auth/SSLS' in current_url:
94
+ log("在主循环中检测到SSLS认证页面,跳转到首页", self.store_username, self.store_name)
95
+ web_page.goto('https://sso.geiwohuo.com/#/home', wait_until='domcontentloaded', timeout=15000)
96
+ web_page.wait_for_timeout(3000)
97
+ continue
98
+
99
+ except Exception as status_error:
100
+ log(f"获取页面状态失败: {status_error}", self.store_username, self.store_name)
101
+ if "crashed" in str(status_error):
102
+ break
103
+
104
+ if web_page.locator('xpath=//div[text()="扫码登录"]').is_visible():
105
+ log('检查到扫码登录,切换至账号登录', self.store_username, self.store_name)
106
+ web_page.locator('xpath=//*[@id="container"]/div[2]/div[4]/img').click()
48
107
 
49
108
  if web_page.locator('xpath=//div[@id="container" and @alita-name="gmpsso"]//button[@type="button" and @id]').nth(0).is_visible():
50
- log("鉴权确定按钮可见 点击'确定'按钮", self.store_username, self.store_name)
51
- web_page.locator('xpath=//div[@id="container" and @alita-name="gmpsso"]//button[@type="button" and @id]').nth(0).click()
52
- web_page.wait_for_load_state("load")
53
- web_page.wait_for_timeout(1000)
109
+ if 'https://sso.geiwohuo.com/#/home' not in web_page.url:
110
+ log("鉴权确定按钮可见 点击'确定'按钮", web_page.title(), web_page.url, self.store_username, self.store_name)
111
+ web_page.locator('xpath=//div[@id="container" and @alita-name="gmpsso"]//button[@type="button" and @id]').nth(0).click()
112
+ web_page.wait_for_timeout(5000)
54
113
 
55
114
  while web_page.locator('//div[text()="验证码"]').is_visible():
56
115
  log(f'等待输入验证码: {wait_count}', self.store_username, self.store_name)
@@ -62,12 +121,27 @@ class SheinLib:
62
121
  time.sleep(5)
63
122
  wait_count += 1
64
123
 
65
- if web_page.locator('//input[@name="username"]').is_visible():
124
+ if web_page.locator('//div[contains(text(),"同意签署协议")]').count() > 0:
125
+ while web_page.locator('//div[contains(text(),"同意签署协议")]').count() == 0:
126
+ log('等待协议内容出现')
127
+ web_page.wait_for_timeout(1000)
128
+
129
+ if web_page.locator('//div[contains(text(),"同意签署协议")]').count() > 0:
130
+ log('检测到同意签署协议')
131
+ web_page.wait_for_timeout(1000)
132
+ log('点击同意复选框')
133
+ web_page.locator('//i[@class="so-checkinput-indicator so-checkinput-checkbox"]').click()
134
+ web_page.wait_for_timeout(1000)
135
+ log('点击同意按钮')
136
+ web_page.locator('//button[span[text()="同意"]]').click()
137
+
138
+ # //button[span[text()="登录"]]
139
+ if web_page.locator('//button[span[text()="登录"]]').is_visible() or web_page.locator('//input[@name="username"]').is_visible():
66
140
  log("用户名输入框可见 等待5秒点击'登录'按钮", self.store_username, self.store_name)
67
141
  web_page.wait_for_timeout(5000)
68
142
  log('点击"登录"', self.store_username, self.store_name)
69
143
  web_page.locator('//button[contains(@class,"login_btn")]').click()
70
- web_page.wait_for_load_state("load")
144
+
71
145
  log('再延时5秒', self.store_username, self.store_name)
72
146
  web_page.wait_for_timeout(5000)
73
147
 
@@ -76,41 +150,90 @@ class SheinLib:
76
150
  return
77
151
 
78
152
  log('商家后台不可见', web_page.title(), web_page.url, self.store_username, self.store_name)
79
- web_page.wait_for_load_state("load")
80
- web_page.wait_for_timeout(1000)
81
-
82
- if 'SHEIN全球商家中心' in web_page.title() and 'https://sso.geiwohuo.com/#/home' in web_page.url:
83
- log('SHEIN全球商家中心 中断循环', self.store_username, self.store_name)
84
- break
85
-
86
- if '后台首页' in web_page.title() and 'https://sso.geiwohuo.com/#/home' in web_page.url:
87
- log('后台首页 中断循环', self.store_username, self.store_name)
88
- break
153
+ if 'https://sso.geiwohuo.com/#/home' in web_page.url:
154
+ web_page.wait_for_timeout(5000)
155
+ web_page.reload()
89
156
 
90
- if '商家后台' in web_page.title() and 'https://sso.geiwohuo.com/#/home' in web_page.url:
91
- log('后台首页 中断循环', self.store_username, self.store_name)
92
- break
157
+ # while r'=/CN' in web_page.url:
158
+ # safe_goto(web_page, 'https://sso.geiwohuo.com/#/home?q=0')
159
+ #
160
+ # web_page.wait_for_timeout(5000)
161
+ # if web_page.locator('//input[@name="username"]').is_visible():
162
+ # log("用户名输入框可见 等待5秒点击'登录'按钮", self.store_username, self.store_name)
163
+ # web_page.wait_for_timeout(5000)
164
+ # log('点击"登录"', self.store_username, self.store_name)
165
+ # web_page.locator('//button[contains(@class,"login_btn")]').click()
166
+ #
167
+ # log('再延时5秒', self.store_username, self.store_name)
168
+ # web_page.wait_for_timeout(5000)
169
+
170
+ web_page.wait_for_timeout(3000)
171
+
172
+ if 'https://sso.geiwohuo.com/#/home' in web_page.url:
173
+ if 'SHEIN全球商家中心' in web_page.title() or '后台首页' in web_page.title() or '商家后台' in web_page.title():
174
+ log(web_page.title(), '中断循环', self.store_username, self.store_name)
175
+ web_page.wait_for_timeout(5000)
176
+ break
93
177
 
94
178
  if 'mrs.biz.sheincorp.cn' in web_page.url and '商家后台' in web_page.title():
95
- web_page.goto('https://sso.geiwohuo.com/#/home')
96
- web_page.wait_for_load_state("load")
97
- web_page.wait_for_timeout(3000)
179
+ try:
180
+ web_page.goto('https://sso.geiwohuo.com/#/home?q=1', wait_until='domcontentloaded', timeout=10000)
181
+ web_page.wait_for_timeout(3000)
182
+ except Exception as nav_error:
183
+ log(f"导航失败,尝试重新加载: {nav_error}", self.store_username, self.store_name)
184
+ web_page.reload(wait_until='domcontentloaded', timeout=10000)
185
+ web_page.wait_for_timeout(5000)
98
186
 
99
187
  if web_page.locator('//h1[contains(text(),"鉴权")]').is_visible():
100
188
  log('检测到鉴权 刷新页面', self.store_username, self.store_name)
101
189
  web_page.reload()
102
- web_page.wait_for_load_state('load')
103
- web_page.wait_for_timeout(3000)
190
+ web_page.wait_for_timeout(5000)
104
191
  web_page.reload()
105
- web_page.wait_for_timeout(3000)
192
+ web_page.wait_for_timeout(5000)
106
193
 
107
194
  if web_page.title() == 'SHEIN':
108
- web_page.goto('https://sso.geiwohuo.com/#/home')
109
- web_page.wait_for_load_state("load")
110
- web_page.wait_for_timeout(3000)
111
-
195
+ try:
196
+ web_page.goto('https://sso.geiwohuo.com/#/home?q=2', wait_until='domcontentloaded', timeout=10000)
197
+ web_page.wait_for_timeout(3000)
198
+ except Exception as nav_error:
199
+ log(f"导航失败,尝试重新加载: {nav_error}", self.store_username, self.store_name)
200
+ web_page.reload(wait_until='domcontentloaded', timeout=10000)
201
+ web_page.wait_for_timeout(5000)
202
+
203
+ break
112
204
  except Exception as e:
113
205
  log(f"错误发生: {e}, 重试中...({self.store_username}, {self.store_name})")
206
+ log(traceback.format_exc())
207
+
208
+ # 收集崩溃时的详细信息
209
+ try:
210
+ crash_url = web_page.url
211
+ crash_title = web_page.title()
212
+ log(f"崩溃时页面信息 - URL: {crash_url}, 标题: {crash_title}", self.store_username, self.store_name)
213
+
214
+ # 尝试截图保存崩溃现场
215
+ try:
216
+ screenshot_path = f"crash_screenshot_{self.store_username}_{int(time.time())}.png"
217
+ web_page.screenshot(path=screenshot_path)
218
+ log(f"已保存崩溃截图: {screenshot_path}", self.store_username, self.store_name)
219
+ except:
220
+ log("无法截取崩溃时的页面截图", self.store_username, self.store_name)
221
+
222
+ except:
223
+ log("无法获取崩溃时的页面信息", self.store_username, self.store_name)
224
+
225
+ # 检查特定类型的错误
226
+ if any(keyword in str(e).lower() for keyword in ['memory', 'out of memory', 'oom']):
227
+ log("检测到内存相关崩溃", self.store_username, self.store_name)
228
+
229
+ if "destroyed" in str(e) or "navigation" in str(e):
230
+ log("检测到导航中断,等待页面稳定后重试", self.store_username, self.store_name)
231
+ web_page.wait_for_timeout(5000)
232
+ continue
233
+
234
+ if 'crashed' in str(e) or 'Target' in str(e):
235
+ log("检测到页面或目标崩溃,直接退出当前循环", self.store_username, self.store_name)
236
+ raise e
114
237
  retries += 1
115
238
  if retries >= MAX_RETRIES:
116
239
  log(f"达到最大重试次数,停止尝试({self.store_username}, {self.store_name})")
@@ -118,6 +241,120 @@ class SheinLib:
118
241
  time.sleep(2) # 错误时等待2秒后重试
119
242
 
120
243
  log('鉴权处理结束')
244
+ # web_page.wait_for_load_state("load")
245
+ # web_page.wait_for_load_state("networkidle")
246
+ web_page.wait_for_timeout(3000)
247
+
248
+ # 获取用户信息
249
+ def get_user(self, uuid=None):
250
+ log(f'获取用户信息:{self.store_username} {self.store_name}')
251
+
252
+ # 生成 uuid 参数,如果没有提供则使用时间戳
253
+ if uuid is None:
254
+ import time
255
+ uuid = str(int(time.time() * 1000))
256
+
257
+ url = f"https://sso.geiwohuo.com/sso-prefix/auth/getUser?uuid={uuid}"
258
+
259
+ # 设置请求头,根据 Chrome 请求
260
+ headers = {
261
+ "gmpsso-language": "CN",
262
+ "origin-url" : "https://sso.geiwohuo.com/#/home/",
263
+ "x-sso-scene" : "gmpsso"
264
+ }
265
+
266
+ # 特定于此请求的配置
267
+ fetch_config = {
268
+ "credentials" : "include",
269
+ "referrer" : "https://sso.geiwohuo.com/",
270
+ "referrerPolicy": "strict-origin-when-cross-origin"
271
+ }
272
+
273
+ response_text = fetch_get(self.web_page, url, headers, fetch_config)
274
+ error_code = response_text.get('code')
275
+ if str(error_code) != '0':
276
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
277
+ info = response_text.get('info', {})
278
+ cache_file = f'{self.config.auto_dir}/shein_user.json'
279
+ info['store_username'] = self.store_username
280
+ info['store_name'] = self.store_name
281
+ write_dict_to_file_ex(cache_file, {self.store_username: info}, [self.store_username])
282
+ log(info)
283
+ self.user_info = info
284
+ return info
285
+
286
+ # 获取供货商信息
287
+ def get_supplier_data(self):
288
+ self.web_page.goto('https://sso.geiwohuo.com/#/mws/seller/new-account-overview')
289
+ self.web_page.wait_for_load_state('load')
290
+ cache_file = f'{self.config.auto_dir}/shein/dict/supplier_data.json'
291
+ info = read_dict_from_file_ex(cache_file, self.store_username, 3600 * 24 * 10)
292
+ if len(info) > 0:
293
+ return info
294
+
295
+ log(f'正在获取 {self.store_name} 供货商信息')
296
+ url = "https://sso.geiwohuo.com/mgs-api-prefix/supplierGrowth/querySupplierCommonData"
297
+ payload = {}
298
+ response_text = fetch(self.web_page, url, payload)
299
+ error_code = response_text.get('code')
300
+ if str(error_code) != '0':
301
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
302
+ info = response_text.get('info')
303
+
304
+ write_dict_to_file_ex(cache_file, {self.store_username: info}, [self.store_username])
305
+
306
+ return info
307
+
308
+ def get_withdraw_list(self, supplier_id, year=0):
309
+ self.web_page.goto('https://sso.geiwohuo.com/#/mws/seller/new-account-overview')
310
+ self.web_page.wait_for_load_state("load")
311
+
312
+ if year == 0:
313
+ first_day, last_day = TimeUtils.get_last_month_range_time()
314
+ else:
315
+ first_day, last_day = TimeUtils.get_year_range_time(year)
316
+
317
+ page_num = 1
318
+ page_size = 200
319
+
320
+ url = f"https://sso.geiwohuo.com/mws/mwms/sso/withdraw/transferRecordList"
321
+ payload = {
322
+ "reqSystemCode" : "mws-front",
323
+ "supplierId" : supplier_id,
324
+ "pageNum" : page_num,
325
+ "pageSize" : page_size,
326
+ "createTimeStart": first_day,
327
+ "createTimeEnd" : last_day,
328
+ # "withdrawStatusList": [30]
329
+ }
330
+ log(payload)
331
+ response_text = fetch(self.web_page, url, payload)
332
+ log(response_text)
333
+ error_code = response_text.get('code')
334
+ if str(error_code) != '0':
335
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
336
+
337
+ withdraw_list = response_text['info']['list']
338
+ total = response_text['info']['count']
339
+ totalPage = math.ceil(total / page_size)
340
+
341
+ cache_file = f'{self.config.auto_dir}/shein/cache/withdraw_list_{first_day}_{last_day}.json'
342
+ withdraw_list_cache = read_dict_from_file_ex(cache_file, self.store_username, 3600 * 12)
343
+ if len(withdraw_list_cache) == int(total):
344
+ log('返回缓存数据: ', len(withdraw_list_cache), total)
345
+ return withdraw_list_cache
346
+
347
+ for page in range(2, totalPage + 1):
348
+ log(f'获取提现列表 第{page}/{totalPage}页')
349
+ page_num = page
350
+ payload['pageNum'] = page_num
351
+ response_text = fetch(self.web_page, url, payload)
352
+ withdraw_list += response_text['info']['list']
353
+ time.sleep(0.1)
354
+
355
+ write_dict_to_file_ex(cache_file, {self.store_username: withdraw_list}, [self.store_username])
356
+
357
+ return withdraw_list
121
358
 
122
359
  # 获取质检报告pdf地址
123
360
  def get_qc_report_url(self, deliverCode, purchaseCode):
@@ -135,6 +372,21 @@ class SheinLib:
135
372
  log(qc_report_url)
136
373
  return qc_report_url
137
374
 
375
+ # 获取稽查报表
376
+ def get_inspect_report_url(self, returnOrderId):
377
+ log(f'获取稽查报告:{returnOrderId}')
378
+ url = f"https://sso.geiwohuo.com/pfmp/returnOrder/queryInspectReport"
379
+ payload = {
380
+ "returnOrderId": returnOrderId,
381
+ }
382
+ response_text = fetch(self.web_page, url, payload)
383
+ error_code = response_text.get('code')
384
+ if str(error_code) != '0':
385
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
386
+ log(response_text)
387
+ report_url = response_text.get('info', {}).get('reportUrl')
388
+ return report_url
389
+
138
390
  def get_return_order_box_detail(self, returnOrderId):
139
391
  log(f'获取退货包裹详情: {returnOrderId}')
140
392
  url = f"https://sso.geiwohuo.com/pfmp/returnOrder/getReturnOrderBoxDetail"
@@ -149,11 +401,31 @@ class SheinLib:
149
401
  raise send_exception(json.dumps(response_text, ensure_ascii=False))
150
402
  list_item = response_text['info']['data']
151
403
 
404
+ for item in list_item:
405
+ # 遍历每个快递单的包裹列表
406
+ for box in item.get('boxList', []):
407
+ # 遍历每个包裹中的商品列表
408
+ for good in box.get('goods', []):
409
+ # 遍历每个商品的详情列表(包含platformSku的层级)
410
+ for detail in good.get('details', []):
411
+ # 在这里添加新字段
412
+ # 示例:添加一个"status"字段,值为"processed"
413
+ supplier_sku = detail.get('supplierSku')
414
+ erp_supplier_name = self.bridge.get_sku_supplier(supplier_sku, self.config.erp_source)
415
+ log(self.config.erp_source, supplier_sku, erp_supplier_name)
416
+ if erp_supplier_name != '-':
417
+ detail['erp_supplier_name'] = erp_supplier_name
418
+ erp_cost_price = self.bridge.get_sku_cost(supplier_sku, self.config.erp_source)
419
+ log(self.config.erp_source, supplier_sku, erp_cost_price)
420
+ if erp_cost_price != '-':
421
+ detail['erp_cost_price'] = erp_cost_price
422
+
423
+ log(list_item)
152
424
  cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_box_detail_{returnOrderId}.json'
153
425
  write_dict_to_file(cache_file, list_item)
426
+ return list_item
154
427
 
155
428
  def get_return_order_list(self, start_date, end_date, only_yesterday=1):
156
-
157
429
  log(f'获取退货列表: {self.store_username} {self.store_name} {start_date} {end_date}')
158
430
 
159
431
  page_num = 1
@@ -161,10 +433,11 @@ class SheinLib:
161
433
 
162
434
  url = f"https://sso.geiwohuo.com/pfmp/returnOrder/page"
163
435
  payload = {
164
- "addTimeStart": f"{start_date} 00:00:00",
165
- "addTimeEnd" : f"{end_date} 23:59:59",
166
- "page" : page_num,
167
- "perPage" : page_size
436
+ "returnOrderType": 1, # 只查询退货
437
+ "addTimeStart" : f"{start_date} 00:00:00",
438
+ "addTimeEnd" : f"{end_date} 23:59:59",
439
+ "page" : page_num,
440
+ "perPage" : page_size
168
441
  }
169
442
  response_text = fetch(self.web_page, url, payload)
170
443
  error_code = response_text.get('code')
@@ -186,19 +459,33 @@ class SheinLib:
186
459
  today_list_item = []
187
460
  # 过滤 退货出库时间 是昨天的
188
461
  for item in list_item:
462
+ returnOrderId = item['id']
463
+ item['store_username'] = self.store_username
464
+ item['store_name'] = self.store_name
465
+ item['store_manager'] = self.config.shein_store_manager.get(str(self.store_username).lower())
466
+
467
+ item['qc_report_url'] = ''
468
+ if int(item['returnScrapType']) == 1:
469
+ purchaseCode = item['sellerOrderNo']
470
+ delivery_code = item['sellerDeliveryNo']
471
+ item['qc_report_url'] = self.get_qc_report_url(delivery_code, purchaseCode)
472
+
473
+ item['report_url'] = ''
474
+ if int(item['returnScrapType']) == 2:
475
+ item['report_url'] = self.get_inspect_report_url(returnOrderId)
476
+
477
+ item['return_box_detail'] = []
478
+
189
479
  has_valid_package = item.get('hasPackage') == 1
190
- is_valid_yesterday = TimeUtils.is_yesterday(item['completeTime'], None) if item.get('completeTime') else False
191
480
  if has_valid_package:
192
- if int(item['returnScrapType']) == 1:
193
- purchaseCode = item['sellerOrderNo']
194
- delivery_code = item['sellerDeliveryNo']
195
- item['qc_report_url'] = self.get_qc_report_url(delivery_code, purchaseCode)
196
- returnOrderId = item['id']
197
- self.get_return_order_box_detail(returnOrderId)
481
+ return_box_detail = self.get_return_order_box_detail(returnOrderId)
482
+ if len(return_box_detail) > 0:
483
+ item['return_box_detail'] = return_box_detail
198
484
 
199
- all_list_item.append(item)
200
- if is_valid_yesterday:
201
- today_list_item.append(item)
485
+ all_list_item.append(item)
486
+ is_valid_yesterday = TimeUtils.is_yesterday(item['completeTime'], None) if item.get('completeTime') else False
487
+ if is_valid_yesterday:
488
+ today_list_item.append(item)
202
489
 
203
490
  cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_{TimeUtils.today_date()}.json'
204
491
  write_dict_to_file_ex(cache_file, {self.store_username: today_list_item}, [self.store_username])
@@ -303,6 +590,53 @@ class SheinLib:
303
590
 
304
591
  return list_item
305
592
 
593
+ def get_ledger_record(self, first_day, last_day):
594
+ page_num = 1
595
+ page_size = 200 # 列表最多返回200条数据 大了没有用
596
+
597
+ cache_file = f'{self.config.auto_dir}/shein/ledger/ledger_record_{self.store_username}_{first_day}_{last_day}.json'
598
+ list_item_cache = read_dict_from_file(cache_file)
599
+
600
+ url = f"https://sso.geiwohuo.com/mils/changeDetail/page"
601
+ payload = {
602
+ "displayChangeTypeList": ["6", "7", "9", "10", "11", "12", "13", "16", "18", "19", "21"], # 出库
603
+ "addTimeStart" : f"{first_day} 00:00:00",
604
+ "addTimeEnd" : f"{last_day} 23:59:59",
605
+ "pageNumber" : page_num,
606
+ "pageSize" : page_size,
607
+ "changeTypeIndex" : "2"
608
+ }
609
+ response_text = fetch(self.web_page, url, payload)
610
+ error_code = response_text.get('code')
611
+ if str(error_code) != '0':
612
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
613
+ list_item = response_text['info']['data']['list']
614
+ total = response_text['info']['data']['count']
615
+ totalPage = math.ceil(total / page_size)
616
+
617
+ if len(list_item_cache) == int(total):
618
+ return list_item_cache
619
+
620
+ for page in range(2, totalPage + 1):
621
+ log(f'获取台账明细列表 第{page}/{totalPage}页')
622
+ payload['pageNumber'] = page
623
+ response_text = fetch(self.web_page, url, payload)
624
+ spu_list_new = response_text['info']['data']['list']
625
+ list_item += spu_list_new
626
+ time.sleep(0.1)
627
+
628
+ for item in list_item:
629
+ supplierSku = item['supplierSku']
630
+ item['store_username'] = self.store_username
631
+ item['store_name'] = self.store_name
632
+ item['store_manager'] = self.config.shein_store_manager.get(str(self.store_username).lower())
633
+ item['cost_price'] = self.bridge.get_sku_cost(supplierSku, self.config.erp_source)
634
+ item['sku_img'] = self.bridge.get_sku_img(supplierSku, self.config.erp_source)
635
+
636
+ write_dict_to_file(cache_file, list_item)
637
+
638
+ return list_item
639
+
306
640
  def get_ledger_list(self, source='mb'):
307
641
  page_num = 1
308
642
  page_size = 200 # 列表最多返回200条数据 大了没有用
@@ -398,12 +732,96 @@ class SheinLib:
398
732
 
399
733
  return list_item
400
734
 
735
+ def refresh_bridge_data_for_list(self, data_list, source='mb', sku_field='supplierSku'):
736
+ """
737
+ 刷新列表中的bridge数据(成本价和SKU图片)
738
+
739
+ Args:
740
+ data_list: 需要刷新的数据列表
741
+ source: ERP数据源,默认为'mb'
742
+ sku_field: SKU字段名,默认为'supplierSku'
743
+
744
+ Returns:
745
+ 刷新后的数据列表
746
+ """
747
+ log(f'开始刷新Bridge数据,共 {len(data_list)} 条记录', self.store_username, self.store_name)
748
+
749
+ for index, item in enumerate(data_list):
750
+ supplier_sku = item.get(sku_field)
751
+ if supplier_sku:
752
+ item['cost_price'] = self.bridge.get_sku_cost(supplier_sku, source)
753
+ item['sku_img'] = self.bridge.get_sku_img(supplier_sku, source)
754
+
755
+ # 每100条记录输出一次进度
756
+ if (index + 1) % 100 == 0:
757
+ log(f'刷新进度: {index + 1}/{len(data_list)}', self.store_username, self.store_name)
758
+
759
+ log(f'Bridge数据刷新完成', self.store_username, self.store_name)
760
+ return data_list
761
+
762
+ def get_vssv_order_list(self):
763
+ """
764
+ 获取VSSV订单列表
765
+
766
+ Args:
767
+ web_page: 页面对象
768
+ store_username: 店铺账号
769
+ store_name: 店铺名称
770
+
771
+ Returns:
772
+ list: 订单列表
773
+ """
774
+ page_num = 1
775
+ page_size = 200
776
+ first_day, last_day = TimeUtils.get_last_month_range()
777
+
778
+ cache_file = f'{self.config.auto_dir}/shein/vssv_order/vssv_order_list_{self.store_username}_{first_day}_{last_day}.json'
779
+ list_item = read_dict_from_file(cache_file, 3600 * 24 * 20)
780
+ if len(list_item) > 0:
781
+ return list_item
782
+
783
+ url = f"https://sso.geiwohuo.com/vssv/order/page"
784
+ payload = {
785
+ "deductionStatus": "2",
786
+ "beginTime" : f"{first_day} 00:00:00",
787
+ "endTime" : f"{last_day} 23:59:59",
788
+ "pageNumber" : page_num,
789
+ "pageSize" : page_size
790
+ }
791
+ response_text = fetch(self.web_page, url, payload)
792
+ error_code = response_text.get('code')
793
+ if str(error_code) != '0':
794
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
795
+ raise
796
+ list_item = response_text['info']['list']
797
+ total = response_text['info']['count']
798
+ totalPage = math.ceil(total / page_size)
799
+
800
+ for page in range(2, totalPage + 1):
801
+ log(f'获取VSSV订单列表 第{page}/{totalPage}页')
802
+ page_num = page
803
+ payload = {
804
+ "deductionStatus": "2",
805
+ "beginTime" : f"{first_day} 00:00:00",
806
+ "endTime" : f"{last_day} 23:59:59",
807
+ "pageNumber" : page_num,
808
+ "pageSize" : page_size
809
+ }
810
+ response_text = fetch(self.web_page, url, payload)
811
+ spu_list_new = response_text['info']['list']
812
+ list_item += spu_list_new
813
+ time.sleep(0.1)
814
+
815
+ write_dict_to_file(cache_file, list_item)
816
+
817
+ return list_item
818
+
401
819
  def get_replenish_list(self):
402
820
  page_num = 1
403
821
  page_size = 50
404
822
  first_day, last_day = TimeUtils.get_last_month_range()
405
823
 
406
- cache_file = f'{self.config.auto_dir}/cache/replenish_list_{self.store_username}_{first_day}_{last_day}.json'
824
+ cache_file = f'{self.config.auto_dir}/shein/cache/replenish_list_{self.store_username}_{first_day}_{last_day}.json'
407
825
  list_item = read_dict_from_file(cache_file, 3600 * 24 * 20)
408
826
  if len(list_item) > 0:
409
827
  return list_item
@@ -425,128 +843,966 @@ class SheinLib:
425
843
  totalPage = math.ceil(total / page_size)
426
844
 
427
845
  for page in range(2, totalPage + 1):
428
- log(f'获取不扣款列表 第{page}/{totalPage}页')
429
- page_num = page
430
- payload = {
431
- "page" : page_num,
432
- "perPage" : page_size,
433
- "tabType" : 2,
434
- "addTimeStart": f"{first_day} 00:00:00",
435
- "addTimeEnd" : f"{last_day} 23:59:59"
436
- }
846
+ log(f'获取不扣款列表 第{page}/{totalPage}页')
847
+ page_num = page
848
+ payload = {
849
+ "page" : page_num,
850
+ "perPage" : page_size,
851
+ "tabType" : 2,
852
+ "addTimeStart": f"{first_day} 00:00:00",
853
+ "addTimeEnd" : f"{last_day} 23:59:59"
854
+ }
855
+ response_text = fetch(self.web_page, url, payload)
856
+ spu_list_new = response_text['info']['data']
857
+ list_item += spu_list_new
858
+ time.sleep(0.1)
859
+
860
+ write_dict_to_file(cache_file, list_item)
861
+
862
+ return list_item
863
+
864
+ def get_return_list(self):
865
+ page_num = 1
866
+ page_size = 200
867
+ first_day, last_day = TimeUtils.get_last_month_range()
868
+
869
+ cache_file = f'{self.config.auto_dir}/shein/cache/return_list_{self.store_username}_{first_day}_{last_day}.json'
870
+ list_item = read_dict_from_file(cache_file, 3600 * 24 * 20)
871
+ if len(list_item) > 0:
872
+ return list_item
873
+
874
+ url = f"https://sso.geiwohuo.com/pfmp/returnOrder/page"
875
+ payload = {
876
+ "addTimeStart" : f"{first_day} 00:00:00",
877
+ "addTimeEnd" : f"{last_day} 23:59:59",
878
+ "returnOrderStatusList": [4],
879
+ "page" : page_num,
880
+ "perPage" : page_size
881
+ }
882
+ response_text = fetch(self.web_page, url, payload)
883
+ error_code = response_text.get('code')
884
+ if str(error_code) != '0':
885
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
886
+
887
+ list_item = response_text['info']['data']
888
+ total = response_text['info']['meta']['count']
889
+ totalPage = math.ceil(total / page_size)
890
+
891
+ for page in range(2, totalPage + 1):
892
+ log(f'获取不扣款列表 第{page}/{totalPage}页')
893
+ page_num = page
894
+ payload = {
895
+ "addTimeStart" : f"{first_day} 00:00:00",
896
+ "addTimeEnd" : f"{last_day} 23:59:59",
897
+ "returnOrderStatusList": [4],
898
+ "page" : page_num,
899
+ "perPage" : page_size
900
+ }
901
+ response_text = fetch(self.web_page, url, payload)
902
+ spu_list_new = response_text['info']['data']
903
+ list_item += spu_list_new
904
+ time.sleep(0.1)
905
+
906
+ write_dict_to_file(cache_file, list_item)
907
+
908
+ return list_item
909
+
910
+ def get_comment_list(self):
911
+ cache_file = f'{self.config.auto_dir}/shein/dict/comment_list_{TimeUtils.today_date()}.json'
912
+ comment_list = read_dict_from_file_ex(cache_file, self.store_username, 3600)
913
+ if len(comment_list) > 0:
914
+ return comment_list
915
+
916
+ page_num = 1
917
+ page_size = 50
918
+
919
+ yesterday = TimeUtils.get_yesterday()
920
+
921
+ url = f"https://sso.geiwohuo.com/gsp/goods/comment/list"
922
+ payload = {
923
+ "page" : page_num,
924
+ "perPage" : page_size,
925
+ "startCommentTime": f"{yesterday} 00:00:00",
926
+ "commentEndTime" : f"{yesterday} 23:59:59",
927
+ "commentStarList" : ["3", "2", "1"]
928
+ }
929
+ response_text = fetch(self.web_page, url, payload)
930
+ error_code = response_text.get('code')
931
+ if str(error_code) != '0':
932
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
933
+
934
+ comment_list = response_text['info']['data']
935
+ total = response_text['info']['meta']['count']
936
+ totalPage = math.ceil(total / page_size)
937
+
938
+ for page in range(2, totalPage + 1):
939
+ log(f'获取评价列表 第{page}/{totalPage}页')
940
+ page_num = page
941
+ payload['page'] = page_num
942
+ response_text = fetch(self.web_page, url, payload)
943
+ comment_list = response_text['info']['data']
944
+ time.sleep(0.1)
945
+
946
+ write_dict_to_file_ex(cache_file, {self.store_username: comment_list}, [self.store_username])
947
+ return comment_list
948
+
949
+ def get_last_month_outbound_amount(self):
950
+ url = "https://sso.geiwohuo.com/mils/report/month/list"
951
+ start, end = TimeUtils.get_current_year_range()
952
+ payload = {
953
+ "reportDateStart": start, "reportDateEnd": end, "pageNumber": 1, "pageSize": 50
954
+ }
955
+ response_text = fetch(self.web_page, url, payload)
956
+ error_code = response_text.get('code')
957
+ if str(error_code) != '0':
958
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
959
+ info = response_text.get('info')
960
+ lst = info.get('data', {}).get('list', [])
961
+ if not lst:
962
+ log(f'⚠️ {self.store_name} 最近一个月无出库记录,金额为0')
963
+ return 0
964
+
965
+ last_item = lst[-1]
966
+ log(f'正在获取 {self.store_name} 最近一个月出库金额: {last_item["totalCustomerAmount"]}')
967
+ return last_item['totalCustomerAmount']
968
+
969
+ def query_attribute_multi(self, attribute_id_list):
970
+ url = "https://sso.geiwohuo.com/spmp-api-prefix/spmp/attribute/query_attribute_multi"
971
+ payload = {
972
+ "attribute_id_list": attribute_id_list,
973
+ }
974
+ response_text = fetch(self.web_page, url, payload)
975
+ error_code = response_text.get('code')
976
+ if str(error_code) != '0':
977
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
978
+ info = response_text.get('info')
979
+ lst = info.get('data', {})
980
+ return lst
981
+
982
+ def get_product_attr(self, spu, attr_name):
983
+ try:
984
+ product_detail = self.get_product_detail(spu)
985
+ product_type_id = product_detail.get('product_type_id')
986
+ category_id = product_detail.get('category_id')
987
+
988
+ if not product_type_id or not category_id:
989
+ return None # 或者根据需要返回一个默认值
990
+
991
+ attribute_template = self.get_attribute_templates(spu, category_id, [product_type_id])
992
+ attr_info = attribute_template.get('attribute_infos', [])
993
+
994
+ # 查找材质属性映射,防止没有匹配项
995
+ attr_item = next((item for item in attr_info if item.get('attribute_name') == attr_name), None)
996
+ if not attr_item:
997
+ return None # 或者返回一个默认值
998
+
999
+ attr_id = attr_item.get('attribute_id')
1000
+
1001
+ # 拿到产品材质的属性值ID
1002
+ product_attribute_list = product_detail.get('product_attribute_list', [])
1003
+ attribute_value_id = next((item['attribute_value_id'] for item in product_attribute_list if item.get('attribute_id') == attr_id), None)
1004
+ if not attribute_value_id:
1005
+ return None # 或者返回一个默认值
1006
+
1007
+ # 获取属性值名称
1008
+ attr_value = next((item['attribute_value'] for item in attr_item.get('attribute_value_info_list', []) if item.get('attribute_value_id') == attribute_value_id), None)
1009
+ return attr_value # 返回找到的属性值
1010
+ except Exception as e:
1011
+ log(f"Error occurred: {e}")
1012
+ send_exception()
1013
+ return None # 或者返回一个默认值
1014
+
1015
+ def get_attribute_templates(self, spu_name, category_id, product_type_id_list):
1016
+ log(f'正在获取 {spu_name} 商品属性模板')
1017
+
1018
+ if not isinstance(product_type_id_list, list):
1019
+ raise '参数错误: product_type_id_list 需要是列表'
1020
+
1021
+ cache_file = f'{self.config.auto_dir}/shein/attribute/attribute_template_{spu_name}.json'
1022
+ attr_list = read_dict_from_file(cache_file, 3600 * 24 * 7)
1023
+ if len(attr_list) > 0:
1024
+ return attr_list
1025
+
1026
+ url = f"https://sso.geiwohuo.com/spmp-api-prefix/spmp/basic/query_attribute_templates"
1027
+ payload = {
1028
+ "category_id" : category_id,
1029
+ "for_update" : True,
1030
+ "product_type_id_list": product_type_id_list,
1031
+ "spu_name" : spu_name
1032
+ }
1033
+ response_text = fetch(self.web_page, url, payload)
1034
+ error_code = response_text.get('code')
1035
+ if str(error_code) != '0':
1036
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
1037
+ info = response_text.get('info')
1038
+
1039
+ data = info.get('data')[0]
1040
+ write_dict_to_file(cache_file, data)
1041
+ return data
1042
+
1043
+ def get_product_detail(self, spu_name, cache_interval=3600 * 24 * 7):
1044
+ cache_file = f'{self.config.auto_dir}/shein/product_detail/product_detail_{spu_name}.json'
1045
+ info = read_dict_from_file(cache_file, cache_interval)
1046
+ if len(info) > 0:
1047
+ return info
1048
+
1049
+ log(f'正在获取 {spu_name} 商品详情')
1050
+ url = f"https://sso.geiwohuo.com/spmp-api-prefix/spmp/product/get_product_detail"
1051
+ payload = {
1052
+ "spu_name": spu_name
1053
+ }
1054
+ response_text = fetch(self.web_page, url, payload)
1055
+ error_code = response_text.get('code')
1056
+ if str(error_code) != '0':
1057
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
1058
+ info = response_text.get('info')
1059
+
1060
+ # 获取 area_attribute_id
1061
+ sample_sku_back_size = info.get('sample_sku_back_size', None)
1062
+ if sample_sku_back_size is not None:
1063
+ area_attribute_ids = [item['area_attribute_id'] for item in sample_sku_back_size.get('area_info_list', [])]
1064
+ attribute_multi = self.query_attribute_multi(area_attribute_ids)
1065
+ info["attribute_multi"] = attribute_multi
1066
+
1067
+ write_dict_to_file(cache_file, info)
1068
+ return info
1069
+
1070
+ def product_month_analysis(self, start_date, end_date):
1071
+ # 店铺信息包含 店铺名称 skc上架状态 skc商品层级 统计周期
1072
+ # 商品信息包含 SPU,SKC,商家SKC,质量等级,商品分类,上架日期,上架天数
1073
+ # SKU信息包含 商家SKU,属性集
1074
+ # 前9列均是skc维度,从SKU信息开始 后面是SKU维度
1075
+ excel_data = [
1076
+ ['店铺信息', '商品信息', 'SKC图片', '30天SKC曝光', '30天SKC点击率', '30天SKC转化率', '评论数', '差评率', '客单退货件数', 'SKU信息', 'SKU图片', 'SKU30天销量', '销售额', '核价', '成本', '30天利润', '30天利润率', 'skc']
1077
+ ]
1078
+ excel_data2 = [
1079
+ ['店铺信息', '商品信息', 'SKC图片', '日期', 'SKC销量', 'SKC曝光', 'SKC点击率', 'SKC转化率', 'skc']
1080
+ ]
1081
+ skc_list = self.get_bak_base_info()
1082
+ cache_file = f'{self.config.auto_dir}/shein/sku_price/sku_price_{self.store_username}.json'
1083
+ dict_sku = read_dict_from_file(cache_file)
1084
+ cache_file = f'{self.config.auto_dir}/shein/quality_label/quality_label_{self.store_username}.json'
1085
+ dict_quality_label = read_dict_from_file(cache_file)
1086
+
1087
+ cache_file_analysis = f'{self.config.auto_dir}/shein/product_analysis/skc_skc_analysis_{self.store_username}_{start_date}_{end_date}.json'
1088
+ dict_analysis = read_dict_from_file(cache_file_analysis)
1089
+
1090
+ for skc_item in skc_list:
1091
+ categoryName = skc_item['categoryName']
1092
+ spu = skc_item['spu'] # SPU
1093
+ skc = skc_item['skc'] # SKC
1094
+ supplierCode = skc_item['supplierCode'] # 商家SKC
1095
+ skc_img = skc_item['picUrl'] # SKC图片
1096
+ shelfDate = skc_item['shelfDate'] # 上架日期
1097
+ shelfDays = skc_item['shelfDays'] # 上架天数
1098
+ shelfStatusName = skc_item['shelfStatus']['name'] # 上架状态
1099
+ quality_label = dict_quality_label.get(skc, {}).get('name', '') # 质量等级
1100
+ if quality_label == '无判断':
1101
+ quality_label = ''
1102
+
1103
+ if shelfStatusName == '待上架':
1104
+ log('商品未上架跳过:', skc)
1105
+ continue
1106
+
1107
+ goods_level = skc_item.get('goodsLevel', {}).get('name', '-')
1108
+ if goods_level in ['自主停产', '退供款']:
1109
+ log(f'商品 {goods_level} 跳过:', skc)
1110
+ continue
1111
+
1112
+ dict_sku_sales = self.get_skc_actual_sales_dict(skc, start_date, end_date)
1113
+ dict_skc_trend = self.get_skc_trend(spu, skc, start_date, end_date)
1114
+
1115
+ # 检查这个 SKC 是否有任何 SKU 有销量(与 excel_data 保持一致)
1116
+ has_sales = False
1117
+ for sku_item in skc_item['skuList']:
1118
+ c30dSaleCnt = sku_item.get('c30dSaleCnt', 0)
1119
+ attr = sku_item.get('attr', '')
1120
+ if attr != '合计' and int(c30dSaleCnt) > 0:
1121
+ has_sales = True
1122
+ break
1123
+
1124
+ # 只有当有趋势数据且有销量时才添加到 excel_data2(与 excel_data 保持一致)
1125
+ if dict_skc_trend and has_sales:
1126
+ for stat_date, dict_item in dict_skc_trend.items():
1127
+ store_info = f'{self.store_username}\n{self.store_name}\n({shelfStatusName})\n{goods_level}\n{start_date}\n{end_date}'
1128
+ product_info = f'SPU: {spu}\nSKC: {skc}\n商家SKC: {supplierCode}\n商品分类: {categoryName}\n上架日期: {shelfDate}\n上架天数: {shelfDays}\n质量等级: {quality_label}'
1129
+
1130
+ row_item2 = []
1131
+ row_item2.append(store_info)
1132
+ row_item2.append(product_info)
1133
+ row_item2.append(skc_img)
1134
+ row_item2.append(stat_date)
1135
+ row_item2.append(dict_item.get('saleCnt', 0))
1136
+ row_item2.append(dict_item.get('epsUvIdx', 0))
1137
+ row_item2.append(dict_item.get('epsGdsCtrIdx', 0))
1138
+ row_item2.append(dict_item.get('gdsPayCtrIdx', 0))
1139
+ row_item2.append(skc if skc else '') # 确保 skc 不为 None
1140
+ excel_data2.append(row_item2)
1141
+
1142
+ for sku_item in skc_item['skuList']:
1143
+ supplierSku = sku_item['supplierSku'] # 商家SKU
1144
+ attr = sku_item['attr'] # 属性集
1145
+ sku = sku_item['skuCode'] # SKU
1146
+
1147
+ c30dSaleCnt = sku_item['c30dSaleCnt'] # 近30天销量
1148
+ if attr == '合计' or int(c30dSaleCnt) == 0:
1149
+ log(f'跳过: {supplierSku},近30天销量: {c30dSaleCnt}')
1150
+ continue
1151
+
1152
+ price = dict_sku[sku]
1153
+
1154
+ store_info = f'{self.store_username}\n{self.store_name}\n({shelfStatusName})\n{goods_level}\n{start_date}\n{end_date}'
1155
+ product_info = f'SPU: {spu}\nSKC: {skc}\n商家SKC: {supplierCode}\n商品分类: {categoryName}\n上架日期: {shelfDate}\n上架天数: {shelfDays}\n质量等级: {quality_label}'
1156
+
1157
+ epsUvIdx = dict_analysis.get(skc, {}).get('epsUvIdx', 0)
1158
+ epsGdsCtrIdx = dict_analysis.get(skc, {}).get('epsGdsCtrIdx', 0)
1159
+ gdsPayCtrIdx = dict_analysis.get(skc, {}).get('gdsPayCtrIdx', 0)
1160
+ totalCommentCnt = dict_analysis.get(skc, {}).get('totalCommentCnt', 0)
1161
+ badCommentRate = dict_analysis.get(skc, {}).get('badCommentRate', 0)
1162
+ returnOrderCnt = dict_analysis.get(skc, {}).get('returnOrderCnt', 0)
1163
+
1164
+ sku_info = f'平台SKU: {sku}\n商家SKU: {supplierSku}\n属性集: {attr}'
1165
+ sku_img = self.bridge.get_sku_img(supplierSku, 'mb')
1166
+ # cost_price = self.bridge.get_sku_cost(sku_item['supplierSku'], self.config.erp_source)
1167
+ cost_price = self.bridge.get_sku_cost(sku_item['supplierSku'], 'mb')
1168
+
1169
+ row_item = []
1170
+ row_item.append(store_info)
1171
+ row_item.append(product_info)
1172
+ row_item.append(skc_img)
1173
+ row_item.append(epsUvIdx)
1174
+ row_item.append(epsGdsCtrIdx)
1175
+ row_item.append(gdsPayCtrIdx)
1176
+ row_item.append(totalCommentCnt)
1177
+ row_item.append(badCommentRate)
1178
+ row_item.append(returnOrderCnt)
1179
+ row_item.append(sku_info)
1180
+ row_item.append(sku_img)
1181
+ row_item.append(dict_sku_sales.get(sku, 0))
1182
+ row_item.append('') # 销售额(公式计算)
1183
+ row_item.append(price) # 核价
1184
+ row_item.append(cost_price) # 成本
1185
+ row_item.append('') # 30天利润(公式计算)
1186
+ row_item.append('') # 30天利润率(公式计算)
1187
+ row_item.append(skc)
1188
+ excel_data.append(row_item)
1189
+
1190
+ cache_file = f'{self.config.auto_dir}/shein/product_analysis/product_analysis_{TimeUtils.today_date()}.json'
1191
+ write_dict_to_file_ex(cache_file, {self.store_username: excel_data}, [self.store_username])
1192
+
1193
+ cache_file = f'{self.config.auto_dir}/shein/product_analysis/product_analysis_2_{TimeUtils.today_date()}.json'
1194
+ write_dict_to_file_ex(cache_file, {self.store_username: excel_data2}, [self.store_username])
1195
+ return excel_data
1196
+
1197
+ def get_product(self):
1198
+ excel_data = [
1199
+ ['店铺信息', '产品信息', 'SKC', '商家SKC', 'SKC图片', '商家SKU', '属性集', '近7天销量', '近30天销量', '核价', 'ERP成本价', '近7天利润', '近30天利润', '导出时间', 'SPU', 'SKC_FOR_STAT']
1200
+ ]
1201
+ skc_list = self.get_bak_base_info()
1202
+ cache_file = f'{self.config.auto_dir}/shein/sku_price/sku_price_{self.store_username}.json'
1203
+ dict_sku = read_dict_from_file(cache_file)
1204
+ for skc_item in skc_list:
1205
+ categoryName = skc_item['categoryName']
1206
+ spu = skc_item['spu']
1207
+ skc = skc_item['skc']
1208
+ supplierCode = skc_item['supplierCode']
1209
+ skc_img = skc_item['picUrl']
1210
+ shelfDate = skc_item['shelfDate']
1211
+ shelfDays = skc_item['shelfDays']
1212
+ shelfStatusName = skc_item['shelfStatus']['name']
1213
+ # if shelfStatusName != '已下架':
1214
+ # continue
1215
+ for sku_item in skc_item['skuList']:
1216
+ supplierSku = sku_item['supplierSku']
1217
+ attr = sku_item['attr']
1218
+ sku = sku_item['skuCode']
1219
+ c7dSaleCnt = sku_item['c7dSaleCnt']
1220
+ c30dSaleCnt = sku_item['c30dSaleCnt']
1221
+ if attr == '合计' or int(c30dSaleCnt) == 0:
1222
+ log(f'跳过: {supplierSku},近30天销量: {c30dSaleCnt}')
1223
+ continue
1224
+
1225
+ price = dict_sku[sku]
1226
+
1227
+ product_info = f'SPU: {spu}\n商品分类: {categoryName}\n上架日期: {shelfDate}\n上架天数: {shelfDays}\n上架状态: {shelfStatusName}'
1228
+
1229
+ store_info = f'{self.store_username}\n{self.store_name}\n{self.config.shein_store_manager.get(self.store_username)}'
1230
+
1231
+ row_item = []
1232
+ row_item.append(store_info)
1233
+ row_item.append(product_info)
1234
+ row_item.append(skc)
1235
+ row_item.append(supplierCode)
1236
+ row_item.append(skc_img)
1237
+ row_item.append(supplierSku)
1238
+ row_item.append(attr)
1239
+ row_item.append(c7dSaleCnt)
1240
+ row_item.append(c30dSaleCnt)
1241
+ row_item.append(price)
1242
+ row_item.append('')
1243
+ row_item.append('')
1244
+ row_item.append('')
1245
+ row_item.append(TimeUtils.current_datetime())
1246
+ row_item.append(spu)
1247
+ row_item.append(skc)
1248
+ excel_data.append(row_item)
1249
+
1250
+ cache_file = f'{self.config.auto_dir}/shein/product/product_{TimeUtils.today_date()}.json'
1251
+ write_dict_to_file_ex(cache_file, {self.store_username: excel_data}, [self.store_username])
1252
+ return excel_data
1253
+
1254
+ def generate_product_dict(self):
1255
+ pass
1256
+ dict_sku_to_skc = []
1257
+ dict_sku_not_found = []
1258
+ skc_list = self.get_bak_base_info()
1259
+ for skc_item in skc_list:
1260
+ skc_item['store_username'] = self.store_username
1261
+ skc_item['store_name'] = self.store_name
1262
+ skc_item['store_manager'] = self.config.shein_store_manager.get(str(self.store_username).lower())
1263
+ spu = skc_item['spu']
1264
+ skc = skc_item['skc']
1265
+ supplierCode = skc_item['supplierCode']
1266
+
1267
+ shelf_status = skc_item.get('shelfStatus', {}).get('name', '-')
1268
+ if int(skc_item['shelfStatus']['value']) != 1:
1269
+ log('商品未上架跳过:', skc)
1270
+ continue
1271
+
1272
+ goods_level = skc_item.get('goodsLevel', {}).get('name', '-')
1273
+ if goods_level in ['自主停产', '退供款']:
1274
+ log(f'商品{goods_level}跳过:', skc)
1275
+ continue
1276
+
1277
+ # 倒序遍历 skuList,安全删除
1278
+ for i in range(len(skc_item['skuList']) - 1, -1, -1):
1279
+ sku_item = skc_item['skuList'][i]
1280
+ if sku_item['skuCode'] == '合计':
1281
+ del skc_item['skuList'][i] # 删除“合计”
1282
+ continue
1283
+
1284
+ cost_price = self.bridge.get_sku_cost(sku_item['supplierSku'], self.config.erp_source)
1285
+ if not isinstance(cost_price, (int, float)):
1286
+ dict_sku_not_found.append([
1287
+ self.store_username,
1288
+ f'{self.store_name}',
1289
+ self.config.shein_store_manager.get(str(self.store_username).lower()),
1290
+ spu,
1291
+ skc,
1292
+ supplierCode,
1293
+ sku_item['supplierSku'],
1294
+ shelf_status,
1295
+ goods_level,
1296
+ '忆托未匹配到成本价,可能原因: 1.没填商家SKU,2.商家SKU没有绑定本地SKU,3.商家SKU填写错误'
1297
+ ])
1298
+ elif cost_price == 0:
1299
+ dict_sku_not_found.append([
1300
+ self.store_username,
1301
+ f'{self.store_name}',
1302
+ self.config.shein_store_manager.get(str(self.store_username).lower()),
1303
+ spu,
1304
+ skc,
1305
+ supplierCode,
1306
+ sku_item['supplierSku'],
1307
+ shelf_status,
1308
+ goods_level,
1309
+ '忆托未匹配到成本价为:0'
1310
+ ])
1311
+
1312
+ dict_sku_to_skc.append([
1313
+ sku_item['supplierSku'],
1314
+ supplierCode,
1315
+ ])
1316
+
1317
+ cache_file = f'{self.config.auto_dir}/shein/dict/sku_not_found.json'
1318
+ write_dict_to_file_ex(cache_file, {self.store_username: dict_sku_not_found}, [self.store_username])
1319
+
1320
+ cache_file = f'{self.config.auto_dir}/shein/dict/sku_to_skc.json'
1321
+ write_dict_to_file_ex(cache_file, {self.store_username: dict_sku_to_skc}, [self.store_username])
1322
+
1323
+ # 存储商品库
1324
+ def store_product_info(self):
1325
+ # todo 商品详情 属性 规格 图片 重量 与 尺寸
1326
+ skc_list = self.get_bak_base_info()
1327
+ cache_file = f'{self.config.auto_dir}/shein/sku_price/sku_price_{self.store_username}.json'
1328
+ dict_sku = read_dict_from_file(cache_file)
1329
+ dict_product_detail = []
1330
+ for skc_item in skc_list:
1331
+ skc_item['store_username'] = self.store_username
1332
+ skc_item['store_name'] = self.store_name
1333
+ skc_item['store_manager'] = self.config.shein_store_manager.get(str(self.store_username).lower())
1334
+ spu = skc_item['spu']
1335
+ if spu not in dict_product_detail:
1336
+ dict_product_detail.append(spu)
1337
+ material = self.get_product_attr(spu, '材质')
1338
+ log(material) # 这一步是为了获取 spu 详情和属性
1339
+
1340
+ # 倒序遍历 skuList,安全删除
1341
+ for i in range(len(skc_item['skuList']) - 1, -1, -1):
1342
+ sku_item = skc_item['skuList'][i]
1343
+ if sku_item['skuCode'] == '合计':
1344
+ del skc_item['skuList'][i] # 删除“合计”
1345
+ continue
1346
+ sku_item['price'] = dict_sku[sku_item['skuCode']]
1347
+ cost_price = self.bridge.get_sku_cost(sku_item['supplierSku'], self.config.erp_source)
1348
+ sku_item['erp_cost_price'] = cost_price if isinstance(cost_price, (int, float)) else None
1349
+ sku_item['erp_supplier_name'] = self.bridge.get_sku_supplier(sku_item['supplierSku'], self.config.erp_source)
1350
+ stock = self.bridge.get_sku_stock(sku_item['supplierSku'], self.config.erp_source)
1351
+ sku_item['erp_stock'] = stock if isinstance(stock, (int, float)) else None
1352
+
1353
+ cache_file = f'{self.config.auto_dir}/shein/product/skc_list_{self.store_username}.json'
1354
+ write_dict_to_file_ex(cache_file, {self.store_username: skc_list}, [self.store_username])
1355
+
1356
+ skc_file = f'{self.config.auto_dir}/shein/product/skc_list_file.json'
1357
+ write_dict_to_file_ex(skc_file, {self.store_username: cache_file}, [self.store_username])
1358
+
1359
+ detail_file = f'{self.config.auto_dir}/shein/product/product_detail_file.json'
1360
+ write_dict_to_file_ex(detail_file, {self.store_username: dict_product_detail}, [self.store_username])
1361
+
1362
+ def get_skc_diagnose_dict(self, start_date="", end_date=""):
1363
+ log(f'获取商品分析某个月的字典 {start_date} {end_date} {self.store_name} {self.store_username}')
1364
+
1365
+ cache_file_analysis = f'{self.config.auto_dir}/shein/product_analysis/skc_skc_analysis_{self.store_username}_{start_date}_{end_date}.json'
1366
+
1367
+ dict_analysis = read_dict_from_file(cache_file_analysis)
1368
+ if len(dict_analysis) > 0:
1369
+ return dict_analysis
1370
+
1371
+ dt_goods = self.get_dt_time_goods()
1372
+ if not TimeUtils.is_yesterday_date(dt_goods, "%Y%m%d"):
1373
+ log("数据尚未更新: dt_goods:", dt_goods)
1374
+ return []
1375
+
1376
+ url = "https://sso.geiwohuo.com/sbn/new_goods/get_skc_diagnose_list"
1377
+ page_num = 1
1378
+ page_size = 100
1379
+ payload = {
1380
+ "areaCd" : "cn",
1381
+ "dt" : dt_goods,
1382
+ "countrySite": [
1383
+ "shein-all"
1384
+ ],
1385
+ "startDate" : start_date.replace('-', ""),
1386
+ "endDate" : end_date.replace('-', ""),
1387
+ "pageNum" : page_num,
1388
+ "pageSize" : page_size,
1389
+ }
1390
+ response_text = fetch(self.web_page, url, payload)
1391
+ error_code = response_text.get('code')
1392
+ if str(error_code) != '0':
1393
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
1394
+ spu_list = response_text['info']['data']
1395
+ total = response_text['info']['meta']['count']
1396
+ totalPage = math.ceil(total / page_size)
1397
+
1398
+ for page in range(2, totalPage + 1):
1399
+ log(f'获取商品分析列表(最近上架的) 第{page}/{totalPage}页')
1400
+ payload.update({"pageNum": page})
1401
+ response_text = fetch(self.web_page, url, payload)
1402
+ spu_list_new = response_text['info']['data']
1403
+ spu_list += spu_list_new
1404
+ time.sleep(0.3)
1405
+
1406
+ cache_file = f'{self.config.auto_dir}/shein/product_analysis/skc_dict_{self.store_username}_{start_date}_{end_date}.json'
1407
+ write_dict_to_file(cache_file, spu_list)
1408
+
1409
+ for skc_item in spu_list:
1410
+ skc = skc_item['skc']
1411
+ skc_item['store_username'] = self.store_username
1412
+ skc_item['store_name'] = self.store_name
1413
+ dict_analysis[skc] = skc_item
1414
+
1415
+ write_dict_to_file(cache_file_analysis, dict_analysis)
1416
+
1417
+ return dict_analysis
1418
+
1419
+ def get_skc_diagnose_list(self, shelf_date_begin="", shelf_date_end=""):
1420
+ log(f'获取商品分析列表(最近上架的或在售的) {shelf_date_begin} {shelf_date_end} {self.store_name} {self.store_username}')
1421
+
1422
+ dt_goods = self.get_dt_time_goods()
1423
+ if not TimeUtils.is_yesterday_date(dt_goods, "%Y%m%d"):
1424
+ log("数据尚未更新: dt_goods:", dt_goods)
1425
+ return []
1426
+
1427
+ yesterday = TimeUtils.get_past_nth_day(1, None, '%Y%m%d')
1428
+
1429
+ url = "https://sso.geiwohuo.com/sbn/new_goods/get_skc_diagnose_list"
1430
+ page_num = 1
1431
+ page_size = 100
1432
+ payload = {
1433
+ "areaCd" : "cn",
1434
+ "dt" : dt_goods,
1435
+ "countrySite": [
1436
+ "shein-all"
1437
+ ],
1438
+ "startDate" : yesterday,
1439
+ "endDate" : yesterday,
1440
+ "pageNum" : page_num,
1441
+ "pageSize" : page_size,
1442
+ "onsaleFlag" : 1,
1443
+ # "localFrstSaleBeginDate": shelf_date_begin,
1444
+ # "localFrstSaleEndDate" : shelf_date_end,
1445
+ }
1446
+ response_text = fetch(self.web_page, url, payload)
1447
+ error_code = response_text.get('code')
1448
+ if str(error_code) != '0':
1449
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
1450
+ spu_list = response_text['info']['data']
1451
+ total = response_text['info']['meta']['count']
1452
+ totalPage = math.ceil(total / page_size)
1453
+
1454
+ for page in range(2, totalPage + 1):
1455
+ log(f'获取商品分析列表(最近上架的) 第{page}/{totalPage}页')
1456
+ payload.update({"pageNum": page})
437
1457
  response_text = fetch(self.web_page, url, payload)
438
1458
  spu_list_new = response_text['info']['data']
439
- list_item += spu_list_new
440
- time.sleep(0.1)
1459
+ spu_list += spu_list_new
1460
+ time.sleep(0.3)
441
1461
 
442
- write_dict_to_file(cache_file, list_item)
1462
+ cache_file = f'{self.config.auto_dir}/shein/product_analysis/skc_list_{self.store_username}.json'
1463
+ write_dict_to_file(cache_file, spu_list)
1464
+
1465
+ cache_file = f'{self.config.auto_dir}/shein/dict/skc_shelf_date_{self.store_username}.json'
1466
+ dict_skc_shelf_date = read_dict_from_file(cache_file)
1467
+
1468
+ # 活动信息
1469
+ # AB实验数据
1470
+
1471
+ # 预先过滤掉不需要的商品状态
1472
+ log(f'过滤前商品数量: {len(spu_list)}')
1473
+ exclude_levels = ['退供款', '自主停产', '自主下架']
1474
+ spu_list = [item for item in spu_list if item['layerNm'] not in exclude_levels]
1475
+ log(f'过滤后剩余商品数量: {len(spu_list)}')
1476
+
1477
+ for skc_item in spu_list:
1478
+ skc = skc_item['skc']
1479
+ skc_item['stat_date'] = datetime.strptime(yesterday, "%Y%m%d").strftime("%Y-%m-%d")
1480
+ skc_item['store_username'] = self.store_username
1481
+ skc_item['store_name'] = self.store_name
1482
+ skc_item['shelf_date'] = dict_skc_shelf_date[skc]
1483
+ ab_cache_file = f'{self.config.auto_dir}/shein/cache/ab_test_list_{skc}_{TimeUtils.today_date()}.json'
1484
+ skc_item['ab_test'] = read_dict_from_file(ab_cache_file)
1485
+ for prom_inf_ing in skc_item['promCampaign'].get('promInfIng') or []:
1486
+ prom_id = prom_inf_ing['promId']
1487
+ log('prom_id:', prom_id, len(prom_id))
1488
+ if len(prom_id) >= 11:
1489
+ # 托管活动
1490
+ prom_inf_ing['promDetail'] = self.get_skc_activity_price_info(skc, prom_id)
1491
+ elif len(prom_id) >= 8:
1492
+ # 营销工具
1493
+ prom_inf_ing['promDetail'] = self.query_goods_detail(prom_id)
1494
+ else:
1495
+ # 营销活动
1496
+ prom_inf_ing['promDetail'] = self.get_partake_activity_detail(prom_id, skc)
1497
+
1498
+ for prom_inf_ready in skc_item['promCampaign'].get('promInfReady') or []:
1499
+ prom_id = prom_inf_ready['promId']
1500
+ log('prom_id:', prom_id, len(prom_id))
1501
+ if len(prom_id) >= 11:
1502
+ prom_inf_ready['promDetail'] = self.get_skc_activity_price_info(skc, prom_id)
1503
+ elif len(prom_id) >= 8:
1504
+ prom_inf_ready['promDetail'] = self.query_goods_detail(prom_id)
1505
+ else:
1506
+ prom_inf_ready['promDetail'] = self.get_partake_activity_detail(prom_id, skc)
443
1507
 
444
- return list_item
1508
+ cache_file = f'{self.config.auto_dir}/shein/product_analysis/skc_model_{self.store_username}_{TimeUtils.today_date()}.json'
1509
+ write_dict_to_file(cache_file, spu_list)
445
1510
 
446
- def get_return_list(self):
447
- page_num = 1
448
- page_size = 200
449
- first_day, last_day = TimeUtils.get_last_month_range()
1511
+ return spu_list
450
1512
 
451
- cache_file = f'{self.config.auto_dir}/cache/return_list_{self.store_username}_{first_day}_{last_day}.json'
452
- list_item = read_dict_from_file(cache_file, 3600 * 24 * 20)
453
- if len(list_item) > 0:
454
- return list_item
1513
+ # 获取备货信息列表 最近35天上架的
1514
+ def get_latest_shelf_list(self, shelf_date_begin="", shelf_date_end=""):
1515
+ log(f'获取备货信息列表(最近上架的或已上架的) {shelf_date_begin} {shelf_date_end} {self.store_name} {self.store_username}')
455
1516
 
456
- url = f"https://sso.geiwohuo.com/pfmp/returnOrder/page"
1517
+ dict_skc_shelf_date = {}
1518
+
1519
+ url = "https://sso.geiwohuo.com/idms/goods-skc/list"
1520
+ pageNumber = 1
1521
+ pageSize = 100
457
1522
  payload = {
458
- "addTimeStart" : f"{first_day} 00:00:00",
459
- "addTimeEnd" : f"{last_day} 23:59:59",
460
- "returnOrderStatusList": [4],
461
- "page" : page_num,
462
- "perPage" : page_size
1523
+ "pageNumber" : pageNumber,
1524
+ "pageSize" : pageSize,
1525
+ "supplierCodes" : "",
1526
+ "skcs" : "",
1527
+ "spu" : "",
1528
+ "c7dSaleCntBegin" : "",
1529
+ "c7dSaleCntEnd" : "",
1530
+ "goodsLevelIdList" : [],
1531
+ "supplyStatus" : "",
1532
+ "shelfStatus" : 1, # 已上架
1533
+ "categoryIdList" : [],
1534
+ "skcStockBegin" : "",
1535
+ "skcStockEnd" : "",
1536
+ "skuStockBegin" : "",
1537
+ "skuStockEnd" : "",
1538
+ "skcSaleDaysBegin" : "",
1539
+ "skcSaleDaysEnd" : "",
1540
+ "skuSaleDaysBegin" : "",
1541
+ "skuSaleDaysEnd" : "",
1542
+ "planUrgentCountBegin" : "",
1543
+ "planUrgentCountEnd" : "",
1544
+ "skcAvailableOrderBegin": "",
1545
+ "skcAvailableOrderEnd" : "",
1546
+ "skuAvailableOrderBegin": "",
1547
+ "skuAvailableOrderEnd" : "",
1548
+ "shelfDateBegin" : shelf_date_begin,
1549
+ "shelfDateEnd" : shelf_date_end,
1550
+ "stockWarnStatusList" : [],
1551
+ "labelFakeIdList" : [],
1552
+ "sheinSaleByInventory" : "",
1553
+ "tspIdList" : [],
1554
+ "adviceStatus" : [],
1555
+ "sortBy7dSaleCnt" : 2
463
1556
  }
464
1557
  response_text = fetch(self.web_page, url, payload)
465
1558
  error_code = response_text.get('code')
466
1559
  if str(error_code) != '0':
467
1560
  raise send_exception(json.dumps(response_text, ensure_ascii=False))
468
1561
 
469
- list_item = response_text['info']['data']
470
- total = response_text['info']['meta']['count']
471
- totalPage = math.ceil(total / page_size)
1562
+ spu_list = response_text['info']['list']
472
1563
 
1564
+ # skc_list = [item['skc'] for item in spu_list]
1565
+ # self.get_activity_label(skc_list)
1566
+ # self.get_preemption_list(skc_list)
1567
+ # self.get_sku_price_v2(skc_list)
1568
+ # self.get_stock_advice(skc_list)
1569
+
1570
+ total = response_text['info']['count']
1571
+ totalPage = math.ceil(total / pageSize)
473
1572
  for page in range(2, totalPage + 1):
474
- log(f'获取不扣款列表 第{page}/{totalPage}页')
475
- page_num = page
476
- payload = {
477
- "addTimeStart" : f"{first_day} 00:00:00",
478
- "addTimeEnd" : f"{last_day} 23:59:59",
479
- "returnOrderStatusList": [4],
480
- "page" : page_num,
481
- "perPage" : page_size
482
- }
1573
+ log(f'获取备货信息商品列表 第{page}/{totalPage}页')
1574
+ payload['pageNumber'] = page
483
1575
  response_text = fetch(self.web_page, url, payload)
484
- spu_list_new = response_text['info']['data']
485
- list_item += spu_list_new
486
- time.sleep(0.1)
487
1576
 
488
- write_dict_to_file(cache_file, list_item)
1577
+ new_spu_list = response_text['info']['list']
1578
+ spu_list += new_spu_list
489
1579
 
490
- return list_item
1580
+ # skc_list = [item['skc'] for item in new_spu_list]
1581
+ # self.get_activity_label(skc_list)
1582
+ # self.get_preemption_list(skc_list)
1583
+ # self.get_sku_price_v2(skc_list)
1584
+ # self.get_stock_advice(skc_list)
491
1585
 
492
- def get_comment_list(self):
493
- cache_file = f'{self.config.auto_dir}/shein/dict/comment_list_{TimeUtils.today_date()}.json'
494
- comment_list = read_dict_from_file_ex(cache_file, self.store_username, 3600)
495
- if len(comment_list) > 0:
496
- return comment_list
1586
+ time.sleep(0.3)
497
1587
 
498
- page_num = 1
499
- page_size = 50
1588
+ # key = f'{self.store_username}'
1589
+ # cache_file = f'{self.config.auto_dir}/shein/cache/bak_info_list_{key}_{shelf_date_begin}_{shelf_date_end}.json'
1590
+ # write_dict_to_file_ex(cache_file, {key: spu_list}, [key])
500
1591
 
501
- yesterday = TimeUtils.get_yesterday()
1592
+ for skc_item in spu_list:
1593
+ skc = skc_item['skc']
1594
+ shelfDate = skc_item['shelfDate']
1595
+ dict_skc_shelf_date[skc] = shelfDate
502
1596
 
503
- url = f"https://sso.geiwohuo.com/gsp/goods/comment/list"
1597
+ cache_file = f'{self.config.auto_dir}/shein/dict/skc_shelf_date_{self.store_username}.json'
1598
+ dict = read_dict_from_file(cache_file)
1599
+ dict.update(dict_skc_shelf_date)
1600
+ write_dict_to_file(cache_file, dict)
1601
+
1602
+ return spu_list
1603
+
1604
+ # 获取备货信息列表
1605
+ def get_bak_base_info(self):
1606
+ log(f'获取备货信息列表 {self.store_name} {self.store_username}')
1607
+ url = "https://sso.geiwohuo.com/idms/goods-skc/list"
1608
+ pageNumber = 1
1609
+ pageSize = 100
504
1610
  payload = {
505
- "page" : page_num,
506
- "perPage" : page_size,
507
- "startCommentTime": f"{yesterday} 00:00:00",
508
- "commentEndTime" : f"{yesterday} 23:59:59",
509
- "commentStarList" : ["3", "2", "1"]
1611
+ "pageNumber" : pageNumber,
1612
+ "pageSize" : pageSize,
1613
+ "supplierCodes" : "",
1614
+ "skcs" : "",
1615
+ "spu" : "",
1616
+ "c7dSaleCntBegin" : "",
1617
+ "c7dSaleCntEnd" : "",
1618
+ "goodsLevelIdList" : [],
1619
+ "supplyStatus" : "",
1620
+ "shelfStatus" : "",
1621
+ "categoryIdList" : [],
1622
+ "skcStockBegin" : "",
1623
+ "skcStockEnd" : "",
1624
+ "skuStockBegin" : "",
1625
+ "skuStockEnd" : "",
1626
+ "skcSaleDaysBegin" : "",
1627
+ "skcSaleDaysEnd" : "",
1628
+ "skuSaleDaysBegin" : "",
1629
+ "skuSaleDaysEnd" : "",
1630
+ "planUrgentCountBegin" : "",
1631
+ "planUrgentCountEnd" : "",
1632
+ "skcAvailableOrderBegin": "",
1633
+ "skcAvailableOrderEnd" : "",
1634
+ "skuAvailableOrderBegin": "",
1635
+ "skuAvailableOrderEnd" : "",
1636
+ "shelfDateBegin" : "",
1637
+ "shelfDateEnd" : "",
1638
+ "stockWarnStatusList" : [],
1639
+ "labelFakeIdList" : [],
1640
+ "sheinSaleByInventory" : "",
1641
+ "tspIdList" : [],
1642
+ "adviceStatus" : [],
1643
+ "sortBy7dSaleCnt" : 2
510
1644
  }
511
1645
  response_text = fetch(self.web_page, url, payload)
512
1646
  error_code = response_text.get('code')
513
1647
  if str(error_code) != '0':
514
1648
  raise send_exception(json.dumps(response_text, ensure_ascii=False))
515
1649
 
516
- comment_list = response_text['info']['data']
517
- total = response_text['info']['meta']['count']
518
- totalPage = math.ceil(total / page_size)
1650
+ spu_list = response_text['info']['list']
519
1651
 
1652
+ skc_list = [item['skc'] for item in spu_list]
1653
+ self.get_activity_label(skc_list)
1654
+ self.get_quality_label(skc_list)
1655
+ self.get_preemption_list(skc_list)
1656
+ self.get_sku_price_v2(skc_list)
1657
+ self.get_stock_advice(skc_list)
1658
+
1659
+ total = response_text['info']['count']
1660
+ totalPage = math.ceil(total / pageSize)
520
1661
  for page in range(2, totalPage + 1):
521
- log(f'获取评价列表 第{page}/{totalPage}页')
522
- page_num = page
523
- payload['page'] = page_num
1662
+ log(f'获取备货信息商品列表 第{page}/{totalPage}页')
1663
+ payload['pageNumber'] = page
524
1664
  response_text = fetch(self.web_page, url, payload)
525
- comment_list = response_text['info']['data']
526
- time.sleep(0.1)
527
1665
 
528
- write_dict_to_file_ex(cache_file, {self.store_username: comment_list}, [self.store_username])
529
- return comment_list
1666
+ new_spu_list = response_text['info']['list']
1667
+ spu_list += new_spu_list
530
1668
 
531
- def get_last_month_outbound_amount(self):
532
- url = "https://sso.geiwohuo.com/mils/report/month/list"
533
- start, end = TimeUtils.get_current_year_range()
1669
+ skc_list = [item['skc'] for item in new_spu_list]
1670
+ self.get_activity_label(skc_list)
1671
+ self.get_quality_label(skc_list)
1672
+ self.get_preemption_list(skc_list)
1673
+ self.get_sku_price_v2(skc_list)
1674
+ self.get_stock_advice(skc_list)
1675
+
1676
+ time.sleep(0.3)
1677
+
1678
+ key = f'{self.store_username}'
1679
+ cache_file = f'{self.config.auto_dir}/shein/cache/bak_info_list_{key}.json'
1680
+ write_dict_to_file_ex(cache_file, {key: spu_list}, [key])
1681
+
1682
+ return spu_list
1683
+
1684
+ def get_skc_week_sale_list(self, spu, skc, start_from=None):
1685
+ dict_skc = self.get_dict_skc_week_trend_v2(spu, skc, start_from)
1686
+ date_list = TimeUtils.get_past_7_days_list(start_from)
1687
+ saleCnt7d = 0
1688
+ sales_detail = []
1689
+ for date in date_list:
1690
+ saleCnt = get_safe_value(dict_skc.get(date, {}), 'saleCnt', 0)
1691
+ epsUvIdx = get_safe_value(dict_skc.get(date, {}), 'epsUvIdx', 0)
1692
+
1693
+ saleCnt7d += saleCnt
1694
+ sales_detail.append(f'{date}({TimeUtils.get_weekday_name(date)}): {saleCnt}/{epsUvIdx}')
1695
+
1696
+ sales_data = []
1697
+ for date in date_list:
1698
+ goodsUvIdx = get_safe_value(dict_skc.get(date, {}), 'goodsUvIdx', 0) # 商详访客
1699
+ epsGdsCtrIdx = get_safe_value(dict_skc.get(date, {}), 'epsGdsCtrIdx', 0) # 点击率
1700
+
1701
+ payUvIdx = get_safe_value(dict_skc.get(date, {}), 'payUvIdx', 0) # 支付人数
1702
+ gdsPayCtrIdx = get_safe_value(dict_skc.get(date, {}), 'gdsPayCtrIdx', 0) # 转化率
1703
+
1704
+ sales_data.append(f'{date}({TimeUtils.get_weekday_name(date)}): {epsGdsCtrIdx:.2%}({goodsUvIdx})/{gdsPayCtrIdx:.2%}({payUvIdx})')
1705
+
1706
+ return sales_detail, sales_data, saleCnt7d
1707
+
1708
+ def stat_new_product_to_bak(self):
1709
+ # 直接调用 get_skc_week_actual_sales 來获取周销 计算是否能转成备货款
1710
+ skc_list = self.get_bak_base_info() # 这个地方 不要加已上架和正常供货参数 直接取所有的skc列表
1711
+ # 以算昨日7.2日为例 上架天数为29天(转换成上架日期),且过去7天销量达到类目备货标准和没有达到备货标准的skc数量
1712
+ # 1.计算某个skc的上架日期
1713
+ # 2.计算某个skc的基于某个日期的过去7天销量
1714
+ # 3.获取叶子类目的备货标准
1715
+ header = ['店铺账号', '店铺名称', '店长', '统计日期', 'SKC图片', '商品信息', '新品成功转备货款', '第4周SKC销量/SKC曝光', '第4周SKC点击率/SKC转化率', 'SKC', 'SPU']
1716
+ excel_data = []
1717
+ stat_date_list = TimeUtils.get_dates_from_first_of_month_to_yesterday()
1718
+ for stat_date in stat_date_list:
1719
+ # 计算 stat_date 这天 的上架日期 是 filter_shelf_date
1720
+ filter_shelf_date = TimeUtils.get_past_nth_day(29, stat_date)
1721
+ log(f'stat_date:{stat_date},filter_shelf_date:{filter_shelf_date}')
1722
+ # 筛选 上架日期是 filter_shelf_date 这天的skc有哪些
1723
+ filter_skc_list = [skc_item for skc_item in skc_list if skc_item['shelfDate'] == filter_shelf_date]
1724
+ # 再统计 这些skc 在 stat_date 这天的 前7天销量
1725
+ # 看看这个7天销量是否达到了类目的备货标准 统计 计数
1726
+ for skc_item in filter_skc_list:
1727
+ skc = skc_item['skc']
1728
+ spu = skc_item['spu']
1729
+ log(f'skc:{skc}, spu:{spu}')
1730
+
1731
+ row_item = []
1732
+ row_item.append(self.store_username)
1733
+
1734
+ status_cn = skc_item.get('shelfStatus').get('name')
1735
+ goods_level = skc_item['goodsLevel']['name']
1736
+ goods_label = [label["name"] for label in skc_item['goodsLabelList']]
1737
+ store_info = f'{self.store_name}\n({status_cn})\n{goods_level}\n{",".join(goods_label).strip()}\n{stat_date_list[-1]}\n{stat_date_list[0]}'
1738
+ row_item.append(store_info)
1739
+ store_manager = self.config.shein_store_manager.get(str(self.store_username).lower())
1740
+ row_item.append(store_manager)
1741
+ row_item.append(stat_date)
1742
+ row_item.append(skc_item['picUrl'])
1743
+
1744
+ standard_value = (skc_item.get('stockStandard') or {}).get('value') or 0
1745
+
1746
+ sale_detail, sale_rate, sale_num = self.get_skc_week_sale_list(spu, skc, stat_date)
1747
+ success = int(standard_value > 0 and sale_num >= standard_value)
1748
+
1749
+ categoryName = skc_item['categoryName']
1750
+ shelfDate = skc_item['shelfDate']
1751
+ product_info = (
1752
+ f'SPU: {spu}\n'
1753
+ f'SKC: {skc}\n'
1754
+ f'上架日期: {shelfDate}\n'
1755
+ f'类目: {categoryName}\n'
1756
+ f'备货标准/第4周销: {standard_value}/{sale_num}\n'
1757
+ )
1758
+ row_item.append(product_info)
1759
+ row_item.append(success)
1760
+ row_item.append("\n".join(sale_detail))
1761
+ row_item.append("\n".join(sale_rate))
1762
+ row_item.append(skc)
1763
+ row_item.append(spu)
1764
+ excel_data.append(row_item)
1765
+
1766
+ cache_file = f'{self.config.auto_dir}/shein/dict/new_product_to_bak_{TimeUtils.today_date()}.json'
1767
+ write_dict_to_file_ex(cache_file, {self.store_username: [header] + excel_data}, [self.store_username])
1768
+
1769
+ def get_funds_data_lz(self):
1770
+ log(f'正在获取 {self.store_name} 财务数据')
1771
+ url = "https://sso.geiwohuo.com/sso/homePage/dataOverview/v2/detail"
534
1772
  payload = {
535
- "reportDateStart": start, "reportDateEnd": end, "pageNumber": 1, "pageSize": 50
1773
+ "metaIndexIds": [
1774
+ 298,
1775
+ 67,
1776
+ 70,
1777
+ 72
1778
+ ],
536
1779
  }
537
1780
  response_text = fetch(self.web_page, url, payload)
538
1781
  error_code = response_text.get('code')
539
1782
  if str(error_code) != '0':
540
1783
  raise send_exception(json.dumps(response_text, ensure_ascii=False))
541
1784
  info = response_text.get('info')
542
- lst = info.get('data', {}).get('list', [])
543
- if not lst:
544
- log(f'⚠️ {self.store_name} 最近一个月无出库记录,金额为0')
545
- return 0
1785
+ num298 = 0 # 在途商品金额
1786
+ num67 = 0 # 在仓商品金额
1787
+ num70 = 0 # 待结算金额
1788
+ num72 = 0 # 可提现金额
1789
+ for item in info['list']:
1790
+ if item['metaIndexId'] == 298:
1791
+ num298 = item['count']
1792
+ if item['metaIndexId'] == 67:
1793
+ num67 = item['count']
1794
+ if item['metaIndexId'] == 70:
1795
+ num70 = item['count']
1796
+ if item['metaIndexId'] == 72:
1797
+ num72 = item['count']
546
1798
 
547
- last_item = lst[-1]
548
- log(f'正在获取 {self.store_name} 最近一个月出库金额: {last_item["totalCustomerAmount"]}')
549
- return last_item['totalCustomerAmount']
1799
+ outAmount = self.get_last_month_outbound_amount()
1800
+ store_manager = self.config.shein_store_manager.get(str(self.store_username).lower())
1801
+ NotifyItem = [f'{self.store_name}', self.store_username, store_manager, num298, num67, num70, num72, '', outAmount, '', '', '', TimeUtils.current_datetime()]
1802
+ log(NotifyItem)
1803
+ cache_file = f'{self.config.auto_dir}/shein/cache/stat_fund_lz_{TimeUtils.today_date()}.json'
1804
+ write_dict_to_file_ex(cache_file, {self.store_username: NotifyItem}, [self.store_username])
1805
+ return NotifyItem
550
1806
 
551
1807
  def get_funds_data(self):
552
1808
  log(f'正在获取 {self.store_name} 财务数据')
@@ -899,7 +2155,6 @@ class SheinLib:
899
2155
  return count
900
2156
 
901
2157
  def get_week_sales_stat_detail(self):
902
- global ListNotify, NotifyItem
903
2158
  dt = self.get_dt_time()
904
2159
  yesterday = TimeUtils.get_yesterday(dt)
905
2160
  date_7_days_ago = TimeUtils.get_past_nth_day(6, None, '%Y-%m-%d')
@@ -929,15 +2184,23 @@ class SheinLib:
929
2184
  last_item = SheinStoreSalesDetailManager(self.config.database_url).get_records_as_dict(self.store_username, yesterday)
930
2185
  log('last_item', last_item)
931
2186
  day_item = info[-1]
2187
+ log(day_item)
932
2188
  item = {}
933
2189
  item["store_username"] = self.store_username
934
2190
  item["store_name"] = self.store_name
935
2191
  item["day"] = day_item["dataDate"]
936
- item["sales_num"] = day_item["saleCnt1d"]
2192
+ item["sales_num"] = day_item["saleCnt1d"] or 0
937
2193
  item['sales_num_inc'] = item['sales_num'] - last_item.get('sales_num', 0)
938
- item['sales_amount'] = day_item['dealAmt1d']
2194
+
2195
+ if int(self.user_info.get('lv1CategoryId')) == 216506: # 自运营POP店
2196
+ log('gmv1d', day_item['gmv1d'])
2197
+ item['sales_amount'] = day_item['gmv1d'] if isinstance(day_item['gmv1d'], (int, float)) else 0
2198
+ else:
2199
+ item['sales_amount'] = day_item['dealAmt1d'] or 0
2200
+
2201
+ log('sales_amount', item['sales_amount'])
939
2202
  item['sales_amount_inc'] = item['sales_amount'] - float(last_item.get('sales_amount', 0))
940
- item['visitor_num'] = day_item['idxShopGoodsUv1d']
2203
+ item['visitor_num'] = day_item['idxShopGoodsUv1d'] or 0
941
2204
  item['visitor_num_inc'] = item['visitor_num'] - last_item.get('visitor_num', 0)
942
2205
  item['bak_A_num'] = self.get_product_bak_A_count()
943
2206
  item['bak_A_num_inc'] = item['bak_A_num'] - last_item.get('bak_A_num', 0)
@@ -1076,7 +2339,7 @@ class SheinLib:
1076
2339
  item.append(stock_str)
1077
2340
  item.append(cost_price)
1078
2341
  item.append(supplyPrice)
1079
- sale_num_list, sale_data_list = self.get_skc_week_sale_list(spu, skc, sku)
2342
+ sale_num_list, sale_data_list = self.get_sku_week_sale_list(spu, skc, sku)
1080
2343
  item.append("\n".join(sale_num_list))
1081
2344
  item.append("\n".join(sale_data_list))
1082
2345
  item.append(self.get_skc_activity_label(skc, sku, dictActivityPrice))
@@ -1287,7 +2550,7 @@ class SheinLib:
1287
2550
 
1288
2551
  cache_file2 = f'{self.config.auto_dir}/shein/dict/skc_shelf_{self.store_username}.json'
1289
2552
  write_dict_to_file(cache_file2, DictSkcShelf)
1290
- cache_file3 = f'{self.config.auto_dir}/dict/skc_product_{self.store_username}.json'
2553
+ cache_file3 = f'{self.config.auto_dir}/shein/dict/skc_product_{self.store_username}.json'
1291
2554
  write_dict_to_file(cache_file3, DictSkcProduct)
1292
2555
 
1293
2556
  write_dict_to_file(cache_file, DictSpuInfo)
@@ -1366,6 +2629,33 @@ class SheinLib:
1366
2629
  list_item += response_text['info']['data']
1367
2630
  time.sleep(0.1)
1368
2631
 
2632
+ log(list_item)
2633
+ write_dict_to_file(cache_file, list_item)
2634
+ return list_item
2635
+
2636
+ def get_partake_activity_detail(self, activity_id, skc):
2637
+ log(f'正在获取营销活动报名记录详情 {self.store_name}')
2638
+ page_num = 1
2639
+ page_size = 100
2640
+ cache_file = f'{self.config.auto_dir}/shein/cache/platform_activity_{activity_id}_{skc}.json'
2641
+ list_item = read_dict_from_file(cache_file)
2642
+ if len(list_item) > 0:
2643
+ return list_item
2644
+
2645
+ url = f"https://sso.geiwohuo.com/mrs-api-prefix/mbrs/activity/get_partake_activity_goods_list?page_num={page_num}&page_size={page_size}"
2646
+ payload = {
2647
+ "goods_audit_status": 1,
2648
+ "activity_id_list" : [activity_id],
2649
+ "skc_list" : [skc]
2650
+ }
2651
+
2652
+ response_text = fetch(self.web_page, url, payload)
2653
+ error_code = response_text.get('code')
2654
+ if str(error_code) != '0':
2655
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
2656
+ list_item = response_text['info']['data']
2657
+
2658
+ log(list_item)
1369
2659
  write_dict_to_file(cache_file, list_item)
1370
2660
  return list_item
1371
2661
 
@@ -1446,6 +2736,36 @@ class SheinLib:
1446
2736
 
1447
2737
  write_dict_to_file(cache_file, dict_activity_price)
1448
2738
 
2739
+ def get_skc_actual_sales_dict(self, skc, first_day, last_day):
2740
+ cache_file = f'{self.config.auto_dir}/shein/cache/actual_sales_{skc}_{first_day}_{last_day}.json'
2741
+ if datetime.now().hour >= 9:
2742
+ DictSkuSales = read_dict_from_file(cache_file)
2743
+ else:
2744
+ DictSkuSales = read_dict_from_file(cache_file, 1800)
2745
+ if len(DictSkuSales) > 0:
2746
+ return DictSkuSales
2747
+
2748
+ url = f"https://sso.geiwohuo.com/idms/sale-trend/detail"
2749
+ payload = {
2750
+ "skc" : skc,
2751
+ "startDate": first_day,
2752
+ "endDate" : last_day,
2753
+ "daysToAdd": 0
2754
+ }
2755
+ response_text = fetch(self.web_page, url, payload)
2756
+ error_code = response_text.get('code')
2757
+ if str(error_code) != '0':
2758
+ log(response_text)
2759
+ return {}
2760
+ info = response_text['info']
2761
+ for sale_item in info['actualSalesVolumeMap']:
2762
+ sku = sale_item['skuCode']
2763
+ if sku is not None:
2764
+ DictSkuSales[sku] = sale_item['actualSalesVolume']
2765
+
2766
+ write_dict_to_file(cache_file, DictSkuSales)
2767
+ return DictSkuSales
2768
+
1449
2769
  def get_skc_week_actual_sales(self, skc):
1450
2770
  first_day, last_day = TimeUtils.get_past_7_days_range()
1451
2771
  cache_file = f'{self.config.auto_dir}/shein/cache/{skc}_{first_day}_{last_day}.json'
@@ -1498,6 +2818,22 @@ class SheinLib:
1498
2818
 
1499
2819
  return dict
1500
2820
 
2821
+ def get_quality_label(self, skc_list):
2822
+ url = f"https://sso.geiwohuo.com/idms/goods-skc/quality-label"
2823
+ payload = skc_list
2824
+ response_text = fetch(self.web_page, url, payload)
2825
+ error_code = response_text.get('code')
2826
+ if str(error_code) != '0':
2827
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
2828
+ dict = response_text['info']
2829
+
2830
+ cache_file = f'{self.config.auto_dir}/shein/quality_label/quality_label_{self.store_username}.json'
2831
+ dict_label = read_dict_from_file(cache_file)
2832
+ dict_label.update(dict)
2833
+ write_dict_to_file(cache_file, dict_label)
2834
+
2835
+ return dict
2836
+
1501
2837
  def get_activity_label(self, skc_list):
1502
2838
  url = f"https://sso.geiwohuo.com/idms/goods-skc/activity-label"
1503
2839
  payload = skc_list
@@ -1514,6 +2850,23 @@ class SheinLib:
1514
2850
 
1515
2851
  return dict
1516
2852
 
2853
+ def get_sku_price_pop(self, spu):
2854
+ pass
2855
+ log(f'获取pop sku价格列表', spu)
2856
+ info = self.get_product_detail(spu)
2857
+
2858
+ dict_sku_price_new = {}
2859
+ for skc_item in info['skc_list']:
2860
+ for sku_item in skc_item['sku_list']:
2861
+ sku = sku_item['sku_code']
2862
+ special_price = sku_item['price_info_list'][0]['special_price']
2863
+ dict_sku_price_new[sku] = special_price
2864
+
2865
+ cache_file = f'{self.config.auto_dir}/shein/sku_price/sku_price_{self.store_username}.json'
2866
+ dict_sku_price = read_dict_from_file(cache_file)
2867
+ dict_sku_price.update(dict_sku_price_new)
2868
+ write_dict_to_file(cache_file, dict_sku_price)
2869
+
1517
2870
  def get_sku_price_v2(self, skc_list):
1518
2871
  log(f'获取sku价格列表', skc_list)
1519
2872
  url = "https://sso.geiwohuo.com/idms/goods-skc/price"
@@ -1565,6 +2918,24 @@ class SheinLib:
1565
2918
  log(f'dt: {self.dt}')
1566
2919
  return self.dt
1567
2920
 
2921
+ def get_dt_time_goods(self):
2922
+ if self.dt_goods is not None:
2923
+ log(f'字典dt_goods: {self.dt_goods}')
2924
+ return self.dt_goods
2925
+ log('获取非实时更新时间')
2926
+ url = "https://sso.geiwohuo.com/sbn/common/get_update_time"
2927
+ payload = {
2928
+ "pageCode": "GoodsPreviewNew",
2929
+ "areaCd" : "cn"
2930
+ }
2931
+ response_text = fetch(self.web_page, url, payload)
2932
+ error_code = response_text.get('code')
2933
+ if str(error_code) != '0':
2934
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
2935
+ self.dt_goods = response_text.get('info').get('dt')
2936
+ log(f'接口dt_goods: {self.dt_goods}')
2937
+ return self.dt_goods
2938
+
1568
2939
  def get_dict_skc_week_trend(self):
1569
2940
  page_num = 1
1570
2941
  page_size = 100
@@ -1765,6 +3136,7 @@ class SheinLib:
1765
3136
  skc = str(spu_info['skc'])
1766
3137
  # if not shein_db.exists_sales_1_days_ago(skc):
1767
3138
  # log(f'未查到昨天销量: {skc}')
3139
+ self.get_skc_week_actual_sales(skc)
1768
3140
  self.get_skc_sales(skc, date_60_days_ago, date_1_days_ago)
1769
3141
  skcCode = spu_info['supplierCode']
1770
3142
  product_name = DictSpuInfo[spu]['product_name_en']
@@ -1874,7 +3246,7 @@ class SheinLib:
1874
3246
  sku_item.append(skc) # SKC
1875
3247
  sku_item.append(spu_info['supplierCode']) # SKC货号
1876
3248
 
1877
- sale_num_list, sale_data_list = self.get_skc_week_sale_list(spu, skc, sku)
3249
+ sale_num_list, sale_data_list = self.get_sku_week_sale_list(spu, skc, sku)
1878
3250
  sku_item.append("\n".join(sale_num_list))
1879
3251
  sku_item.append("\n".join(sale_data_list))
1880
3252
  sku_item.append(self.get_skc_activity_label(skc, sku, dictActivityPrice))
@@ -1885,7 +3257,7 @@ class SheinLib:
1885
3257
  # SKC趋势数据
1886
3258
  sku_item.append(skc_trend.get('saleCnt', 0)) # SKC近7天销量
1887
3259
  sku_item.append(skc_trend.get('epsUvIdx', 0)) # SKC近7天曝光人数
1888
- sku_item.append(skc_trend.get('goodsUvIdx', 0)) # SKC近7天商详访客
3260
+ sku_item.append(skc_trend.get('goodsUv', 0)) # SKC近7天商详访客
1889
3261
  sku_item.append(skc_trend.get('epsGdsCtrIdx', 0)) # SKC近7天点击率
1890
3262
  sku_item.append(skc_trend.get('payUvIdx', 0)) # SKC近7天支付人数
1891
3263
  sku_item.append(skc_trend.get('gdsPayCtrIdx', 0)) # SKC近7天支付率
@@ -1898,14 +3270,63 @@ class SheinLib:
1898
3270
 
1899
3271
  return product_sku_list
1900
3272
 
3273
+ # 获取一个skc一段时间内的销售趋势(商品明细中的)
3274
+ def get_skc_trend(self, spu, skc, start_date, end_date):
3275
+ dt = self.get_dt_time_goods()
3276
+
3277
+ # 将字符串转换为日期对象
3278
+ date1 = datetime.strptime(end_date, "%Y-%m-%d").date()
3279
+ date2 = datetime.strptime(dt, "%Y%m%d").date()
3280
+ if date1 > date2:
3281
+ log(f'get_skc_trend: dt:{dt} < end_date: {end_date}')
3282
+
3283
+ cache_file = f'{self.config.auto_dir}/shein/dict/skc_trend_{skc}_{start_date}_{end_date}.json'
3284
+ DictSkc = read_dict_from_file(cache_file)
3285
+ if len(DictSkc) > 0:
3286
+ return DictSkc
3287
+
3288
+ url = f"https://sso.geiwohuo.com/sbn/new_goods/get_skc_diagnose_trend"
3289
+ payload = {
3290
+ "areaCd" : "cn",
3291
+ "countrySite": [
3292
+ "shein-all"
3293
+ ],
3294
+ "dt" : dt,
3295
+ "endDate" : end_date.replace('-', ''),
3296
+ "spu" : [spu],
3297
+ "skc" : [skc],
3298
+ "startDate" : start_date.replace('-', ''),
3299
+ }
3300
+ response_text = fetch(self.web_page, url, payload)
3301
+ log(response_text)
3302
+ error_code = response_text.get('code')
3303
+ if str(error_code) != '0':
3304
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
3305
+
3306
+ data_list = response_text['info']
3307
+ DictSkc = {}
3308
+ for date_item in data_list:
3309
+ dataDate = date_item['dataDate']
3310
+ # epsUvIdx = date_item['epsUvIdx']
3311
+ # saleCnt = date_item['saleCnt']
3312
+ DictSkc[dataDate] = date_item
3313
+
3314
+ log('len(DictSkc)', len(DictSkc))
3315
+ write_dict_to_file(cache_file, DictSkc)
3316
+ return DictSkc
3317
+
1901
3318
  # 获取一个skc一周内的销售趋势(商品明细中的)
1902
- def get_dict_skc_week_trend_v2(self, spu, skc):
3319
+ def get_dict_skc_week_trend_v2(self, spu, skc, start_from=None):
1903
3320
  dt = self.get_dt_time()
1904
3321
 
1905
- date_7_days_ago = TimeUtils.get_past_nth_day(7, None, '%Y%m%d')
1906
- log('-7', date_7_days_ago)
1907
- date_1_days_ago = TimeUtils.get_past_nth_day(1, None, '%Y%m%d')
1908
- log('-1', date_1_days_ago)
3322
+ date_7_days_ago, date_1_days_ago = TimeUtils.get_past_7_days_range_format(start_from, '%Y%m%d')
3323
+ log(date_7_days_ago, date_1_days_ago, 'dt', dt)
3324
+
3325
+ # 将字符串转换为日期对象
3326
+ date1 = datetime.strptime(date_1_days_ago, "%Y%m%d").date()
3327
+ date2 = datetime.strptime(dt, "%Y%m%d").date()
3328
+ if date1 > date2:
3329
+ send_exception(f'get_dict_skc_week_trend_v2: dt:{dt} < date_1_days_ago: {date_1_days_ago}')
1909
3330
 
1910
3331
  cache_file = f'{self.config.auto_dir}/shein/dict/dict_skc_week_trend_{skc}_{date_7_days_ago}_{date_1_days_ago}.json'
1911
3332
  if datetime.now().hour >= 9:
@@ -1944,7 +3365,7 @@ class SheinLib:
1944
3365
  write_dict_to_file(cache_file, DictSkc)
1945
3366
  return DictSkc
1946
3367
 
1947
- def get_skc_week_sale_list(self, spu, skc, sku):
3368
+ def get_sku_week_sale_list(self, spu, skc, sku):
1948
3369
  dict_skc = self.get_dict_skc_week_trend_v2(spu, skc)
1949
3370
  date_list = TimeUtils.get_past_7_days_list()
1950
3371
  first_day, last_day = TimeUtils.get_past_7_days_range()
@@ -2067,6 +3488,10 @@ class SheinLib:
2067
3488
  raise send_exception(json.dumps(response_text, ensure_ascii=False))
2068
3489
 
2069
3490
  spu_list = response_text['info']['list']
3491
+ # if int(self.user_info.get('lv1CategoryId')) == 216506: # 自运营POP店
3492
+ # for spu_item in spu_list:
3493
+ # spu = spu_item.get('spu')
3494
+ # self.get_sku_price_pop(spu)
2070
3495
 
2071
3496
  skc_list = [item['skc'] for item in spu_list]
2072
3497
  self.get_activity_label(skc_list)
@@ -2083,6 +3508,11 @@ class SheinLib:
2083
3508
  response_text = fetch(self.web_page, url, payload)
2084
3509
  spu_list_new = response_text['info']['list']
2085
3510
 
3511
+ # if int(self.user_info.get('lv1CategoryId')) == 216506: # 自运营POP店
3512
+ # for spu_item in spu_list:
3513
+ # spu = spu_item.get('spu')
3514
+ # self.get_product_detail(spu)
3515
+
2086
3516
  skc_list = [item['skc'] for item in spu_list_new]
2087
3517
  self.get_activity_label(skc_list)
2088
3518
  self.get_preemption_list(skc_list)
@@ -2317,7 +3747,7 @@ class SheinLib:
2317
3747
  if mode in [6] and sales7cn > 0:
2318
3748
  continue
2319
3749
 
2320
- sale_num_list, sale_data_list = self.get_skc_week_sale_list(spu, skc, sku)
3750
+ sale_num_list, sale_data_list = self.get_sku_week_sale_list(spu, skc, sku)
2321
3751
  row_item.append("\n".join(sale_num_list))
2322
3752
  row_item.append("\n".join(sale_data_list))
2323
3753
  row_item.append(self.get_skc_activity_label(skc, sku, dictActivityPrice))
@@ -2333,3 +3763,763 @@ class SheinLib:
2333
3763
  write_dict_to_file_ex(cache_file, {self.store_name: NotifyItem}, {self.store_name})
2334
3764
 
2335
3765
  return excel_data
3766
+
3767
+ def check_order_list(self, source, first_day, last_day):
3768
+ page_num = 1
3769
+ page_size = 200 # 列表最多返回200条数据 大了没有用
3770
+
3771
+ cache_file = f'{self.config.auto_dir}/shein/cache/check_order_{first_day}_{last_day}.json'
3772
+ list_item_cache = read_dict_from_file_ex(cache_file, self.store_username)
3773
+
3774
+ url = f"https://sso.geiwohuo.com/gsfs/finance/reportOrder/dualMode/checkOrderList/item/union"
3775
+ payload = {
3776
+ "page" : page_num,
3777
+ "perPage" : page_size,
3778
+ "detailAddTimeStart": f"{first_day} 00:00:00",
3779
+ "detailAddTimeEnd" : f"{last_day} 23:59:59"
3780
+ }
3781
+ response_text = fetch(self.web_page, url, payload)
3782
+ error_code = response_text.get('code')
3783
+ if str(error_code) != '0':
3784
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
3785
+ list_item = response_text['info']['data']
3786
+ total = response_text['info']['meta']['count']
3787
+ totalPage = math.ceil(total / page_size)
3788
+
3789
+ log(self.store_name, self.store_username, total, len(list_item_cache))
3790
+ if int(total) == len(list_item_cache):
3791
+ log('总数与缓存数量相同 跳过剩余页抓取', total)
3792
+ return list_item_cache
3793
+
3794
+ for page in range(2, totalPage + 1):
3795
+ log(f'获取收支明细列表 第{page}/{totalPage}页')
3796
+ payload['page'] = page
3797
+ response_text = fetch(self.web_page, url, payload)
3798
+ spu_list_new = response_text['info']['data']
3799
+ list_item += spu_list_new
3800
+ time.sleep(0.1)
3801
+
3802
+ for item in list_item:
3803
+ supplierSku = item['skuSn']
3804
+ item['cost_price'] = self.bridge.get_sku_cost(supplierSku, source)
3805
+ item['sku_img'] = self.bridge.get_sku_img(supplierSku, source)
3806
+
3807
+ write_dict_to_file_ex(cache_file, {self.store_username: list_item}, [self.store_username])
3808
+ return list_item
3809
+
3810
+ def get_ab_test_list(self, status=4, test_type=2):
3811
+ """
3812
+ 获取AB测试列表
3813
+
3814
+ Args:
3815
+ status: 测试状态,可选值:
3816
+ 4: 进行中
3817
+ test_type: 测试类型,可选值:
3818
+ 2: skc测试
3819
+
3820
+ Returns:
3821
+ list: AB测试列表
3822
+ """
3823
+ log(f'获取AB测试列表: status={status}, test_type={test_type}')
3824
+
3825
+ # 构建缓存文件名
3826
+ cache_key = f'{test_type}_{status}'
3827
+ cache_file = f'{self.config.auto_dir}/shein/cache/ab_test_list_{self.store_username}_{cache_key}.json'
3828
+ ab_test_list = read_dict_from_file_ex(cache_file, self.store_username, 3600 * 12)
3829
+ if len(ab_test_list) > 0:
3830
+ log('返回缓存数据: ', len(ab_test_list))
3831
+ return ab_test_list
3832
+
3833
+ page_num = 1
3834
+ page_size = 100
3835
+
3836
+ url = f"https://sso.geiwohuo.com/spmc-api-prefix/spmp/image/ab_test/get_test_list?page_num={page_num}&page_size={page_size}"
3837
+ payload = {}
3838
+
3839
+ # 添加可选参数
3840
+ if status is not None:
3841
+ payload["status"] = status
3842
+ if test_type is not None:
3843
+ payload["test_type"] = test_type
3844
+
3845
+ log(payload)
3846
+ response_text = fetch(self.web_page, url, payload)
3847
+ error_code = response_text.get('code')
3848
+ if str(error_code) != '0':
3849
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
3850
+
3851
+ ab_test_list = response_text['info']['data']
3852
+ total = response_text['info']['meta']['count']
3853
+ totalPage = math.ceil(total / page_size)
3854
+
3855
+ for page in range(2, totalPage + 1):
3856
+ log(f'获取AB测试列表 第{page}/{totalPage}页')
3857
+ page_num = page
3858
+ url = f"https://sso.geiwohuo.com/spmc-api-prefix/spmp/image/ab_test/get_test_list?page_num={page_num}&page_size={page_size}"
3859
+ response_text = fetch(self.web_page, url, payload)
3860
+ ab_test_list += response_text['info']['data']
3861
+ time.sleep(0.1)
3862
+
3863
+ write_dict_to_file_ex(cache_file, {self.store_username: ab_test_list}, [self.store_username])
3864
+
3865
+ for test_list in ab_test_list:
3866
+ test_task_id = test_list['test_task_id']
3867
+ skc = test_list['spu_or_skc_name']
3868
+ test_list['experimental_data'] = self.get_ab_test_result(test_task_id)
3869
+ cache_file = f'{self.config.auto_dir}/shein/cache/ab_test_list_{skc}_{TimeUtils.today_date()}.json'
3870
+ write_dict_to_file(cache_file, test_list)
3871
+
3872
+ return ab_test_list
3873
+
3874
+ def get_ab_test_result(self, test_task_id):
3875
+ """
3876
+ 获取AB测试实验结果
3877
+
3878
+ Args:
3879
+ test_task_id: 测试任务ID
3880
+
3881
+ Returns:
3882
+ dict: 实验结果数据,包含:
3883
+ - control_group_data: 对照组数据
3884
+ - expose_uv: 曝光人数
3885
+ - cart_uv: 加购人数
3886
+ - other_click_uv: 其他点击人数
3887
+ - goods_uv: 商详访客
3888
+ - goods_cnt: 商品数量
3889
+ - click_rate: 点击率
3890
+ - cart_rate: 加购率
3891
+ - conversion_rate: 转化率
3892
+ - experiment_group_data: 实验组数据(字段同对照组)
3893
+ """
3894
+ log(f'获取AB测试结果: test_task_id={test_task_id}')
3895
+
3896
+ cache_file = f'{self.config.auto_dir}/shein/cache/ab_test_result_{test_task_id}.json'
3897
+ ab_test_result = read_dict_from_file(cache_file, 3600 * 12)
3898
+ if len(ab_test_result) > 0:
3899
+ log('返回缓存数据')
3900
+ return ab_test_result
3901
+
3902
+ url = f"https://sso.geiwohuo.com/spmc-api-prefix/spmp/image/ab_test/compare_experimental_data"
3903
+ payload = {
3904
+ "test_task_id": test_task_id
3905
+ }
3906
+
3907
+ response_text = fetch(self.web_page, url, payload)
3908
+ error_code = response_text.get('code')
3909
+ if str(error_code) != '0':
3910
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
3911
+
3912
+ ab_test_result = response_text['info']
3913
+
3914
+ write_dict_to_file(cache_file, ab_test_result)
3915
+
3916
+ return ab_test_result
3917
+
3918
+ def download_finance_details(self, download_dir, start_date, end_date, output_file_name=None):
3919
+ """
3920
+ 下载并处理财务收支明细文件
3921
+
3922
+ 功能说明:
3923
+ - 自动下载财务收支明细文件(可能是zip或xlsx格式)
3924
+ - 如果是zip文件(数据超过5000条),自动解压并合并多个Excel
3925
+ - 如果是xlsx文件(数据少于5000条),直接处理
3926
+ - 在Excel开头添加3列:店铺账号、店铺名称、店长
3927
+ - 自动识别需要保持为字符串的列(如业务单号等)
3928
+
3929
+ Args:
3930
+ start_date: 开始日期,格式: YYYY-MM-DD
3931
+ end_date: 结束日期,格式: YYYY-MM-DD
3932
+
3933
+ Returns:
3934
+ str: 处理后的Excel文件路径
3935
+ """
3936
+ import os
3937
+ import requests
3938
+ from datetime import datetime
3939
+ import zipfile
3940
+ import shutil
3941
+ import openpyxl
3942
+ from openpyxl import Workbook
3943
+ import pandas as pd
3944
+
3945
+ log(f'开始下载财务收支明细: {start_date} ~ {end_date}', self.store_username, self.store_name)
3946
+
3947
+ # 准备下载目录
3948
+ # download_dir = f'{self.config.auto_dir}/shein/finance_details'
3949
+ os.makedirs(download_dir, exist_ok=True)
3950
+
3951
+ # 最终输出文件路径
3952
+ if output_file_name is None:
3953
+ output_file_name = f'finance_details_{self.store_username}_{start_date}_{end_date}.xlsx'
3954
+ output_file_path = os.path.join(download_dir, output_file_name)
3955
+
3956
+ # 如果最终文件已存在,直接返回
3957
+ if os.path.exists(output_file_path):
3958
+ log(f'处理后的文件已存在,直接返回: {output_file_path}')
3959
+ return output_file_path
3960
+
3961
+ # 第一步:查询当前已有的任务列表(用于后续对比)
3962
+ log('步骤1: 查询当前已有的任务列表')
3963
+ url = "https://sso.geiwohuo.com/sso/common/fileExport/list"
3964
+ query_start_time = TimeUtils.get_past_nth_day(1, None, '%Y-%m-%d') # 查询最近1天的任务
3965
+ payload = {
3966
+ "page" : 1,
3967
+ "perPage" : 50,
3968
+ "fileStatusList" : [1], # 1-已生成
3969
+ "createTimeStart": f"{query_start_time} 00:00:00",
3970
+ "createTimeEnd" : f"{TimeUtils.today_date()} 23:59:59"
3971
+ }
3972
+
3973
+ response_text = fetch(self.web_page, url, payload)
3974
+ error_code = response_text.get('code')
3975
+ existing_task_ids = set()
3976
+ if str(error_code) == '0':
3977
+ data_list = response_text.get('info', {}).get('data', [])
3978
+ existing_task_ids = {item.get('id') for item in data_list if item.get('fileName') == '财务收支明细'}
3979
+ log(f'当前已有任务数量: {len(existing_task_ids)}')
3980
+
3981
+ # 第二步:记录当前时间并创建导出任务
3982
+ log('步骤2: 创建导出任务')
3983
+ task_create_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
3984
+ log(f'任务创建时间: {task_create_time}')
3985
+
3986
+ url = "https://sso.geiwohuo.com/gsfs/common/file/export/financeDetailsItem"
3987
+ payload = {
3988
+ "type" : 1,
3989
+ "mode" : 2,
3990
+ "detailAddTimeStart": f"{start_date} 00:00:00",
3991
+ "detailAddTimeEnd" : f"{end_date} 23:59:59"
3992
+ }
3993
+
3994
+ response_text = fetch(self.web_page, url, payload)
3995
+ error_code = response_text.get('code')
3996
+ if str(error_code) != '0':
3997
+ # 检查是否是"暂无数据可导出"的情况,这是正常情况
3998
+ if str(error_code) == 'gsfs98008':
3999
+ log('暂无数据可导出,返回None')
4000
+ return None
4001
+ raise send_exception(f'创建导出任务失败: {json.dumps(response_text, ensure_ascii=False)}')
4002
+
4003
+ log('导出任务创建成功,等待文件生成...')
4004
+
4005
+ # 第三步:轮询查询任务状态,查找新创建的任务
4006
+ log('步骤3: 轮询查询任务状态')
4007
+ task_id = None
4008
+ file_extension = None
4009
+ max_retry = 60 # 最多查询60次(10分钟)
4010
+ retry_count = 0
4011
+
4012
+ # 使用任务创建时间作为查询起始时间(向前推1分钟以避免时间误差)
4013
+ create_time_obj = datetime.strptime(task_create_time, '%Y-%m-%d %H:%M:%S')
4014
+ from datetime import timedelta
4015
+ query_time_obj = create_time_obj - timedelta(minutes=3)
4016
+ query_start_time_str = query_time_obj.strftime('%Y-%m-%d %H:%M:%S')
4017
+
4018
+ while retry_count < max_retry:
4019
+ retry_count += 1
4020
+ time.sleep(10) # 每10秒查询一次
4021
+
4022
+ log(f'第{retry_count}次查询任务状态...', self.store_username, self.store_name)
4023
+ url = "https://sso.geiwohuo.com/sso/common/fileExport/list"
4024
+ payload = {
4025
+ "page" : 1,
4026
+ "perPage" : 50,
4027
+ "fileStatusList" : [1], # 1-已生成
4028
+ "createTimeStart": query_start_time_str,
4029
+ "createTimeEnd" : f"{TimeUtils.today_date()} 23:59:59"
4030
+ }
4031
+
4032
+ log(payload)
4033
+ response_text = fetch(self.web_page, url, payload)
4034
+ log(response_text)
4035
+ error_code = response_text.get('code')
4036
+ if str(error_code) != '0':
4037
+ log(f'查询任务列表失败: {response_text}')
4038
+ continue
4039
+
4040
+ # 查找新出现的财务收支明细任务
4041
+ data_list = response_text.get('info', {}).get('data', [])
4042
+ for item in data_list:
4043
+ item_id = item.get('id')
4044
+ item_create_time = item.get('createTime')
4045
+
4046
+ log(item_id, existing_task_ids)
4047
+ # 条件:1.文件名匹配 2.状态为已生成 3.不在之前的任务列表中 4.创建时间在任务创建时间之后
4048
+ if (item.get('fileName') == '财务收支明细' and item.get('fileStatus') == 1 and item_id not in existing_task_ids):
4049
+ file_extension = item.get('fileExtension', 'xlsx') # 获取文件扩展名,默认xlsx
4050
+ log(f'找到新创建的任务: ID={item_id}, 创建时间={item_create_time}, 文件类型={file_extension}')
4051
+ task_id = item_id
4052
+ break
4053
+
4054
+ if task_id:
4055
+ log(f'任务已完成,任务ID: {task_id}')
4056
+ break
4057
+
4058
+ if not task_id:
4059
+ raise send_exception(f'导出任务超时,查询{max_retry}次后仍未完成')
4060
+
4061
+ # 第四步:获取下载地址
4062
+ log('步骤4: 获取文件下载地址')
4063
+ url = f"https://sso.geiwohuo.com/sso/common/fileExport/getFileUrl?id={task_id}"
4064
+ headers = {
4065
+ "gmpsso-language": "CN",
4066
+ "origin-url" : "https://sso.geiwohuo.com/#/download-management/list",
4067
+ "x-sso-scene" : "gmpsso"
4068
+ }
4069
+
4070
+ fetch_config = {
4071
+ "credentials" : "include",
4072
+ "referrer" : "https://sso.geiwohuo.com/",
4073
+ "referrerPolicy": "strict-origin-when-cross-origin"
4074
+ }
4075
+
4076
+ response_text = fetch_get(self.web_page, url, headers, fetch_config)
4077
+ error_code = response_text.get('code')
4078
+ if str(error_code) != '0':
4079
+ raise send_exception(f'获取下载地址失败: {json.dumps(response_text, ensure_ascii=False)}')
4080
+
4081
+ download_url = response_text.get('info', {}).get('url')
4082
+ if not download_url:
4083
+ raise send_exception('下载地址为空')
4084
+
4085
+ log(f'获取到下载地址: {download_url}')
4086
+
4087
+ # 第五步:下载文件
4088
+ log(f'步骤5: 下载文件到本地(文件类型: {file_extension})')
4089
+
4090
+ # 下载临时文件
4091
+ temp_file_name = f'finance_details_temp_{self.store_username}_{start_date}_{end_date}.{file_extension}'
4092
+ temp_file_path = os.path.join(download_dir, temp_file_name)
4093
+
4094
+ # 使用requests下载文件
4095
+ response = requests.get(download_url, stream=True)
4096
+ if response.status_code == 200:
4097
+ with open(temp_file_path, 'wb') as f:
4098
+ for chunk in response.iter_content(chunk_size=8192):
4099
+ f.write(chunk)
4100
+ log(f'文件下载成功: {temp_file_path}')
4101
+ else:
4102
+ raise send_exception(f'文件下载失败,状态码: {response.status_code}')
4103
+
4104
+ # 第六步:处理文件并添加店铺信息列
4105
+ log('步骤6: 处理文件并添加店铺信息列')
4106
+
4107
+ # 从文件名中提取store_username
4108
+ store_username = self.store_username
4109
+
4110
+ # 自动识别需要保持为字符串的列(包含以下关键词的列保持为字符串)
4111
+ str_keywords = ['业务单号']
4112
+
4113
+ all_data = []
4114
+ header = None
4115
+ dtype_dict = None
4116
+
4117
+ if file_extension == 'zip':
4118
+ # 处理zip文件:解压并合并多个Excel
4119
+ log('文件类型为zip,开始解压和合并...')
4120
+
4121
+ # 解压到临时目录
4122
+ extract_dir = os.path.join(download_dir, 'temp')
4123
+ os.makedirs(extract_dir, exist_ok=True)
4124
+
4125
+ log(f'解压文件到: {extract_dir}')
4126
+ with zipfile.ZipFile(temp_file_path, 'r') as zip_ref:
4127
+ zip_ref.extractall(extract_dir)
4128
+
4129
+ # 查找所有excel文件
4130
+ excel_files = []
4131
+ for root, dirs, files in os.walk(extract_dir):
4132
+ for file in files:
4133
+ if file.endswith(('.xlsx', '.xls')):
4134
+ excel_files.append(os.path.join(root, file))
4135
+
4136
+ log(f'找到 {len(excel_files)} 个excel文件')
4137
+
4138
+ if len(excel_files) == 0:
4139
+ raise Exception('zip文件中未找到excel文件')
4140
+
4141
+ # 读取并合并所有excel数据
4142
+ for idx, excel_file in enumerate(excel_files):
4143
+ log(f'读取文件 {idx + 1}/{len(excel_files)}: {os.path.basename(excel_file)}')
4144
+
4145
+ try:
4146
+ # 第一次读取时,确定需要保持为字符串的列
4147
+ if idx == 0:
4148
+ df_temp = pd.read_excel(excel_file, sheet_name=0, nrows=0)
4149
+ all_columns = df_temp.columns.tolist()
4150
+
4151
+ # 自动识别字符串列
4152
+ str_columns = []
4153
+ for col in all_columns:
4154
+ col_str = str(col)
4155
+ if any(keyword in col_str for keyword in str_keywords):
4156
+ str_columns.append(col)
4157
+
4158
+ if str_columns:
4159
+ log(f'自动识别需要保持为字符串的列: {str_columns}')
4160
+ dtype_dict = {col: str for col in str_columns}
4161
+
4162
+ # 使用pandas读取excel,指定特定列为字符串类型
4163
+ df = pd.read_excel(excel_file, sheet_name=0, dtype=dtype_dict)
4164
+ log(f'pandas读取成功,数据形状: {df.shape} (行数×列数)')
4165
+
4166
+ # 获取表头
4167
+ if idx == 0:
4168
+ header = df.columns.tolist()
4169
+ log(f'表头: {header[:5]}... (显示前5列)')
4170
+
4171
+ # 获取数据
4172
+ data_rows = df.values.tolist()
4173
+ all_data.extend(data_rows)
4174
+ log(f'第{idx + 1}个文件添加了 {len(data_rows)} 行数据')
4175
+
4176
+ except Exception as e:
4177
+ log(f'pandas读取失败: {e},尝试使用openpyxl读取')
4178
+ # 备用方案:使用openpyxl
4179
+ wb = openpyxl.load_workbook(excel_file, read_only=True, data_only=True)
4180
+
4181
+ if '财务收支明细' in wb.sheetnames:
4182
+ ws = wb['财务收支明细']
4183
+ else:
4184
+ ws = wb.worksheets[0]
4185
+
4186
+ log(f'使用工作表: {ws.title}')
4187
+ rows = list(ws.iter_rows(values_only=True))
4188
+
4189
+ if idx == 0 and len(rows) > 0:
4190
+ header = list(rows[0])
4191
+ all_data.extend(rows[1:])
4192
+ elif len(rows) > 1:
4193
+ all_data.extend(rows[1:])
4194
+
4195
+ wb.close()
4196
+
4197
+ # 清理临时解压目录
4198
+ shutil.rmtree(extract_dir)
4199
+ log('临时解压目录已清理')
4200
+
4201
+ else:
4202
+ # 处理单个xlsx文件
4203
+ log('文件类型为xlsx,直接读取...')
4204
+
4205
+ try:
4206
+ # 确定需要保持为字符串的列
4207
+ df_temp = pd.read_excel(temp_file_path, sheet_name=0, nrows=0)
4208
+ all_columns = df_temp.columns.tolist()
4209
+
4210
+ str_columns = []
4211
+ for col in all_columns:
4212
+ col_str = str(col)
4213
+ if any(keyword in col_str for keyword in str_keywords):
4214
+ str_columns.append(col)
4215
+
4216
+ if str_columns:
4217
+ log(f'自动识别需要保持为字符串的列: {str_columns}')
4218
+ dtype_dict = {col: str for col in str_columns}
4219
+
4220
+ # 读取excel
4221
+ df = pd.read_excel(temp_file_path, sheet_name=0, dtype=dtype_dict)
4222
+ log(f'pandas读取成功,数据形状: {df.shape} (行数×列数)')
4223
+
4224
+ header = df.columns.tolist()
4225
+ log(f'表头: {header[:5]}... (显示前5列)')
4226
+
4227
+ all_data = df.values.tolist()
4228
+ log(f'读取了 {len(all_data)} 行数据')
4229
+
4230
+ except Exception as e:
4231
+ log(f'pandas读取失败: {e},尝试使用openpyxl读取')
4232
+ # 备用方案
4233
+ wb = openpyxl.load_workbook(temp_file_path, read_only=True, data_only=True)
4234
+
4235
+ if '财务收支明细' in wb.sheetnames:
4236
+ ws = wb['财务收支明细']
4237
+ else:
4238
+ ws = wb.worksheets[0]
4239
+
4240
+ rows = list(ws.iter_rows(values_only=True))
4241
+ if len(rows) > 0:
4242
+ header = list(rows[0])
4243
+ all_data = rows[1:]
4244
+
4245
+ wb.close()
4246
+
4247
+ log(f'合并完成,共 {len(all_data)} 行数据')
4248
+
4249
+ # 在表头前添加3列
4250
+ new_header = ['店铺账号', '店铺名称', '店长'] + header
4251
+
4252
+ # 在每行数据前添加3列
4253
+ new_data = []
4254
+ for row in all_data:
4255
+ # 将 tuple 转换为 list,并在前面添加3列
4256
+ new_row = [store_username, '', ''] + list(row)
4257
+ new_data.append(new_row)
4258
+
4259
+ # 创建新的工作簿并写入数据
4260
+ log(f'写入合并后的excel: {output_file_path}')
4261
+ wb_new = Workbook()
4262
+ ws_new = wb_new.active
4263
+ ws_new.title = '财务收支明细'
4264
+
4265
+ # 写入表头
4266
+ ws_new.append(new_header)
4267
+
4268
+ # 写入数据
4269
+ for row_data in new_data:
4270
+ ws_new.append(row_data)
4271
+
4272
+ # 保存文件(显式转换为str以避免类型提示警告)
4273
+ wb_new.save(str(output_file_path))
4274
+ log(f'合并完成,文件已保存: {output_file_path}')
4275
+
4276
+ # 删除临时下载文件
4277
+ if os.path.exists(temp_file_path):
4278
+ os.remove(temp_file_path)
4279
+ log('临时下载文件已清理')
4280
+
4281
+ return output_file_path
4282
+
4283
+ def query_hosting_info_list(self):
4284
+ """
4285
+ 查询店铺活动托管规则列表
4286
+
4287
+ Returns:
4288
+ list: 托管规则列表,每个元素包含:
4289
+ - hosting_id: 托管规则ID
4290
+ - scene_type: 场景类型
4291
+ - state: 状态
4292
+ - hosting_name: 托管规则名称
4293
+ - hosting_tools_id: 托管工具ID
4294
+ - hosting_tools_state: 托管工具状态
4295
+ - time_zone: 时区
4296
+ - create_user: 创建用户
4297
+ - last_update_user: 最后更新用户
4298
+ - insert_time: 创建时间
4299
+ - last_update_time: 最后更新时间
4300
+ - exist_act_goods: 是否存在活动商品
4301
+ """
4302
+ log(f'正在获取 {self.store_name} 店铺活动托管规则列表')
4303
+
4304
+ cache_file = f'{self.config.auto_dir}/shein/cache/hosting_info_list_{self.store_username}_{TimeUtils.today_date()}.json'
4305
+ hosting_list = read_dict_from_file_ex(cache_file, self.store_username, 3600 * 8)
4306
+ if len(hosting_list) > 0:
4307
+ log('返回缓存数据')
4308
+ return hosting_list
4309
+
4310
+ url = "https://sso.geiwohuo.com/mrs-api-prefix/promotion/hosting/query_hosting_info_list"
4311
+ payload = {}
4312
+
4313
+ response_text = fetch(self.web_page, url, payload)
4314
+ error_code = response_text.get('code')
4315
+ if str(error_code) != '0':
4316
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
4317
+
4318
+ hosting_list = response_text.get('info', [])
4319
+ log(f'获取到 {len(hosting_list)} 条托管规则')
4320
+
4321
+ write_dict_to_file_ex(cache_file, {self.store_username: hosting_list}, [self.store_username])
4322
+
4323
+ return hosting_list
4324
+
4325
+ def query_hosting_activity_goods(self, hosting_id, goods_states=None):
4326
+ """
4327
+ 查询托管活动参与的商品
4328
+
4329
+ Args:
4330
+ hosting_id: 托管规则ID
4331
+ goods_states: 商品状态列表,默认为[1](在售)
4332
+
4333
+ Returns:
4334
+ list: 参与托管活动的商品列表,每个元素包含:
4335
+ - goods_state: 商品状态
4336
+ - skc_info_list: SKC信息列表
4337
+ - skc_id: SKC ID
4338
+ - skc_name: SKC名称
4339
+ - goods_name: 商品名称
4340
+ - image_url: 图片URL
4341
+ - act_stock_num: 活动库存数量
4342
+ - act_sales_num: 活动销量
4343
+ - activity_info: 活动信息
4344
+ - sku_info_list: SKU信息列表
4345
+ """
4346
+ if goods_states is None:
4347
+ goods_states = [1]
4348
+
4349
+ log(f'正在获取 {self.store_name} 托管活动商品列表 hosting_id={hosting_id}')
4350
+
4351
+ cache_file = f'{self.config.auto_dir}/shein/cache/hosting_activity_goods_{self.store_username}_{hosting_id}_{TimeUtils.today_date()}.json'
4352
+ goods_list = read_dict_from_file_ex(cache_file, self.store_username, 3600 * 8)
4353
+ if len(goods_list) > 0:
4354
+ log('返回缓存数据')
4355
+ return goods_list
4356
+
4357
+ page_num = 1
4358
+ page_size = 100
4359
+
4360
+ url = "https://sso.geiwohuo.com/mrs-api-prefix/promotion/hosting/query_hosting_activity_goods"
4361
+ payload = {
4362
+ "goods_states": goods_states,
4363
+ "hosting_id" : str(hosting_id),
4364
+ "page_num" : page_num,
4365
+ "page_size" : page_size
4366
+ }
4367
+
4368
+ response_text = fetch(self.web_page, url, payload)
4369
+ error_code = response_text.get('code')
4370
+ if str(error_code) != '0':
4371
+ raise send_exception(json.dumps(response_text, ensure_ascii=False))
4372
+
4373
+ goods_list = response_text.get('info', [])
4374
+ if not goods_list:
4375
+ log('未获取到商品数据')
4376
+ return []
4377
+
4378
+ # 获取第一页数据
4379
+ first_item = goods_list[0] if goods_list else {}
4380
+ skc_info_list = first_item.get('skc_info_list', {})
4381
+ all_data = skc_info_list.get('data', [])
4382
+ meta = skc_info_list.get('meta', {})
4383
+ total = meta.get('count', 0)
4384
+
4385
+ log(f'第1页获取到 {len(all_data)} 条商品,总数: {total}')
4386
+
4387
+ # 如果有多页,继续获取
4388
+ if total > page_size:
4389
+ totalPage = math.ceil(total / page_size)
4390
+ for page in range(2, totalPage + 1):
4391
+ log(f'获取托管活动商品列表 第{page}/{totalPage}页')
4392
+ payload['page_num'] = page
4393
+ response_text = fetch(self.web_page, url, payload)
4394
+ error_code = response_text.get('code')
4395
+ if str(error_code) != '0':
4396
+ log(f'获取第{page}页失败: {response_text}')
4397
+ continue
4398
+
4399
+ page_goods_list = response_text.get('info', [])
4400
+ if page_goods_list:
4401
+ page_data = page_goods_list[0].get('skc_info_list', {}).get('data', [])
4402
+ all_data.extend(page_data)
4403
+ log(f'第{page}页获取到 {len(page_data)} 条商品')
4404
+
4405
+ time.sleep(0.1)
4406
+
4407
+ log(f'总共获取到 {len(all_data)} 条商品')
4408
+
4409
+ # 保存缓存
4410
+ write_dict_to_file_ex(cache_file, {self.store_username: all_data}, [self.store_username])
4411
+
4412
+ return all_data
4413
+
4414
+ def get_skc_activity_price_info(self, skc, activity_id):
4415
+ """
4416
+ 根据SKC和活动ID获取供货价、活动价和活动库存
4417
+
4418
+ Args:
4419
+ skc: SKC名称
4420
+ activity_id: 活动ID(可以是字符串或整数)
4421
+
4422
+ Returns:
4423
+ dict: 包含以下键值的字典,如果未找到则返回None:
4424
+ - sku_price: SKU供货价(取第一个SKU的价格)
4425
+ - act_sku_price: SKU活动价(取第一个SKU的活动价)
4426
+ - act_stock_num: 活动库存数量
4427
+ - skc_name: SKC名称
4428
+ - goods_name: 商品名称
4429
+ - activity_id: 活动ID
4430
+ - currency: 币种
4431
+ - image_url: 商品图片
4432
+ """
4433
+ log(f'获取SKC活动价格信息: skc={skc}, activity_id={activity_id}')
4434
+
4435
+ # 转换activity_id为整数进行比较
4436
+ try:
4437
+ target_activity_id = int(activity_id)
4438
+ except (ValueError, TypeError):
4439
+ log(f'无效的activity_id: {activity_id}')
4440
+ return None
4441
+
4442
+ # 缓存文件,使用skc和activity_id作为缓存key
4443
+ cache_file = f'{self.config.auto_dir}/shein/cache/skc_activity_price_{self.store_username}_{skc}_{activity_id}_{TimeUtils.today_date()}.json'
4444
+ cached_data = read_dict_from_file(cache_file, 3600 * 8)
4445
+ if cached_data:
4446
+ log('返回缓存的价格信息')
4447
+ return cached_data
4448
+
4449
+ # 获取所有托管规则
4450
+ hosting_list = self.query_hosting_info_list()
4451
+
4452
+ if not hosting_list:
4453
+ log('未找到任何托管规则')
4454
+ return None
4455
+
4456
+ # 遍历所有托管规则,查找匹配的SKC和活动
4457
+ for hosting in hosting_list:
4458
+ hosting_id = hosting.get('hosting_id')
4459
+ if not hosting_id:
4460
+ continue
4461
+
4462
+ log(f'查询托管规则: hosting_id={hosting_id}, hosting_name={hosting.get("hosting_name")}')
4463
+
4464
+ # 获取该托管规则下的商品
4465
+ goods_list = self.query_hosting_activity_goods(hosting_id)
4466
+
4467
+ # 在商品列表中查找匹配的SKC
4468
+ for goods_item in goods_list:
4469
+ skc_name = goods_item.get('skc_name', '')
4470
+
4471
+ # 匹配SKC名称
4472
+ if skc_name != skc:
4473
+ continue
4474
+
4475
+ # 检查活动信息
4476
+ activity_info = goods_item.get('activity_info', {})
4477
+ goods_activity_id = activity_info.get('activity_id')
4478
+
4479
+ # 匹配活动ID
4480
+ try:
4481
+ if int(goods_activity_id) != target_activity_id:
4482
+ continue
4483
+ except (ValueError, TypeError):
4484
+ continue
4485
+
4486
+ log(f'找到匹配的SKC: {skc_name}, activity_id={goods_activity_id}')
4487
+
4488
+ # 提取活动库存
4489
+ act_stock_num = goods_item.get('act_stock_num', 0)
4490
+
4491
+ # 获取第一个SKU的价格信息
4492
+ sku_info_list = goods_item.get('sku_info_list', [])
4493
+ if not sku_info_list:
4494
+ log(f'SKC {skc_name} 没有SKU信息')
4495
+ continue
4496
+
4497
+ first_sku = sku_info_list[0]
4498
+ sku_price = first_sku.get('sku_price', 0)
4499
+ act_sku_price = first_sku.get('act_sku_price', 0)
4500
+ currency = first_sku.get('currency', 'CNY')
4501
+
4502
+ # 构建返回结果
4503
+ result = {
4504
+ 'skc_name' : skc_name,
4505
+ 'goods_name' : goods_item.get('goods_name', ''),
4506
+ 'image_url' : goods_item.get('image_url', ''),
4507
+ 'activity_id' : goods_activity_id,
4508
+ 'act_stock_num': act_stock_num,
4509
+ 'sku_price' : sku_price,
4510
+ 'act_sku_price': act_sku_price,
4511
+ 'currency' : currency,
4512
+ 'start_time' : activity_info.get('start_time', ''),
4513
+ 'end_time' : activity_info.get('end_time', ''),
4514
+ 'time_zone' : activity_info.get('time_zone', ''),
4515
+ }
4516
+
4517
+ log(f'SKC供货价: {sku_price} {currency}, 活动价: {act_sku_price} {currency}, 活动库存: {act_stock_num}')
4518
+
4519
+ # 保存缓存
4520
+ write_dict_to_file(cache_file, result)
4521
+
4522
+ return result
4523
+
4524
+ log(f'未找到匹配的SKC和活动: skc={skc}, activity_id={activity_id}')
4525
+ return None