qrpa 1.0.32__tar.gz → 1.0.34__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of qrpa might be problematic. Click here for more details.
- {qrpa-1.0.32 → qrpa-1.0.34}/PKG-INFO +1 -1
- {qrpa-1.0.32 → qrpa-1.0.34}/pyproject.toml +1 -1
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/feishu_bot_app.py +15 -15
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/fun_excel.py +17 -2
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/shein_excel.py +212 -7
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/shein_lib.py +91 -1
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/wxwork.py +318 -318
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa.egg-info/PKG-INFO +1 -1
- {qrpa-1.0.32 → qrpa-1.0.34}/README.md +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/RateLimitedSender.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/__init__.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/db_migrator.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/fun_base.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/fun_file.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/fun_web.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/fun_win.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/shein_daily_report_model.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/shein_sqlite.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/shein_ziniao.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/temu_chrome.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/temu_excel.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/temu_lib.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/time_utils.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa/time_utils_example.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa.egg-info/SOURCES.txt +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa.egg-info/dependency_links.txt +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/qrpa.egg-info/top_level.txt +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/setup.cfg +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/setup.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/tests/test_db_migrator.py +0 -0
- {qrpa-1.0.32 → qrpa-1.0.34}/tests/test_wxwork.py +0 -0
|
@@ -32,17 +32,17 @@ class FeishuBot:
|
|
|
32
32
|
.build()
|
|
33
33
|
return self._client
|
|
34
34
|
|
|
35
|
-
def _get_chat_id(self,
|
|
35
|
+
def _get_chat_id(self, bot_name: str) -> Optional[str]:
|
|
36
36
|
"""
|
|
37
37
|
根据群组别名获取群组ID
|
|
38
38
|
|
|
39
39
|
Args:
|
|
40
|
-
|
|
40
|
+
bot_name: 群组别名
|
|
41
41
|
|
|
42
42
|
Returns:
|
|
43
43
|
群组ID,如果别名不存在则返回None
|
|
44
44
|
"""
|
|
45
|
-
return self.config.dict_feishu_group.get(
|
|
45
|
+
return self.config.dict_feishu_group.get(bot_name)
|
|
46
46
|
|
|
47
47
|
def _handle_response_error(self, response, operation_name: str):
|
|
48
48
|
"""
|
|
@@ -61,20 +61,20 @@ class FeishuBot:
|
|
|
61
61
|
return True
|
|
62
62
|
return False
|
|
63
63
|
|
|
64
|
-
def send_text(self, content: str,
|
|
64
|
+
def send_text(self, content: str, bot_name: str = 'test') -> bool:
|
|
65
65
|
"""
|
|
66
66
|
发送文本消息
|
|
67
67
|
|
|
68
68
|
Args:
|
|
69
69
|
content: 文本内容
|
|
70
|
-
|
|
70
|
+
bot_name: 群组别名,默认为'test'
|
|
71
71
|
|
|
72
72
|
Returns:
|
|
73
73
|
发送是否成功
|
|
74
74
|
"""
|
|
75
|
-
chat_id = self._get_chat_id(
|
|
75
|
+
chat_id = self._get_chat_id(bot_name)
|
|
76
76
|
if not chat_id:
|
|
77
|
-
lark.logger.error(f"未找到群组别名 '{
|
|
77
|
+
lark.logger.error(f"未找到群组别名 '{bot_name}' 对应的群组ID")
|
|
78
78
|
return False
|
|
79
79
|
|
|
80
80
|
message_content = {"text": content}
|
|
@@ -101,13 +101,13 @@ class FeishuBot:
|
|
|
101
101
|
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
102
102
|
return True
|
|
103
103
|
|
|
104
|
-
def send_image(self, file_path: str,
|
|
104
|
+
def send_image(self, file_path: str, bot_name: str = 'test') -> bool:
|
|
105
105
|
"""
|
|
106
106
|
发送图片消息
|
|
107
107
|
|
|
108
108
|
Args:
|
|
109
109
|
file_path: 图片文件路径
|
|
110
|
-
|
|
110
|
+
bot_name: 群组别名,默认为'test'
|
|
111
111
|
|
|
112
112
|
Returns:
|
|
113
113
|
发送是否成功
|
|
@@ -117,9 +117,9 @@ class FeishuBot:
|
|
|
117
117
|
if not image_key:
|
|
118
118
|
return False
|
|
119
119
|
|
|
120
|
-
chat_id = self._get_chat_id(
|
|
120
|
+
chat_id = self._get_chat_id(bot_name)
|
|
121
121
|
if not chat_id:
|
|
122
|
-
lark.logger.error(f"未找到群组别名 '{
|
|
122
|
+
lark.logger.error(f"未找到群组别名 '{bot_name}' 对应的群组ID")
|
|
123
123
|
return False
|
|
124
124
|
|
|
125
125
|
message_content = {"image_key": image_key}
|
|
@@ -146,13 +146,13 @@ class FeishuBot:
|
|
|
146
146
|
lark.logger.info(lark.JSON.marshal(response.data, indent=4))
|
|
147
147
|
return True
|
|
148
148
|
|
|
149
|
-
def send_excel(self, file_path: str,
|
|
149
|
+
def send_excel(self, file_path: str, bot_name: str = 'test') -> bool:
|
|
150
150
|
"""
|
|
151
151
|
发送Excel文件
|
|
152
152
|
|
|
153
153
|
Args:
|
|
154
154
|
file_path: Excel文件路径
|
|
155
|
-
|
|
155
|
+
bot_name: 群组别名,默认为'test'
|
|
156
156
|
|
|
157
157
|
Returns:
|
|
158
158
|
发送是否成功
|
|
@@ -162,9 +162,9 @@ class FeishuBot:
|
|
|
162
162
|
if not file_key:
|
|
163
163
|
return False
|
|
164
164
|
|
|
165
|
-
chat_id = self._get_chat_id(
|
|
165
|
+
chat_id = self._get_chat_id(bot_name)
|
|
166
166
|
if not chat_id:
|
|
167
|
-
lark.logger.error(f"未找到群组别名 '{
|
|
167
|
+
lark.logger.error(f"未找到群组别名 '{bot_name}' 对应的群组ID")
|
|
168
168
|
return False
|
|
169
169
|
|
|
170
170
|
message_content = {"file_key": file_key}
|
|
@@ -90,6 +90,21 @@ def set_cell_prefix_red(cell, n, color_name):
|
|
|
90
90
|
except Exception as e:
|
|
91
91
|
print(f"设置字体颜色失败: {e}")
|
|
92
92
|
|
|
93
|
+
def wrap_column(sheet, columns=None, WrapText=True):
|
|
94
|
+
if columns is None:
|
|
95
|
+
return
|
|
96
|
+
used_range_col = sheet.range('A1').expand('right')
|
|
97
|
+
for j, cell in enumerate(used_range_col):
|
|
98
|
+
col = j + 1
|
|
99
|
+
col_name = index_to_column_name(col)
|
|
100
|
+
col_val = sheet.range(f'{col_name}1').value
|
|
101
|
+
if col_val is None:
|
|
102
|
+
continue
|
|
103
|
+
for c in columns:
|
|
104
|
+
if c in col_val:
|
|
105
|
+
log(f'设置[{c}] 换行 {WrapText}')
|
|
106
|
+
sheet.range(f'{col_name}:{col_name}').api.WrapText = WrapText
|
|
107
|
+
|
|
93
108
|
def sort_by_column(data, col_index, header_rows=2, reverse=True):
|
|
94
109
|
if not data or header_rows >= len(data):
|
|
95
110
|
return data
|
|
@@ -2626,13 +2641,13 @@ def _execute_operations_batch(excel_path, operations):
|
|
|
2626
2641
|
for sheet_name, operation_type, *args in operations:
|
|
2627
2642
|
# 根据操作类型决定是否需要获取或创建工作表
|
|
2628
2643
|
sheet = None
|
|
2629
|
-
|
|
2644
|
+
|
|
2630
2645
|
# 删除操作不需要获取sheet对象
|
|
2631
2646
|
if operation_type == 'delete':
|
|
2632
2647
|
log(f'删除sheet: {sheet_name}')
|
|
2633
2648
|
delete_sheet_if_exists(wb, sheet_name)
|
|
2634
2649
|
continue
|
|
2635
|
-
|
|
2650
|
+
|
|
2636
2651
|
# 其他操作需要获取或创建工作表
|
|
2637
2652
|
if isinstance(sheet_name, str):
|
|
2638
2653
|
sheet_names = [s.name.strip().lower() for s in wb.sheets]
|
|
@@ -10,10 +10,214 @@ import numpy as np
|
|
|
10
10
|
|
|
11
11
|
class SheinExcel:
|
|
12
12
|
|
|
13
|
-
def __init__(self, config):
|
|
13
|
+
def __init__(self, config, bridge=None):
|
|
14
14
|
self.config = config
|
|
15
|
+
self.bridge = bridge
|
|
15
16
|
pass
|
|
16
17
|
|
|
18
|
+
# 退货列表
|
|
19
|
+
def write_return_list(self, erp, start_date, end_date):
|
|
20
|
+
header = ['退货单号', '退货出库时间', '签收状态', '店铺信息', '店长', '退货类型', '退货原因', 'SKC图片', 'SKC信息', '商家SKU', '属性集', 'SKU退货数量', '平台SKU', 'ERP默认供货商', 'ERP成本', '包裹名', '包裹号', '退货计划单号', '订单号', '发货单', '退回方式', '快递名称', '运单号', '退货地址', '商家联系人', '商家手机号', '入库问题图片地址']
|
|
21
|
+
excel_data = [header]
|
|
22
|
+
|
|
23
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
24
|
+
|
|
25
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_{start_date}_{end_date}.json'
|
|
26
|
+
dict = read_dict_from_file(cache_file)
|
|
27
|
+
for store_username, shein_back_list in dict.items():
|
|
28
|
+
for item in shein_back_list:
|
|
29
|
+
|
|
30
|
+
store_name = dict_store.get(store_username)
|
|
31
|
+
|
|
32
|
+
returnOrderId = item['id']
|
|
33
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_box_detail_{returnOrderId}.json'
|
|
34
|
+
return_detail = read_dict_from_file(cache_file)
|
|
35
|
+
if len(return_detail) == 0:
|
|
36
|
+
continue
|
|
37
|
+
|
|
38
|
+
returnOrderNo = item['returnOrderNo']
|
|
39
|
+
returnOrderTypeName = item['returnOrderTypeName']
|
|
40
|
+
returnOrderStatusName = item['returnOrderStatusName']
|
|
41
|
+
returnReasonTypeName = item['returnReasonTypeName']
|
|
42
|
+
returnReason = item['returnReason']
|
|
43
|
+
waitReturnQuantity = item['waitReturnQuantity']
|
|
44
|
+
skcReturnQuantity = item['returnQuantity']
|
|
45
|
+
returnAmount = item['returnAmount']
|
|
46
|
+
currencyCode = item['currencyCode']
|
|
47
|
+
returnPlanNo = item['returnPlanNo']
|
|
48
|
+
sellerOrderNo = item['sellerOrderNo']
|
|
49
|
+
sellerDeliveryNo = item['sellerDeliveryNo']
|
|
50
|
+
completeTime = item['completeTime']
|
|
51
|
+
returnWayTypeName = item['returnWayTypeName']
|
|
52
|
+
returnExpressCompanyName = item['returnExpressCompanyName']
|
|
53
|
+
expressNoList = item['expressNoList']
|
|
54
|
+
returnAddress = item['returnAddress']
|
|
55
|
+
sellerContract = item['sellerContract']
|
|
56
|
+
sellerContractPhone = item['sellerContractPhone']
|
|
57
|
+
isSign = ['已报废', '已签收', '待签收'][item['isSign']]
|
|
58
|
+
if item['returnScrapType'] == 1:
|
|
59
|
+
urls = item.get('qc_report_url', '-')
|
|
60
|
+
else:
|
|
61
|
+
urls = '\n'.join(item['rejectPicUrlList'])
|
|
62
|
+
|
|
63
|
+
for package_list in return_detail[0]['boxList']:
|
|
64
|
+
package_name = package_list['packageName']
|
|
65
|
+
package_no = package_list['returnBoxNo']
|
|
66
|
+
for skc_item in package_list['goods']:
|
|
67
|
+
skc_img = skc_item['imgPath']
|
|
68
|
+
skc = skc_item['skc']
|
|
69
|
+
supplierCode = skc_item['supplierCode']
|
|
70
|
+
for sku_item in skc_item['details']:
|
|
71
|
+
platformSku = sku_item['platformSku']
|
|
72
|
+
supplierSku = sku_item['supplierSku']
|
|
73
|
+
suffixZh = sku_item['suffixZh']
|
|
74
|
+
returnQuantity = sku_item['returnQuantity']
|
|
75
|
+
|
|
76
|
+
store_info = f'{store_username}\n{store_name}\n处理类型: {returnOrderTypeName}\n退货状态: {returnOrderStatusName}'
|
|
77
|
+
skc_info = f'SKC: {skc}\n供方货号: {supplierCode}\n预计退货数量/执行退货数量: {waitReturnQuantity}/{skcReturnQuantity}\n预计退货货值: {returnAmount} {currencyCode}'
|
|
78
|
+
|
|
79
|
+
row_item = []
|
|
80
|
+
row_item.append(returnOrderNo)
|
|
81
|
+
row_item.append(completeTime)
|
|
82
|
+
row_item.append(isSign)
|
|
83
|
+
row_item.append(store_info)
|
|
84
|
+
row_item.append(self.config.shein_store_manager.get(str(store_username).lower()))
|
|
85
|
+
row_item.append(returnReasonTypeName)
|
|
86
|
+
row_item.append(returnReason)
|
|
87
|
+
row_item.append(skc_img)
|
|
88
|
+
row_item.append(skc_info)
|
|
89
|
+
row_item.append(supplierSku)
|
|
90
|
+
row_item.append(suffixZh)
|
|
91
|
+
row_item.append(returnQuantity)
|
|
92
|
+
row_item.append(platformSku)
|
|
93
|
+
row_item.append(self.bridge.get_sku_supplier(supplierSku, erp))
|
|
94
|
+
row_item.append(self.bridge.get_sku_cost(supplierSku, erp))
|
|
95
|
+
row_item.append(package_name)
|
|
96
|
+
row_item.append(package_no)
|
|
97
|
+
row_item.append(returnPlanNo)
|
|
98
|
+
row_item.append(sellerOrderNo)
|
|
99
|
+
row_item.append(sellerDeliveryNo)
|
|
100
|
+
row_item.append(returnWayTypeName)
|
|
101
|
+
row_item.append(returnExpressCompanyName)
|
|
102
|
+
row_item.append(expressNoList)
|
|
103
|
+
row_item.append(returnAddress)
|
|
104
|
+
row_item.append(sellerContract)
|
|
105
|
+
row_item.append(sellerContractPhone)
|
|
106
|
+
row_item.append(urls)
|
|
107
|
+
|
|
108
|
+
excel_data.append(row_item)
|
|
109
|
+
|
|
110
|
+
cache_file_excel = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_excel_{start_date}_{end_date}.json'
|
|
111
|
+
write_dict_to_file(cache_file_excel, excel_data)
|
|
112
|
+
|
|
113
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_{TimeUtils.today_date()}.json'
|
|
114
|
+
dict = read_dict_from_file(cache_file)
|
|
115
|
+
for store_username, shein_back_list in dict.items():
|
|
116
|
+
for item in shein_back_list:
|
|
117
|
+
|
|
118
|
+
store_name = dict_store.get(store_username)
|
|
119
|
+
|
|
120
|
+
returnOrderId = item['id']
|
|
121
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_box_detail_{returnOrderId}.json'
|
|
122
|
+
return_detail = read_dict_from_file(cache_file)
|
|
123
|
+
if len(return_detail) == 0:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
returnOrderNo = item['returnOrderNo']
|
|
127
|
+
returnOrderTypeName = item['returnOrderTypeName']
|
|
128
|
+
returnOrderStatusName = item['returnOrderStatusName']
|
|
129
|
+
returnReasonTypeName = item['returnReasonTypeName']
|
|
130
|
+
returnReason = item['returnReason']
|
|
131
|
+
waitReturnQuantity = item['waitReturnQuantity']
|
|
132
|
+
skcReturnQuantity = item['returnQuantity']
|
|
133
|
+
returnAmount = item['returnAmount']
|
|
134
|
+
currencyCode = item['currencyCode']
|
|
135
|
+
returnPlanNo = item['returnPlanNo']
|
|
136
|
+
sellerOrderNo = item['sellerOrderNo']
|
|
137
|
+
sellerDeliveryNo = item['sellerDeliveryNo']
|
|
138
|
+
completeTime = item['completeTime']
|
|
139
|
+
returnWayTypeName = item['returnWayTypeName']
|
|
140
|
+
returnExpressCompanyName = item['returnExpressCompanyName']
|
|
141
|
+
expressNoList = item['expressNoList']
|
|
142
|
+
returnAddress = item['returnAddress']
|
|
143
|
+
sellerContract = item['sellerContract']
|
|
144
|
+
sellerContractPhone = item['sellerContractPhone']
|
|
145
|
+
isSign = ['已报废', '已签收', '待签收'][item['isSign']]
|
|
146
|
+
if item['returnScrapType'] == 1:
|
|
147
|
+
urls = item.get('qc_report_url', '-')
|
|
148
|
+
else:
|
|
149
|
+
urls = '\n'.join(item['rejectPicUrlList'])
|
|
150
|
+
|
|
151
|
+
for package_list in return_detail[0]['boxList']:
|
|
152
|
+
package_name = package_list['packageName']
|
|
153
|
+
package_no = package_list['returnBoxNo']
|
|
154
|
+
for skc_item in package_list['goods']:
|
|
155
|
+
skc_img = skc_item['imgPath']
|
|
156
|
+
skc = skc_item['skc']
|
|
157
|
+
supplierCode = skc_item['supplierCode']
|
|
158
|
+
for sku_item in skc_item['details']:
|
|
159
|
+
platformSku = sku_item['platformSku']
|
|
160
|
+
supplierSku = sku_item['supplierSku']
|
|
161
|
+
suffixZh = sku_item['suffixZh']
|
|
162
|
+
returnQuantity = sku_item['returnQuantity']
|
|
163
|
+
|
|
164
|
+
store_info = f'{store_username}\n{store_name}\n处理类型: {returnOrderTypeName}\n退货状态: {returnOrderStatusName}'
|
|
165
|
+
skc_info = f'SKC: {skc}\n供方货号: {supplierCode}\n预计退货数量/执行退货数量: {waitReturnQuantity}/{skcReturnQuantity}\n预计退货货值: {returnAmount} {currencyCode}'
|
|
166
|
+
|
|
167
|
+
row_item = []
|
|
168
|
+
row_item.append(returnOrderNo)
|
|
169
|
+
row_item.append(completeTime)
|
|
170
|
+
row_item.append(isSign)
|
|
171
|
+
row_item.append(store_info)
|
|
172
|
+
row_item.append(self.config.shein_store_manager.get(str(store_username).lower()))
|
|
173
|
+
row_item.append(returnReasonTypeName)
|
|
174
|
+
row_item.append(returnReason)
|
|
175
|
+
row_item.append(skc_img)
|
|
176
|
+
row_item.append(skc_info)
|
|
177
|
+
row_item.append(supplierSku)
|
|
178
|
+
row_item.append(suffixZh)
|
|
179
|
+
row_item.append(returnQuantity)
|
|
180
|
+
row_item.append(platformSku)
|
|
181
|
+
row_item.append(self.bridge.get_sku_supplier(supplierSku, erp))
|
|
182
|
+
row_item.append(self.bridge.get_sku_cost(supplierSku, erp))
|
|
183
|
+
row_item.append(package_name)
|
|
184
|
+
row_item.append(package_no)
|
|
185
|
+
row_item.append(returnPlanNo)
|
|
186
|
+
row_item.append(sellerOrderNo)
|
|
187
|
+
row_item.append(sellerDeliveryNo)
|
|
188
|
+
row_item.append(returnWayTypeName)
|
|
189
|
+
row_item.append(returnExpressCompanyName)
|
|
190
|
+
row_item.append(expressNoList)
|
|
191
|
+
row_item.append(returnAddress)
|
|
192
|
+
row_item.append(sellerContract)
|
|
193
|
+
row_item.append(sellerContractPhone)
|
|
194
|
+
row_item.append(urls)
|
|
195
|
+
|
|
196
|
+
excel_data.append(row_item)
|
|
197
|
+
|
|
198
|
+
sheet_name = '昨日退货列表'
|
|
199
|
+
|
|
200
|
+
cache_file_excel = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_excel_{TimeUtils.today_date()}.json'
|
|
201
|
+
write_dict_to_file(cache_file_excel, excel_data)
|
|
202
|
+
|
|
203
|
+
batch_excel_operations(self.config.excel_return_list, [
|
|
204
|
+
(sheet_name, 'write', excel_data, ['W', 'Z']),
|
|
205
|
+
(sheet_name, 'format', self.format_return_list)
|
|
206
|
+
])
|
|
207
|
+
|
|
208
|
+
def format_return_list(self, sheet):
|
|
209
|
+
merge_by_column_v2(sheet, '退货单号', ['签收状态', '店铺信息', '店长', '退货类型', '退货原因', 'SKC图片', 'SKC信息', '包裹名', '包裹号', '退货计划单号', '订单号', '发货单', '退货出库时间', '退回方式', '快递名称', '运单号', '退货地址', '商家联系人', '商家手机号', '入库问题图片地址'])
|
|
210
|
+
beautify_title(sheet)
|
|
211
|
+
add_borders(sheet)
|
|
212
|
+
format_to_datetime(sheet, ['时间'])
|
|
213
|
+
format_to_money(sheet, ['单价', '金额', '成本'])
|
|
214
|
+
column_to_right(sheet, ['单价', '金额', '成本'])
|
|
215
|
+
wrap_column(sheet, ['退货原因', '退货地址', '入库问题图片地址'])
|
|
216
|
+
autofit_column(sheet, ['店铺别名', 'SKC信息'])
|
|
217
|
+
column_to_left(sheet, ['店铺信息', '商家SKU', '供方货号', '属性集', 'SKC信息', '退货地址'])
|
|
218
|
+
specify_column_width(sheet, ['退货原因', 'SKC信息', '商家SKU', '退货地址'], 200 / 6)
|
|
219
|
+
InsertImageV2(sheet, ['SKC图片'])
|
|
220
|
+
|
|
17
221
|
def dealReturn(self, sheet):
|
|
18
222
|
# 遍历可用行
|
|
19
223
|
used_range_row = sheet.range('A1').expand('down')
|
|
@@ -1076,10 +1280,10 @@ class SheinExcel:
|
|
|
1076
1280
|
excel_path = self.config.excel_shein_finance_month_report_summary
|
|
1077
1281
|
|
|
1078
1282
|
sheet_name = '总表-算法1'
|
|
1079
|
-
|
|
1283
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
1080
1284
|
total_data = []
|
|
1081
1285
|
header = ['店铺账号', '店铺别名']
|
|
1082
|
-
for mall_id, excel_data in
|
|
1286
|
+
for mall_id, excel_data in dict_store.items():
|
|
1083
1287
|
total_data += [[mall_id, excel_data]]
|
|
1084
1288
|
|
|
1085
1289
|
log(total_data)
|
|
@@ -1099,7 +1303,7 @@ class SheinExcel:
|
|
|
1099
1303
|
filtered_value = add_suffixed_column(filtered_value, '毛利', '')
|
|
1100
1304
|
|
|
1101
1305
|
# 匹配店铺店长
|
|
1102
|
-
dict_store_manager_shein =
|
|
1306
|
+
dict_store_manager_shein = self.config.shein_store_manager
|
|
1103
1307
|
for row in filtered_value:
|
|
1104
1308
|
mall_name = row[0]
|
|
1105
1309
|
if mall_name == '店铺账号':
|
|
@@ -1118,10 +1322,10 @@ class SheinExcel:
|
|
|
1118
1322
|
wb.save()
|
|
1119
1323
|
close_excel(app, wb)
|
|
1120
1324
|
|
|
1121
|
-
|
|
1325
|
+
dict_store = read_dict_from_file(self.config.shein_store_alias)
|
|
1122
1326
|
total_data = []
|
|
1123
1327
|
header = ['店铺账号', '店铺别名']
|
|
1124
|
-
for mall_id, excel_data in
|
|
1328
|
+
for mall_id, excel_data in dict_store.items():
|
|
1125
1329
|
total_data += [[mall_id, excel_data]]
|
|
1126
1330
|
|
|
1127
1331
|
filtered_value = [header] + total_data
|
|
@@ -1139,6 +1343,7 @@ class SheinExcel:
|
|
|
1139
1343
|
filtered_value = add_suffixed_column(filtered_value, '毛利', '')
|
|
1140
1344
|
|
|
1141
1345
|
# 匹配店铺店长
|
|
1346
|
+
dict_store_manager_shein = self.config.shein_store_manager
|
|
1142
1347
|
for row in filtered_value:
|
|
1143
1348
|
mall_name = row[0]
|
|
1144
1349
|
if mall_name == '店铺账号':
|
|
@@ -1623,7 +1828,7 @@ class SheinExcel:
|
|
|
1623
1828
|
for col in columns:
|
|
1624
1829
|
column_range = sheet.range(f'{col}{start_row}:{col}{end_row}')
|
|
1625
1830
|
for cell in column_range:
|
|
1626
|
-
if cell.value is not None and
|
|
1831
|
+
if cell.value is not None and cell.value < 0:
|
|
1627
1832
|
cell.font.color = (255, 0, 0)
|
|
1628
1833
|
|
|
1629
1834
|
def _format_daily_header(self, sheet, las_row):
|
|
@@ -119,6 +119,95 @@ class SheinLib:
|
|
|
119
119
|
|
|
120
120
|
log('鉴权处理结束')
|
|
121
121
|
|
|
122
|
+
# 获取质检报告pdf地址
|
|
123
|
+
def get_qc_report_url(self, deliverCode, purchaseCode):
|
|
124
|
+
log(f'获取质检报告:{deliverCode} {purchaseCode}')
|
|
125
|
+
url = f"https://sso.geiwohuo.com/pfmp/returnPlan/queryQcReport"
|
|
126
|
+
payload = {
|
|
127
|
+
"deliverCode" : deliverCode,
|
|
128
|
+
"purchaseCode": purchaseCode
|
|
129
|
+
}
|
|
130
|
+
response_text = fetch(self.web_page, url, payload)
|
|
131
|
+
error_code = response_text.get('code')
|
|
132
|
+
if str(error_code) != '0':
|
|
133
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
134
|
+
qc_report_url = (response_text.get('info', {}).get('data') or [{'qcReportUrl': '质检报告生成中,请稍后查看'}])[0].get('qcReportUrl')
|
|
135
|
+
log(qc_report_url)
|
|
136
|
+
return qc_report_url
|
|
137
|
+
|
|
138
|
+
def get_return_order_box_detail(self, returnOrderId):
|
|
139
|
+
log(f'获取退货包裹详情: {returnOrderId}')
|
|
140
|
+
url = f"https://sso.geiwohuo.com/pfmp/returnOrder/getReturnOrderBoxDetail"
|
|
141
|
+
payload = {
|
|
142
|
+
"returnOrderId": returnOrderId,
|
|
143
|
+
"page" : 1,
|
|
144
|
+
"perPage" : 50
|
|
145
|
+
}
|
|
146
|
+
response_text = fetch(self.web_page, url, payload)
|
|
147
|
+
error_code = response_text.get('code')
|
|
148
|
+
if str(error_code) != '0':
|
|
149
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
150
|
+
list_item = response_text['info']['data']
|
|
151
|
+
|
|
152
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_box_detail_{returnOrderId}.json'
|
|
153
|
+
write_dict_to_file(cache_file, list_item)
|
|
154
|
+
|
|
155
|
+
def get_return_order_list(self, start_date, end_date, only_yesterday=1):
|
|
156
|
+
|
|
157
|
+
log(f'获取退货列表: {self.store_username} {self.store_name} {start_date} {end_date}')
|
|
158
|
+
|
|
159
|
+
page_num = 1
|
|
160
|
+
page_size = 200 # 列表最多返回200条数据 大了没有用
|
|
161
|
+
|
|
162
|
+
url = f"https://sso.geiwohuo.com/pfmp/returnOrder/page"
|
|
163
|
+
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
|
|
168
|
+
}
|
|
169
|
+
response_text = fetch(self.web_page, url, payload)
|
|
170
|
+
error_code = response_text.get('code')
|
|
171
|
+
if str(error_code) != '0':
|
|
172
|
+
raise send_exception(json.dumps(response_text, ensure_ascii=False))
|
|
173
|
+
list_item = response_text['info']['data']
|
|
174
|
+
total = response_text['info']['meta']['count']
|
|
175
|
+
totalPage = math.ceil(total / page_size)
|
|
176
|
+
|
|
177
|
+
for page in range(2, totalPage + 1):
|
|
178
|
+
log(f'获取退供列表 第{page}/{totalPage}页 共{total}条记录')
|
|
179
|
+
payload['page'] = page
|
|
180
|
+
response_text = fetch(self.web_page, url, payload)
|
|
181
|
+
spu_list_new = response_text['info']['data']
|
|
182
|
+
list_item += spu_list_new
|
|
183
|
+
time.sleep(0.1)
|
|
184
|
+
|
|
185
|
+
all_list_item = []
|
|
186
|
+
today_list_item = []
|
|
187
|
+
# 过滤 退货出库时间 是昨天的
|
|
188
|
+
for item in list_item:
|
|
189
|
+
has_valid_package = item.get('hasPackage') == 1
|
|
190
|
+
is_valid_yesterday = TimeUtils.is_yesterday(item['completeTime'], None) if item.get('completeTime') else False
|
|
191
|
+
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)
|
|
198
|
+
|
|
199
|
+
all_list_item.append(item)
|
|
200
|
+
if is_valid_yesterday:
|
|
201
|
+
today_list_item.append(item)
|
|
202
|
+
|
|
203
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_{TimeUtils.today_date()}.json'
|
|
204
|
+
write_dict_to_file_ex(cache_file, {self.store_username: today_list_item}, [self.store_username])
|
|
205
|
+
|
|
206
|
+
cache_file = f'{self.config.auto_dir}/shein/cache/shein_return_order_list_{start_date}_{end_date}.json'
|
|
207
|
+
write_dict_to_file_ex(cache_file, {self.store_username: all_list_item}, [self.store_username])
|
|
208
|
+
|
|
209
|
+
return list_item
|
|
210
|
+
|
|
122
211
|
# 获取希音退供明细 和 台账明细一个接口
|
|
123
212
|
def get_back_list(self, source='mb'):
|
|
124
213
|
page_num = 1
|
|
@@ -1693,7 +1782,8 @@ class SheinLib:
|
|
|
1693
1782
|
sale_model = spu_info['saleModel']['name']
|
|
1694
1783
|
|
|
1695
1784
|
# 过滤已经售罄
|
|
1696
|
-
if
|
|
1785
|
+
if shelf_status == 'SOLD_OUT':
|
|
1786
|
+
log(f'过滤已售罄: {skc} {category_name} {product_name}')
|
|
1697
1787
|
continue
|
|
1698
1788
|
|
|
1699
1789
|
for sku_info in spu_info['skuList']:
|
|
@@ -1,318 +1,318 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
"""
|
|
3
|
-
-------------------------------------------------
|
|
4
|
-
@version : v1.0
|
|
5
|
-
@author : qsir
|
|
6
|
-
@contact : qsir@vxnote.com
|
|
7
|
-
@software : PyCharm
|
|
8
|
-
@filename : wxwork.py
|
|
9
|
-
@create time: 2025/08/03
|
|
10
|
-
@modify time: 2025/08/03
|
|
11
|
-
@describe :
|
|
12
|
-
-------------------------------------------------
|
|
13
|
-
"""
|
|
14
|
-
import json
|
|
15
|
-
import os
|
|
16
|
-
import hashlib
|
|
17
|
-
import base64
|
|
18
|
-
import requests
|
|
19
|
-
from requests_toolbelt import MultipartEncoder
|
|
20
|
-
from datetime import datetime
|
|
21
|
-
|
|
22
|
-
# 通过企微群机器人发送消息
|
|
23
|
-
class WxWorkBot:
|
|
24
|
-
def __init__(self, key):
|
|
25
|
-
self.key = key
|
|
26
|
-
|
|
27
|
-
def upload_media(self, filepath):
|
|
28
|
-
"""
|
|
29
|
-
上传临时素材,给企微群里发文件消息时需要先将文件上传至企微临时素材中
|
|
30
|
-
:param filepath:
|
|
31
|
-
:return: 临时素材的media_id
|
|
32
|
-
"""
|
|
33
|
-
try:
|
|
34
|
-
headers = {
|
|
35
|
-
'Content-Type': 'multipart/form-data',
|
|
36
|
-
}
|
|
37
|
-
with open(filepath, 'rb') as f:
|
|
38
|
-
files = {
|
|
39
|
-
'media': (os.path.basename(filepath), f.read())
|
|
40
|
-
}
|
|
41
|
-
response = requests.post(
|
|
42
|
-
f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={self.key}&type=file',
|
|
43
|
-
headers=headers, files=files)
|
|
44
|
-
response_text = json.loads(response.text)
|
|
45
|
-
if str(response_text.get('errcode')) != '0':
|
|
46
|
-
raise Exception(response_text)
|
|
47
|
-
if response.status_code == 200:
|
|
48
|
-
result = json.loads(response.text)
|
|
49
|
-
return result['media_id']
|
|
50
|
-
else:
|
|
51
|
-
print("HTTP Error:", response.status_code)
|
|
52
|
-
return None
|
|
53
|
-
except Exception as err:
|
|
54
|
-
raise Exception("upload_media error", err)
|
|
55
|
-
|
|
56
|
-
def send_file(self, file_path):
|
|
57
|
-
if not os.path.exists(file_path):
|
|
58
|
-
print('文件不存在: ', file_path)
|
|
59
|
-
return
|
|
60
|
-
"""
|
|
61
|
-
发送文件到群里
|
|
62
|
-
:param file_path:
|
|
63
|
-
:return:
|
|
64
|
-
"""
|
|
65
|
-
media_id = self.upload_media(file_path)
|
|
66
|
-
data = {
|
|
67
|
-
"msgtype": "file",
|
|
68
|
-
"file" : {
|
|
69
|
-
"media_id": media_id
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return self.send_msg(data)
|
|
73
|
-
|
|
74
|
-
def send_text(self, content, mentioned_list=None, mentioned_mobile_list=None):
|
|
75
|
-
"""
|
|
76
|
-
发送文本消息
|
|
77
|
-
:param content:
|
|
78
|
-
:param mentioned_list: 需要@的人userid
|
|
79
|
-
:param mentioned_mobile_list: 需要@的人手机号
|
|
80
|
-
:return:
|
|
81
|
-
"""
|
|
82
|
-
data = {
|
|
83
|
-
"msgtype": "text",
|
|
84
|
-
"text" : {
|
|
85
|
-
"content": content
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if mentioned_list is not None and mentioned_list:
|
|
89
|
-
data['text'].update({"mentioned_list": mentioned_list})
|
|
90
|
-
if mentioned_mobile_list is not None and mentioned_mobile_list:
|
|
91
|
-
data['text'].update({"mentioned_mobile_list": mentioned_mobile_list})
|
|
92
|
-
|
|
93
|
-
self.send_msg(data)
|
|
94
|
-
|
|
95
|
-
def send_markdown(self, content):
|
|
96
|
-
"""
|
|
97
|
-
发送Markdown消息
|
|
98
|
-
:param content:
|
|
99
|
-
:return:
|
|
100
|
-
"""
|
|
101
|
-
data = {
|
|
102
|
-
"msgtype" : "markdown",
|
|
103
|
-
"markdown": {
|
|
104
|
-
"content": content
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
self.send_msg(data)
|
|
108
|
-
|
|
109
|
-
def send_notify(self, title, sub_title_list, data_list):
|
|
110
|
-
"""
|
|
111
|
-
发送Markdown消息
|
|
112
|
-
:param content:
|
|
113
|
-
:return:
|
|
114
|
-
"""
|
|
115
|
-
|
|
116
|
-
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
117
|
-
header = f"{current_date} {title}\n\n"
|
|
118
|
-
|
|
119
|
-
arr_color = ['warning', 'info', 'warning']
|
|
120
|
-
arr_sub_header = [f"<font color='{arr_color[index]}'>{title}</font>" for index, title in enumerate(sub_title_list)]
|
|
121
|
-
sub_header = "\t".join(arr_sub_header) + "\n\n"
|
|
122
|
-
|
|
123
|
-
# 获取每个元素的行索引和列索引
|
|
124
|
-
arr_content = [
|
|
125
|
-
[
|
|
126
|
-
f'{value}' if col_idx == 0 else f"<font color='{arr_color[col_idx - 1]}'>{value}</font>"
|
|
127
|
-
for col_idx, value in enumerate(row)
|
|
128
|
-
] # 每行的元素组成一个子列表
|
|
129
|
-
for row_idx, row in enumerate(data_list) # 外层循环控制行
|
|
130
|
-
]
|
|
131
|
-
# 将二维数组转换为字符串
|
|
132
|
-
content = "\n".join(
|
|
133
|
-
# 对每行的元素列表使用 join(),用 \t 连接
|
|
134
|
-
"\t".join(row) for row in arr_content
|
|
135
|
-
)
|
|
136
|
-
|
|
137
|
-
data = {
|
|
138
|
-
"msgtype" : "markdown",
|
|
139
|
-
"markdown": {
|
|
140
|
-
"content": header + sub_header + content
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
self.send_msg(data)
|
|
144
|
-
|
|
145
|
-
def send_img(self, img_path):
|
|
146
|
-
"""
|
|
147
|
-
发送图片消息
|
|
148
|
-
图片(base64编码前)最大不能超过2M,支持JPG,PNG格式
|
|
149
|
-
:param img_path:
|
|
150
|
-
:return:
|
|
151
|
-
"""
|
|
152
|
-
data = {
|
|
153
|
-
"msgtype": "image",
|
|
154
|
-
"image" : {
|
|
155
|
-
"base64": self.img_to_base64(img_path),
|
|
156
|
-
"md5" : self.img_to_md5(img_path)
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
self.send_msg(data)
|
|
160
|
-
|
|
161
|
-
def send_news(self, title, description, url, picurl):
|
|
162
|
-
"""
|
|
163
|
-
发送图文消息
|
|
164
|
-
:param title: 标题
|
|
165
|
-
:param description: 描述
|
|
166
|
-
:param url: 跳转URL
|
|
167
|
-
:param picurl: 图文图片地址
|
|
168
|
-
:return:
|
|
169
|
-
"""
|
|
170
|
-
data = {
|
|
171
|
-
"msgtype": "news",
|
|
172
|
-
"news" : {
|
|
173
|
-
"articles": [
|
|
174
|
-
{
|
|
175
|
-
"title" : title,
|
|
176
|
-
"description": description,
|
|
177
|
-
"url" : url,
|
|
178
|
-
"picurl" : picurl
|
|
179
|
-
}
|
|
180
|
-
]
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
self.send_msg(data)
|
|
184
|
-
|
|
185
|
-
def send_msg(self, data):
|
|
186
|
-
"""
|
|
187
|
-
发送机器人通用消息到企微群
|
|
188
|
-
:param data: 消息内容json数据
|
|
189
|
-
:return:
|
|
190
|
-
"""
|
|
191
|
-
try:
|
|
192
|
-
header = {
|
|
193
|
-
"Content-Type": "application/json"
|
|
194
|
-
}
|
|
195
|
-
response = requests.post(f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.key}", headers=header, data=json.dumps(data))
|
|
196
|
-
response_text = json.loads(response.text)
|
|
197
|
-
if str(response_text.get('errcode')) != '0':
|
|
198
|
-
raise Exception(response_text)
|
|
199
|
-
if response.status_code == 200:
|
|
200
|
-
result = json.loads(response.text)
|
|
201
|
-
return result
|
|
202
|
-
else:
|
|
203
|
-
print("HTTP Error:", response.status_code)
|
|
204
|
-
return None
|
|
205
|
-
except Exception as err:
|
|
206
|
-
raise Exception("Send Chat Message error", err)
|
|
207
|
-
|
|
208
|
-
def img_to_md5(self, img_path):
|
|
209
|
-
# 读取图片文件并计算MD5值
|
|
210
|
-
with open(img_path, 'rb') as image_file:
|
|
211
|
-
image_data = image_file.read()
|
|
212
|
-
return hashlib.md5(image_data).hexdigest()
|
|
213
|
-
|
|
214
|
-
def img_to_base64(self, img_path):
|
|
215
|
-
# 读取图片文件并转换为Base64编码
|
|
216
|
-
with open(img_path, 'rb') as image_file:
|
|
217
|
-
image_data = image_file.read()
|
|
218
|
-
return base64.b64encode(image_data).decode('utf-8')
|
|
219
|
-
|
|
220
|
-
# 通过企微应用发送消息
|
|
221
|
-
class WxWorkAppBot:
|
|
222
|
-
def __init__(self, corpid, corpsecret, agentid):
|
|
223
|
-
self.corpid = corpid
|
|
224
|
-
self.corpsecret = corpsecret
|
|
225
|
-
self.agentid = agentid
|
|
226
|
-
self.access_token = self._getToken()
|
|
227
|
-
|
|
228
|
-
def _getToken(self):
|
|
229
|
-
try:
|
|
230
|
-
if all([self.corpid, self.corpsecret]):
|
|
231
|
-
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}".format(
|
|
232
|
-
corpid=self.corpid, corpsecret=self.corpsecret)
|
|
233
|
-
response = requests.get(url)
|
|
234
|
-
if response.status_code == 200:
|
|
235
|
-
result = json.loads(response.text)
|
|
236
|
-
return result['access_token']
|
|
237
|
-
else:
|
|
238
|
-
print("HTTP Error:", response.status_code)
|
|
239
|
-
return None
|
|
240
|
-
except Exception as err:
|
|
241
|
-
raise Exception("get WeChat access Token error", err)
|
|
242
|
-
|
|
243
|
-
def _send_msg(self, data):
|
|
244
|
-
self._check_token()
|
|
245
|
-
try:
|
|
246
|
-
send_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}".format(
|
|
247
|
-
access_token=self.access_token)
|
|
248
|
-
response = requests.post(send_url, json.dumps(data))
|
|
249
|
-
if response.status_code == 200:
|
|
250
|
-
result = json.loads(response.text)
|
|
251
|
-
return result
|
|
252
|
-
else:
|
|
253
|
-
print("HTTP Error:", response.status_code)
|
|
254
|
-
return None
|
|
255
|
-
except Exception as err:
|
|
256
|
-
raise Exception("send WeChat Message error", err)
|
|
257
|
-
|
|
258
|
-
def _check_token(self):
|
|
259
|
-
if self.access_token is None:
|
|
260
|
-
self._getToken()
|
|
261
|
-
|
|
262
|
-
def send_msg(self, data):
|
|
263
|
-
return self._send_msg(data)
|
|
264
|
-
|
|
265
|
-
def upload_media(self, filetype, filepath, filename):
|
|
266
|
-
"""
|
|
267
|
-
上传临时素材到企微并获取media_id
|
|
268
|
-
:param filetype: 图片(image)、语音(voice)、视频(video),普通文件(file)
|
|
269
|
-
:param filepath:
|
|
270
|
-
:param filename:
|
|
271
|
-
:return: media_id
|
|
272
|
-
"""
|
|
273
|
-
try:
|
|
274
|
-
self._check_token()
|
|
275
|
-
post_file_url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type={filetype}".format(
|
|
276
|
-
filetype=filetype,
|
|
277
|
-
access_token=self.access_token)
|
|
278
|
-
|
|
279
|
-
m = MultipartEncoder(
|
|
280
|
-
fields={filename: (filename, open(filepath + filename, 'rb'), 'text/plain')},
|
|
281
|
-
)
|
|
282
|
-
response = requests.post(url=post_file_url, data=m, headers={'Content-Type': m.content_type})
|
|
283
|
-
if response.status_code == 200:
|
|
284
|
-
result = json.loads(response.text)
|
|
285
|
-
return result['media_id']
|
|
286
|
-
else:
|
|
287
|
-
print("HTTP Error:", response.status_code)
|
|
288
|
-
return None
|
|
289
|
-
except Exception as err:
|
|
290
|
-
raise Exception("upload media error", err)
|
|
291
|
-
|
|
292
|
-
def get_media(self, media_id):
|
|
293
|
-
"""
|
|
294
|
-
获取临时素材
|
|
295
|
-
:param media_id:
|
|
296
|
-
:return: 返回二进制形式
|
|
297
|
-
"""
|
|
298
|
-
try:
|
|
299
|
-
self._check_token()
|
|
300
|
-
url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"
|
|
301
|
-
params = {
|
|
302
|
-
"access_token": self.access_token,
|
|
303
|
-
"media_id" : media_id
|
|
304
|
-
}
|
|
305
|
-
response = requests.get(url=url, params=params)
|
|
306
|
-
if response.status_code == 200:
|
|
307
|
-
content_type = response.headers.get('Content-Type')
|
|
308
|
-
if content_type == 'application/json':
|
|
309
|
-
response_data = json.loads(response.text)
|
|
310
|
-
print("Error:", response_data.get("errmsg"))
|
|
311
|
-
return None
|
|
312
|
-
else:
|
|
313
|
-
return response.content
|
|
314
|
-
else:
|
|
315
|
-
print("HTTP Error:", response.status_code)
|
|
316
|
-
return None
|
|
317
|
-
except Exception as err:
|
|
318
|
-
raise Exception("get media error", err)
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
-------------------------------------------------
|
|
4
|
+
@version : v1.0
|
|
5
|
+
@author : qsir
|
|
6
|
+
@contact : qsir@vxnote.com
|
|
7
|
+
@software : PyCharm
|
|
8
|
+
@filename : wxwork.py
|
|
9
|
+
@create time: 2025/08/03
|
|
10
|
+
@modify time: 2025/08/03
|
|
11
|
+
@describe :
|
|
12
|
+
-------------------------------------------------
|
|
13
|
+
"""
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import hashlib
|
|
17
|
+
import base64
|
|
18
|
+
import requests
|
|
19
|
+
from requests_toolbelt import MultipartEncoder
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
|
|
22
|
+
# 通过企微群机器人发送消息
|
|
23
|
+
class WxWorkBot:
|
|
24
|
+
def __init__(self, key):
|
|
25
|
+
self.key = key
|
|
26
|
+
|
|
27
|
+
def upload_media(self, filepath):
|
|
28
|
+
"""
|
|
29
|
+
上传临时素材,给企微群里发文件消息时需要先将文件上传至企微临时素材中
|
|
30
|
+
:param filepath:
|
|
31
|
+
:return: 临时素材的media_id
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
headers = {
|
|
35
|
+
'Content-Type': 'multipart/form-data',
|
|
36
|
+
}
|
|
37
|
+
with open(filepath, 'rb') as f:
|
|
38
|
+
files = {
|
|
39
|
+
'media': (os.path.basename(filepath), f.read())
|
|
40
|
+
}
|
|
41
|
+
response = requests.post(
|
|
42
|
+
f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key={self.key}&type=file',
|
|
43
|
+
headers=headers, files=files)
|
|
44
|
+
response_text = json.loads(response.text)
|
|
45
|
+
if str(response_text.get('errcode')) != '0':
|
|
46
|
+
raise Exception(response_text)
|
|
47
|
+
if response.status_code == 200:
|
|
48
|
+
result = json.loads(response.text)
|
|
49
|
+
return result['media_id']
|
|
50
|
+
else:
|
|
51
|
+
print("HTTP Error:", response.status_code)
|
|
52
|
+
return None
|
|
53
|
+
except Exception as err:
|
|
54
|
+
raise Exception("upload_media error", err)
|
|
55
|
+
|
|
56
|
+
def send_file(self, file_path):
|
|
57
|
+
if not os.path.exists(file_path):
|
|
58
|
+
print('文件不存在: ', file_path)
|
|
59
|
+
return
|
|
60
|
+
"""
|
|
61
|
+
发送文件到群里
|
|
62
|
+
:param file_path:
|
|
63
|
+
:return:
|
|
64
|
+
"""
|
|
65
|
+
media_id = self.upload_media(file_path)
|
|
66
|
+
data = {
|
|
67
|
+
"msgtype": "file",
|
|
68
|
+
"file" : {
|
|
69
|
+
"media_id": media_id
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return self.send_msg(data)
|
|
73
|
+
|
|
74
|
+
def send_text(self, content, mentioned_list=None, mentioned_mobile_list=None):
|
|
75
|
+
"""
|
|
76
|
+
发送文本消息
|
|
77
|
+
:param content:
|
|
78
|
+
:param mentioned_list: 需要@的人userid
|
|
79
|
+
:param mentioned_mobile_list: 需要@的人手机号
|
|
80
|
+
:return:
|
|
81
|
+
"""
|
|
82
|
+
data = {
|
|
83
|
+
"msgtype": "text",
|
|
84
|
+
"text" : {
|
|
85
|
+
"content": content
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if mentioned_list is not None and mentioned_list:
|
|
89
|
+
data['text'].update({"mentioned_list": mentioned_list})
|
|
90
|
+
if mentioned_mobile_list is not None and mentioned_mobile_list:
|
|
91
|
+
data['text'].update({"mentioned_mobile_list": mentioned_mobile_list})
|
|
92
|
+
|
|
93
|
+
self.send_msg(data)
|
|
94
|
+
|
|
95
|
+
def send_markdown(self, content):
|
|
96
|
+
"""
|
|
97
|
+
发送Markdown消息
|
|
98
|
+
:param content:
|
|
99
|
+
:return:
|
|
100
|
+
"""
|
|
101
|
+
data = {
|
|
102
|
+
"msgtype" : "markdown",
|
|
103
|
+
"markdown": {
|
|
104
|
+
"content": content
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
self.send_msg(data)
|
|
108
|
+
|
|
109
|
+
def send_notify(self, title, sub_title_list, data_list):
|
|
110
|
+
"""
|
|
111
|
+
发送Markdown消息
|
|
112
|
+
:param content:
|
|
113
|
+
:return:
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
current_date = datetime.now().strftime("%Y-%m-%d")
|
|
117
|
+
header = f"{current_date} {title}\n\n"
|
|
118
|
+
|
|
119
|
+
arr_color = ['warning', 'info', 'warning']
|
|
120
|
+
arr_sub_header = [f"<font color='{arr_color[index]}'>{title}</font>" for index, title in enumerate(sub_title_list)]
|
|
121
|
+
sub_header = "\t".join(arr_sub_header) + "\n\n"
|
|
122
|
+
|
|
123
|
+
# 获取每个元素的行索引和列索引
|
|
124
|
+
arr_content = [
|
|
125
|
+
[
|
|
126
|
+
f'{value}' if col_idx == 0 else f"<font color='{arr_color[col_idx - 1]}'>{value}</font>"
|
|
127
|
+
for col_idx, value in enumerate(row)
|
|
128
|
+
] # 每行的元素组成一个子列表
|
|
129
|
+
for row_idx, row in enumerate(data_list) # 外层循环控制行
|
|
130
|
+
]
|
|
131
|
+
# 将二维数组转换为字符串
|
|
132
|
+
content = "\n".join(
|
|
133
|
+
# 对每行的元素列表使用 join(),用 \t 连接
|
|
134
|
+
"\t".join(row) for row in arr_content
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
data = {
|
|
138
|
+
"msgtype" : "markdown",
|
|
139
|
+
"markdown": {
|
|
140
|
+
"content": header + sub_header + content
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
self.send_msg(data)
|
|
144
|
+
|
|
145
|
+
def send_img(self, img_path):
|
|
146
|
+
"""
|
|
147
|
+
发送图片消息
|
|
148
|
+
图片(base64编码前)最大不能超过2M,支持JPG,PNG格式
|
|
149
|
+
:param img_path:
|
|
150
|
+
:return:
|
|
151
|
+
"""
|
|
152
|
+
data = {
|
|
153
|
+
"msgtype": "image",
|
|
154
|
+
"image" : {
|
|
155
|
+
"base64": self.img_to_base64(img_path),
|
|
156
|
+
"md5" : self.img_to_md5(img_path)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
self.send_msg(data)
|
|
160
|
+
|
|
161
|
+
def send_news(self, title, description, url, picurl):
|
|
162
|
+
"""
|
|
163
|
+
发送图文消息
|
|
164
|
+
:param title: 标题
|
|
165
|
+
:param description: 描述
|
|
166
|
+
:param url: 跳转URL
|
|
167
|
+
:param picurl: 图文图片地址
|
|
168
|
+
:return:
|
|
169
|
+
"""
|
|
170
|
+
data = {
|
|
171
|
+
"msgtype": "news",
|
|
172
|
+
"news" : {
|
|
173
|
+
"articles": [
|
|
174
|
+
{
|
|
175
|
+
"title" : title,
|
|
176
|
+
"description": description,
|
|
177
|
+
"url" : url,
|
|
178
|
+
"picurl" : picurl
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
self.send_msg(data)
|
|
184
|
+
|
|
185
|
+
def send_msg(self, data):
|
|
186
|
+
"""
|
|
187
|
+
发送机器人通用消息到企微群
|
|
188
|
+
:param data: 消息内容json数据
|
|
189
|
+
:return:
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
header = {
|
|
193
|
+
"Content-Type": "application/json"
|
|
194
|
+
}
|
|
195
|
+
response = requests.post(f"https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key={self.key}", headers=header, data=json.dumps(data))
|
|
196
|
+
response_text = json.loads(response.text)
|
|
197
|
+
if str(response_text.get('errcode')) != '0':
|
|
198
|
+
raise Exception(response_text)
|
|
199
|
+
if response.status_code == 200:
|
|
200
|
+
result = json.loads(response.text)
|
|
201
|
+
return result
|
|
202
|
+
else:
|
|
203
|
+
print("HTTP Error:", response.status_code)
|
|
204
|
+
return None
|
|
205
|
+
except Exception as err:
|
|
206
|
+
raise Exception("Send Chat Message error", err)
|
|
207
|
+
|
|
208
|
+
def img_to_md5(self, img_path):
|
|
209
|
+
# 读取图片文件并计算MD5值
|
|
210
|
+
with open(img_path, 'rb') as image_file:
|
|
211
|
+
image_data = image_file.read()
|
|
212
|
+
return hashlib.md5(image_data).hexdigest()
|
|
213
|
+
|
|
214
|
+
def img_to_base64(self, img_path):
|
|
215
|
+
# 读取图片文件并转换为Base64编码
|
|
216
|
+
with open(img_path, 'rb') as image_file:
|
|
217
|
+
image_data = image_file.read()
|
|
218
|
+
return base64.b64encode(image_data).decode('utf-8')
|
|
219
|
+
|
|
220
|
+
# 通过企微应用发送消息
|
|
221
|
+
class WxWorkAppBot:
|
|
222
|
+
def __init__(self, corpid, corpsecret, agentid):
|
|
223
|
+
self.corpid = corpid
|
|
224
|
+
self.corpsecret = corpsecret
|
|
225
|
+
self.agentid = agentid
|
|
226
|
+
self.access_token = self._getToken()
|
|
227
|
+
|
|
228
|
+
def _getToken(self):
|
|
229
|
+
try:
|
|
230
|
+
if all([self.corpid, self.corpsecret]):
|
|
231
|
+
url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={corpid}&corpsecret={corpsecret}".format(
|
|
232
|
+
corpid=self.corpid, corpsecret=self.corpsecret)
|
|
233
|
+
response = requests.get(url)
|
|
234
|
+
if response.status_code == 200:
|
|
235
|
+
result = json.loads(response.text)
|
|
236
|
+
return result['access_token']
|
|
237
|
+
else:
|
|
238
|
+
print("HTTP Error:", response.status_code)
|
|
239
|
+
return None
|
|
240
|
+
except Exception as err:
|
|
241
|
+
raise Exception("get WeChat access Token error", err)
|
|
242
|
+
|
|
243
|
+
def _send_msg(self, data):
|
|
244
|
+
self._check_token()
|
|
245
|
+
try:
|
|
246
|
+
send_url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}".format(
|
|
247
|
+
access_token=self.access_token)
|
|
248
|
+
response = requests.post(send_url, json.dumps(data))
|
|
249
|
+
if response.status_code == 200:
|
|
250
|
+
result = json.loads(response.text)
|
|
251
|
+
return result
|
|
252
|
+
else:
|
|
253
|
+
print("HTTP Error:", response.status_code)
|
|
254
|
+
return None
|
|
255
|
+
except Exception as err:
|
|
256
|
+
raise Exception("send WeChat Message error", err)
|
|
257
|
+
|
|
258
|
+
def _check_token(self):
|
|
259
|
+
if self.access_token is None:
|
|
260
|
+
self._getToken()
|
|
261
|
+
|
|
262
|
+
def send_msg(self, data):
|
|
263
|
+
return self._send_msg(data)
|
|
264
|
+
|
|
265
|
+
def upload_media(self, filetype, filepath, filename):
|
|
266
|
+
"""
|
|
267
|
+
上传临时素材到企微并获取media_id
|
|
268
|
+
:param filetype: 图片(image)、语音(voice)、视频(video),普通文件(file)
|
|
269
|
+
:param filepath:
|
|
270
|
+
:param filename:
|
|
271
|
+
:return: media_id
|
|
272
|
+
"""
|
|
273
|
+
try:
|
|
274
|
+
self._check_token()
|
|
275
|
+
post_file_url = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token={access_token}&type={filetype}".format(
|
|
276
|
+
filetype=filetype,
|
|
277
|
+
access_token=self.access_token)
|
|
278
|
+
|
|
279
|
+
m = MultipartEncoder(
|
|
280
|
+
fields={filename: (filename, open(filepath + filename, 'rb'), 'text/plain')},
|
|
281
|
+
)
|
|
282
|
+
response = requests.post(url=post_file_url, data=m, headers={'Content-Type': m.content_type})
|
|
283
|
+
if response.status_code == 200:
|
|
284
|
+
result = json.loads(response.text)
|
|
285
|
+
return result['media_id']
|
|
286
|
+
else:
|
|
287
|
+
print("HTTP Error:", response.status_code)
|
|
288
|
+
return None
|
|
289
|
+
except Exception as err:
|
|
290
|
+
raise Exception("upload media error", err)
|
|
291
|
+
|
|
292
|
+
def get_media(self, media_id):
|
|
293
|
+
"""
|
|
294
|
+
获取临时素材
|
|
295
|
+
:param media_id:
|
|
296
|
+
:return: 返回二进制形式
|
|
297
|
+
"""
|
|
298
|
+
try:
|
|
299
|
+
self._check_token()
|
|
300
|
+
url = "https://qyapi.weixin.qq.com/cgi-bin/media/get"
|
|
301
|
+
params = {
|
|
302
|
+
"access_token": self.access_token,
|
|
303
|
+
"media_id" : media_id
|
|
304
|
+
}
|
|
305
|
+
response = requests.get(url=url, params=params)
|
|
306
|
+
if response.status_code == 200:
|
|
307
|
+
content_type = response.headers.get('Content-Type')
|
|
308
|
+
if content_type == 'application/json':
|
|
309
|
+
response_data = json.loads(response.text)
|
|
310
|
+
print("Error:", response_data.get("errmsg"))
|
|
311
|
+
return None
|
|
312
|
+
else:
|
|
313
|
+
return response.content
|
|
314
|
+
else:
|
|
315
|
+
print("HTTP Error:", response.status_code)
|
|
316
|
+
return None
|
|
317
|
+
except Exception as err:
|
|
318
|
+
raise Exception("get media error", err)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|