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.
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 新品分析数据模型
5
+ 使用SQLAlchemy定义shein_new_product_analysis表结构
6
+ """
7
+
8
+ from sqlalchemy import create_engine, Column, Integer, String, DECIMAL, Date, BigInteger, DateTime, Text, Index
9
+ from sqlalchemy.ext.declarative import declarative_base
10
+ from sqlalchemy.orm import sessionmaker
11
+ from datetime import datetime
12
+ import json
13
+
14
+ # 创建基类
15
+ Base = declarative_base()
16
+
17
+ class SheinNewProductAnalysis(Base):
18
+ """
19
+ 希音新品分析表
20
+ 存储SKC新品分析相关数据
21
+ """
22
+ __tablename__ = 'shein_new_product_analysis'
23
+
24
+ # 主键ID
25
+ id = Column(BigInteger, primary_key=True, autoincrement=True, comment='主键ID')
26
+
27
+ # 店铺信息
28
+ store_username = Column(String(100), nullable=False, comment='店铺账号')
29
+ store_name = Column(String(200), nullable=True, comment='店铺名称')
30
+
31
+ # 日期信息
32
+ stat_date = Column(Date, nullable=False, comment='统计日期')
33
+ shelf_date = Column(Date, nullable=True, comment='上架日期')
34
+
35
+ # SKC信息
36
+ skc = Column(String(100), nullable=False, comment='平台SKC')
37
+ sku_supplier_no = Column(String(100), nullable=True, comment='商家SKC')
38
+ layer_nm = Column(String(100), nullable=True, comment='商品层级')
39
+
40
+ # 品类信息
41
+ new_cate1_nm = Column(String(200), nullable=True, comment='一级品类名称')
42
+ new_cate2_nm = Column(String(200), nullable=True, comment='二级品类名称')
43
+ new_cate3_nm = Column(String(200), nullable=True, comment='三级品类名称')
44
+ new_cate4_nm = Column(String(200), nullable=True, comment='四级品类名称')
45
+
46
+ goods_name = Column(String(500), nullable=True, comment='商品标题')
47
+ img_url = Column(String(500), nullable=True, comment='SKC图片URL')
48
+
49
+ # 状态标识
50
+ onsale_flag = Column(Integer, nullable=True, default=0, comment='在售标识(0-否,1-是)')
51
+ sale_flag = Column(Integer, nullable=True, default=0, comment='上架标识(0-否,1-是)')
52
+ new_goods_tag = Column(Integer, nullable=True, comment='新品标签(1-新品爆款,2-新品畅销,3-潜力新品,4-新品)')
53
+
54
+ # 销售数据
55
+ sale_cnt = Column(Integer, nullable=True, default=0, comment='销量')
56
+ pay_order_cnt = Column(Integer, nullable=True, default=0, comment='支付订单数')
57
+ goods_uv = Column(Integer, nullable=True, default=0, comment='商品访客数')
58
+ cart_uv_idx = Column(Integer, nullable=True, default=0, comment='加车访客')
59
+ eps_uv_idx = Column(Integer, nullable=True, default=0, comment='曝光人数')
60
+
61
+ # 转化率数据
62
+ eps_gds_ctr_idx = Column(DECIMAL(10, 4), nullable=True, comment='点击率')
63
+ gds_pay_ctr_idx = Column(DECIMAL(10, 4), nullable=True, comment='转化率')
64
+ bad_comment_rate = Column(DECIMAL(10, 4), nullable=True, comment='差评率')
65
+
66
+ # 促销活动信息(JSON格式)
67
+ prom_inf_ing = Column(Text, nullable=True, comment='活动中的促销活动(JSON)')
68
+ prom_inf_ready = Column(Text, nullable=True, comment='即将开始的促销活动(JSON)')
69
+
70
+ # AB测试信息(JSON格式)
71
+ ab_test = Column(Text, nullable=True, comment='AB测试数据(JSON)')
72
+
73
+ # 分析字段(不参与更新)
74
+ front_price = Column(DECIMAL(10, 2), nullable=True, comment='前台价格')
75
+ reason_analysis = Column(Text, nullable=True, comment='原因分析')
76
+ optimization_action = Column(Text, nullable=True, comment='优化动作')
77
+
78
+ # 备注
79
+ remark = Column(String(500), nullable=True, comment='备注')
80
+
81
+ # 时间戳
82
+ created_at = Column(DateTime, nullable=True, default=datetime.now, comment='创建时间')
83
+ updated_at = Column(DateTime, nullable=True, default=datetime.now, onupdate=datetime.now, comment='更新时间')
84
+
85
+ # 创建联合唯一索引
86
+ __table_args__ = (
87
+ Index('idx_date_skc', 'stat_date', 'skc', unique=True),
88
+ Index('idx_stat_date', 'stat_date'),
89
+ Index('idx_skc', 'skc'),
90
+ Index('idx_store_username', 'store_username'),
91
+ )
92
+
93
+ def __repr__(self):
94
+ return f"<SheinNewProductAnalysis(id={self.id}, store_username='{self.store_username}', skc='{self.skc}', stat_date={self.stat_date})>"
95
+
96
+ @property
97
+ def new_goods_tag_name(self):
98
+ """
99
+ 获取新品标签的文本描述
100
+
101
+ Returns:
102
+ str: 新品标签文本描述
103
+ """
104
+ return self.get_new_goods_tag_name(self.new_goods_tag)
105
+
106
+ @staticmethod
107
+ def get_new_goods_tag_name(tag):
108
+ """
109
+ 将新品标签代码转换为文本描述
110
+
111
+ Args:
112
+ tag (str): 新品标签代码
113
+
114
+ Returns:
115
+ str: 新品标签文本描述
116
+ """
117
+ tag_map = {
118
+ '1': '新品爆款',
119
+ '2': '新品畅销',
120
+ '3': '潜力新品',
121
+ '4': '新品'
122
+ }
123
+ return tag_map.get(str(tag), '') if tag else ''
124
+
125
+ class NewProductAnalysisManager:
126
+ """
127
+ 新品分析数据管理器
128
+ 提供数据库操作相关方法
129
+ """
130
+
131
+ # database_url = f"mysql+pymysql://{config.mysql_username}:{config.mysql_password}@{config.mysql_host}:{config.mysql_port}/{config.mysql_database}"
132
+
133
+ @classmethod
134
+ def __init__(self, database_url=None):
135
+ """
136
+ 初始化数据库连接
137
+
138
+ Args:
139
+ database_url (str): 数据库连接URL
140
+ """
141
+ # if database_url is None:
142
+ # database_url = self.database_url
143
+ print(f"{self.__name__} 连接数据库: {database_url}")
144
+ self.engine = create_engine(database_url, echo=False)
145
+ self.Session = sessionmaker(bind=self.engine)
146
+
147
+ def create_table(self):
148
+ """
149
+ 创建数据表
150
+ """
151
+ Base.metadata.create_all(self.engine)
152
+ print("数据表创建成功")
153
+
154
+ def insert_data(self, data_list):
155
+ """
156
+ 批量插入数据
157
+
158
+ Args:
159
+ data_list (list): 数据字典列表
160
+
161
+ Returns:
162
+ int: 成功插入的记录数
163
+ """
164
+ session = self.Session()
165
+ try:
166
+ insert_count = 0
167
+ for data in data_list:
168
+ # 检查是否存在(根据唯一索引:stat_date + skc)
169
+ existing = session.query(SheinNewProductAnalysis).filter(
170
+ SheinNewProductAnalysis.stat_date == data.get('stat_date'),
171
+ SheinNewProductAnalysis.skc == data.get('skc')
172
+ ).first()
173
+
174
+ # 先处理JSON字段转换(无论是新增还是更新都需要)
175
+ # 使用 'in' 判断而不是 'and',确保空列表[]和空字典{}也能被转换
176
+ if 'prom_inf_ing' in data and isinstance(data['prom_inf_ing'], (dict, list)):
177
+ data['prom_inf_ing'] = json.dumps(data['prom_inf_ing'], ensure_ascii=False)
178
+ if 'prom_inf_ready' in data and isinstance(data['prom_inf_ready'], (dict, list)):
179
+ data['prom_inf_ready'] = json.dumps(data['prom_inf_ready'], ensure_ascii=False)
180
+ if 'ab_test' in data and isinstance(data['ab_test'], (dict, list)):
181
+ data['ab_test'] = json.dumps(data['ab_test'], ensure_ascii=False)
182
+
183
+ if existing:
184
+ # 更新现有记录(排除不更新的分析字段)
185
+ exclude_fields = {'front_price', 'reason_analysis', 'optimization_action'}
186
+ for key, value in data.items():
187
+ if key not in exclude_fields:
188
+ setattr(existing, key, value)
189
+ setattr(existing, 'updated_at', datetime.now())
190
+ else:
191
+ # 插入新记录
192
+ new_record = SheinNewProductAnalysis(**data)
193
+ session.add(new_record)
194
+ insert_count += 1
195
+
196
+ session.commit()
197
+ print(f"成功插入/更新 {insert_count} 条记录")
198
+ return insert_count
199
+ except Exception as e:
200
+ session.rollback()
201
+ print(f"插入数据失败: {e}")
202
+ raise
203
+ finally:
204
+ session.close()
205
+
206
+ def get_records_by_date_range(self, store_username, start_date, end_date):
207
+ """
208
+ 根据日期范围查询记录
209
+
210
+ Args:
211
+ store_username (str): 店铺账号
212
+ start_date (str): 开始日期
213
+ end_date (str): 结束日期
214
+
215
+ Returns:
216
+ list: 记录列表
217
+ """
218
+ session = self.Session()
219
+ try:
220
+ records = session.query(SheinNewProductAnalysis).filter(
221
+ SheinNewProductAnalysis.store_username == store_username,
222
+ SheinNewProductAnalysis.stat_date >= start_date,
223
+ SheinNewProductAnalysis.stat_date <= end_date
224
+ ).all()
225
+ return records
226
+ finally:
227
+ session.close()
228
+
229
+ def get_records_by_skc(self, store_username, skc):
230
+ """
231
+ 根据SKC查询记录
232
+
233
+ Args:
234
+ store_username (str): 店铺账号
235
+ skc (str): 平台SKC
236
+
237
+ Returns:
238
+ list: 记录列表
239
+ """
240
+ session = self.Session()
241
+ try:
242
+ records = session.query(SheinNewProductAnalysis).filter(
243
+ SheinNewProductAnalysis.store_username == store_username,
244
+ SheinNewProductAnalysis.skc == skc
245
+ ).order_by(SheinNewProductAnalysis.stat_date.desc()).all()
246
+ return records
247
+ finally:
248
+ session.close()
249
+
250
+ def _extract_prom_info(self, prom_list, current_skc=None):
251
+ """
252
+ 提取促销活动信息
253
+
254
+ Args:
255
+ prom_list (list): 促销活动列表
256
+ current_skc (str): 当前SKC,用于从promDetail中筛选匹配的记录
257
+
258
+ Returns:
259
+ list: 提取后的促销活动信息列表
260
+ """
261
+ if not prom_list:
262
+ return []
263
+
264
+ result = []
265
+ for prom in prom_list:
266
+ prom_info = {
267
+ 'promNm' : prom.get('promNm'),
268
+ 'promId' : prom.get('promId'),
269
+ 'startDate': prom.get('startDate'),
270
+ 'endDate' : prom.get('endDate')
271
+ }
272
+
273
+ prom_id = prom.get('promId', '')
274
+ prom_detail = prom.get('promDetail', [])
275
+
276
+ # 如果提供了current_skc,从promDetail中筛选匹配的记录
277
+ if current_skc and prom_detail and isinstance(prom_detail, list):
278
+ # 查找匹配当前SKC的detail记录
279
+ matched_detail = None
280
+ for detail in prom_detail:
281
+ if isinstance(detail, dict) and detail.get('skc') == current_skc:
282
+ matched_detail = detail
283
+ break
284
+
285
+ # 如果找到匹配的记录,使用它;否则使用第一个
286
+ if matched_detail:
287
+ prom_detail = [matched_detail]
288
+ # 如果没找到匹配的,保持原样(使用第一个)
289
+
290
+ # 根据promId长度判断取哪些字段
291
+ if len(str(prom_id)) >= 11 and prom_detail:
292
+ # 营销工具:取第一个detail的数据
293
+ detail = prom_detail[0] if isinstance(prom_detail, list) else prom_detail
294
+ if isinstance(detail, dict):
295
+ prom_info['attend_num_sum'] = detail.get('act_stock_num') or detail.get('attend_num_sum')
296
+ prom_info['supply_price'] = detail.get('sku_price') or detail.get('supply_price')
297
+ prom_info['product_act_price'] = detail.get('act_sku_price') or detail.get('product_act_price')
298
+ elif len(str(prom_id)) >= 8 and prom_detail:
299
+ # 营销工具:取第一个detail的数据
300
+ detail = prom_detail[0] if isinstance(prom_detail, list) else prom_detail
301
+ if isinstance(detail, dict):
302
+ prom_info['attend_num_sum'] = detail.get('attend_num_sum')
303
+ prom_info['supply_price'] = detail.get('supply_price')
304
+ prom_info['product_act_price'] = detail.get('product_act_price')
305
+ elif prom_detail:
306
+ # 营销活动:取第一个detail的数据
307
+ detail = prom_detail[0] if isinstance(prom_detail, list) else prom_detail
308
+ if isinstance(detail, dict):
309
+ prom_info['attend_num'] = detail.get('attend_num')
310
+ prom_info['goods_cost'] = detail.get('goods_cost')
311
+ prom_info['attend_cost'] = detail.get('attend_cost')
312
+
313
+ result.append(prom_info)
314
+
315
+ return result
316
+
317
+ def import_from_json(self, json_data_list):
318
+ """
319
+ 从JSON数据导入到数据库
320
+
321
+ Args:
322
+ json_data_list (list): JSON数据列表
323
+
324
+ Returns:
325
+ int: 成功导入的记录数
326
+ """
327
+ print(f"开始处理 {len(json_data_list)} 条JSON数据")
328
+ data_list = []
329
+ skipped_count = 0
330
+
331
+ for idx, item in enumerate(json_data_list, 1):
332
+ # 验证必填字段
333
+ stat_date = item.get('stat_date')
334
+ skc = item.get('skc')
335
+
336
+ if not stat_date:
337
+ print(f"⚠️ 警告: 第 {idx} 条记录缺少必填字段 stat_date (统计日期),跳过该记录")
338
+ skipped_count += 1
339
+ continue
340
+
341
+ if not skc:
342
+ print(f"⚠️ 警告: 第 {idx} 条记录缺少必填字段 skc,跳过该记录")
343
+ skipped_count += 1
344
+ continue
345
+
346
+ # 提取促销活动信息(传入当前SKC以筛选匹配的促销信息)
347
+ prom_campaign = item.get('promCampaign', {})
348
+ prom_inf_ing = self._extract_prom_info(prom_campaign.get('promInfIng'), current_skc=skc)
349
+ prom_inf_ready = self._extract_prom_info(prom_campaign.get('promInfReady'), current_skc=skc)
350
+
351
+ # 提取AB测试信息(去掉product_name字段)
352
+ ab_test = item.get('ab_test', {})
353
+ if ab_test and isinstance(ab_test, dict):
354
+ ab_test_copy = ab_test.copy()
355
+ ab_test_copy.pop('product_name', None)
356
+ ab_test = ab_test_copy
357
+
358
+ # 构建数据库记录
359
+ record = {
360
+ 'store_username' : item.get('store_username'),
361
+ 'store_name' : item.get('store_name'),
362
+ 'stat_date' : stat_date,
363
+ 'shelf_date' : item.get('shelf_date'),
364
+ 'skc' : skc,
365
+ 'sku_supplier_no' : item.get('skuSupplierNo'),
366
+ 'layer_nm' : item.get('layerNm'),
367
+ 'new_cate1_nm' : item.get('newCate1Nm'),
368
+ 'new_cate2_nm' : item.get('newCate2Nm'),
369
+ 'new_cate3_nm' : item.get('newCate3Nm'),
370
+ 'new_cate4_nm' : item.get('newCate4Nm'),
371
+ 'goods_name' : item.get('goodsName'),
372
+ 'img_url' : item.get('imgUrl'),
373
+ 'onsale_flag' : int(item.get('onsaleFlag', 0)),
374
+ 'sale_flag' : int(item.get('saleFlag', 0)),
375
+ 'new_goods_tag' : item.get('newGoodsTag'),
376
+ 'sale_cnt' : item.get('saleCnt', 0),
377
+ 'pay_order_cnt' : item.get('payOrderCnt', 0),
378
+ 'goods_uv' : item.get('goodsUv', 0),
379
+ 'cart_uv_idx' : item.get('cartUvIdx', 0),
380
+ 'eps_uv_idx' : item.get('epsUvIdx', 0),
381
+ 'eps_gds_ctr_idx' : item.get('epsGdsCtrIdx', 0),
382
+ 'gds_pay_ctr_idx' : item.get('gdsPayCtrIdx', 0),
383
+ 'bad_comment_rate': item.get('badCommentRate', 0),
384
+ 'prom_inf_ing' : prom_inf_ing,
385
+ 'prom_inf_ready' : prom_inf_ready,
386
+ 'ab_test' : ab_test
387
+ }
388
+
389
+ print(f"✓ 处理第 {idx} 条: SKC={record['skc']}, stat_date={stat_date}")
390
+ data_list.append(record)
391
+
392
+ if skipped_count > 0:
393
+ print(f"⚠️ 共跳过 {skipped_count} 条记录(缺少必填字段)")
394
+
395
+ # 调用insert_data方法插入数据
396
+ return self.insert_data(data_list)
397
+
398
+ def update_front_price(self, stat_date, skc, front_price):
399
+ """
400
+ 更新指定SKC的前台价格
401
+
402
+ Args:
403
+ stat_date (str): 统计日期
404
+ skc (str): 平台SKC
405
+ front_price (float): 前台价格
406
+
407
+ Returns:
408
+ bool: 是否成功
409
+ """
410
+ session = self.Session()
411
+ try:
412
+ record = session.query(SheinNewProductAnalysis).filter(
413
+ SheinNewProductAnalysis.stat_date == stat_date,
414
+ SheinNewProductAnalysis.skc == skc
415
+ ).first()
416
+
417
+ if record:
418
+ record.front_price = front_price
419
+ setattr(record, 'updated_at', datetime.now())
420
+ session.commit()
421
+ print(f"成功更新前台价格: {skc} ({stat_date}) -> {front_price}")
422
+ return True
423
+ else:
424
+ print(f"未找到记录: SKC={skc}, 日期={stat_date}")
425
+ return False
426
+ except Exception as e:
427
+ session.rollback()
428
+ print(f"更新前台价格失败: {e}")
429
+ raise
430
+ finally:
431
+ session.close()
432
+
433
+ def delete_records_by_date(self, store_username, stat_date):
434
+ """
435
+ 删除指定日期的记录
436
+
437
+ Args:
438
+ store_username (str): 店铺账号
439
+ stat_date (str): 统计日期
440
+
441
+ Returns:
442
+ int: 删除的记录数
443
+ """
444
+ session = self.Session()
445
+ try:
446
+ delete_count = session.query(SheinNewProductAnalysis).filter(
447
+ SheinNewProductAnalysis.store_username == store_username,
448
+ SheinNewProductAnalysis.stat_date == stat_date
449
+ ).delete()
450
+ session.commit()
451
+ print(f"成功删除 {delete_count} 条记录")
452
+ return delete_count
453
+ except Exception as e:
454
+ session.rollback()
455
+ print(f"删除数据失败: {e}")
456
+ raise
457
+ finally:
458
+ session.close()
459
+
460
+ # 创建全局实例,用于快速访问
461
+ _new_product_analysis_manager = None
462
+
463
+ def get_new_product_analysis_manager():
464
+ """
465
+ 获取新品分析管理器实例
466
+
467
+ Returns:
468
+ NewProductAnalysisManager: 新品分析管理器实例
469
+ """
470
+ global _new_product_analysis_manager
471
+ if _new_product_analysis_manager is None:
472
+ _new_product_analysis_manager = NewProductAnalysisManager()
473
+ return _new_product_analysis_manager
474
+
475
+ def insert_analysis_data(data_list):
476
+ """
477
+ 插入分析数据的便捷函数
478
+
479
+ Args:
480
+ data_list (list): 数据字典列表
481
+
482
+ Returns:
483
+ int: 成功插入的记录数
484
+ """
485
+ manager = get_new_product_analysis_manager()
486
+ return manager.insert_data(data_list)
487
+
488
+ def import_from_json(json_data_list):
489
+ """
490
+ 从JSON数据导入的便捷函数
491
+
492
+ Args:
493
+ json_data_list (list): JSON数据列表
494
+
495
+ Returns:
496
+ int: 成功导入的记录数
497
+ """
498
+ manager = get_new_product_analysis_manager()
499
+ return manager.import_from_json(json_data_list)
500
+
501
+ def get_records_by_skc(store_username, skc):
502
+ """
503
+ 根据SKC查询记录的便捷函数
504
+
505
+ Args:
506
+ store_username (str): 店铺账号
507
+ skc (str): 平台SKC
508
+
509
+ Returns:
510
+ list: 记录列表
511
+ """
512
+ manager = get_new_product_analysis_manager()
513
+ return manager.get_records_by_skc(store_username, skc)
514
+
515
+
516
+ def update_front_price(stat_date, skc, front_price):
517
+ """
518
+ 更新前台价格的便捷函数
519
+
520
+ Args:
521
+ stat_date (str): 统计日期
522
+ skc (str): 平台SKC
523
+ front_price (float): 前台价格
524
+
525
+ Returns:
526
+ bool: 是否成功
527
+ """
528
+ manager = get_new_product_analysis_manager()
529
+ return manager.update_front_price(stat_date, skc, front_price)
530
+
531
+
532
+ if __name__ == '__main__':
533
+ database_url = "mysql+pymysql://root:123wyk@127.0.0.1:3306/lz"
534
+
535
+ # 测试代码
536
+ manager = NewProductAnalysisManager(database_url)
537
+
538
+ # 创建表
539
+ manager.create_table()
540
+
541
+ # 读取JSON文件
542
+ with open('../../docs/skc_model_GS9740414_2025-10-22.json', 'r', encoding='utf-8') as f:
543
+ json_data = json.load(f)
544
+ count = manager.import_from_json(json_data)
545
+ print(f"成功导入 {count} 条记录")
546
+
547
+ # with open('../../docs/skc_model_S19118100_2025-10-15.json', 'r', encoding='utf-8') as f:
548
+ # json_data = json.load(f)
549
+ # count = manager.import_from_json(json_data)
550
+ # print(f"成功导入 {count} 条记录")
551
+
552
+ # 更新前台价格(手动设置,后续导入不会覆盖)
553
+ # manager.update_front_price('2025-10-15', 'si2409238815318914', 19.99)
554
+
555
+ # 或使用便捷函数
556
+ # update_front_price('2025-10-15', 'si2409238815318914', 19.99)