Gabriel00A commited on
Commit
aa040e6
·
verified ·
1 Parent(s): 56d718d

Update process_report.py

Browse files
Files changed (1) hide show
  1. process_report.py +57 -173
process_report.py CHANGED
@@ -13,8 +13,7 @@ import json
13
  import base64
14
  from io import BytesIO
15
  from typing import Optional, Tuple, List
16
-
17
- from datetime import datetime, date, timedelta
18
 
19
  import pandas as pd
20
  import requests
@@ -30,15 +29,14 @@ FROM_EMAIL = os.environ.get("FROM_EMAIL")
30
  TO_EMAIL = os.environ.get("TO_EMAIL")
31
  TIMEZONE = os.environ.get("TZ", "Asia/Shanghai")
32
 
33
- # ====== 业务相关字段(尽量兼容括号全角/半角差异)======
34
- # 一些系统表可能列名略有差异,这里做一个“同义列名”匹配表
35
  ALIASES = {
36
  "请购日期": ["请购日期", "请购日", "申请日期"],
37
  "请购单号": ["请购单号", "请购单编号", "申请单号"],
38
  "物料编码": ["物料编码", "物料号", "物料代码"],
39
  "物料名称": ["物料名称", "品名", "名称"],
40
  "纱支密度": ["纱支密度", "纱支/密度", "纱支 密度"],
41
- "门幅(CM)": ["门幅(CM)", "门幅(CM)", "门幅cm", "门幅"], # 已修正
42
  "颜色": ["颜色", "色号/颜色", "色号"],
43
  "主单位": ["主单位", "单位"],
44
  "主数量": ["主数量", "数量", "请购数量"],
@@ -51,31 +49,16 @@ ALIASES = {
51
  "计划到货日期": ["计划到货日期", "预计到货日期", "承诺到货日期", "计划到货日"],
52
  }
53
 
54
- # “邮件发送的格式”列顺序,如果检测到模板文件,会按模板优先排序
55
  EMAIL_COLS_DEFAULT = [
56
- "请购日期","请购单号","物料编码","物料名称","纱支密度","门幅(CM)","颜色","主单位", # 已修正
57
  "主数量","需求日期","供应商","到货日期","到货主数量","入库日期","入库主数量","目前进度"
58
  ]
59
 
60
- TEMPLATE_CANDIDATES = [
61
- # 若你把模板Excel放进仓库根目录或 templates 目录,可被自动识别
62
- "/workspace/邮件发送的格式.xlsx",
63
- "/workspace/templates/邮件发送的格式.xlsx",
64
- "/app/邮件发送的格式.xlsx",
65
- "/app/templates/邮件发送的格式.xlsx",
66
- ]
67
-
68
-
69
  # ====== 工具函数 ======
70
  def _today() -> date:
71
- # 用本地系统日期即可(Space容器时区一般是UTC;你可在 Space 里设 TZ 环境变量 + tzdata 以保证正确)
72
  return datetime.now().date()
73
 
74
-
75
  def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
76
- """
77
- 将 DataFrame 的列名映射到标准中文名(按 ALIASES)。
78
- """
79
  mapped = {}
80
  for std_name, variants in ALIASES.items():
81
  for v in df.columns:
@@ -86,12 +69,7 @@ def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
86
  df = df.rename(columns=mapped)
87
  return df
88
 
89
-
90
  def _find_header_row(path: str, must_have: List[str] = None, try_rows: int = 10) -> int:
91
- """
92
- 尝试在前 try_rows 行中找到包含关键列(如“物料编码”、“主数量”)的表头行。
93
- 找不到则返回 0。
94
- """
95
  must_have = must_have or ["物料编码", "主数量"]
96
  for r in range(try_rows):
97
  try:
@@ -103,23 +81,16 @@ def _find_header_row(path: str, must_have: List[str] = None, try_rows: int = 10)
103
  return r
104
  return 0
105
 
106
-
107
  def read_system_export(path: str) -> pd.DataFrame:
108
- """
109
- 读取“系统导出格式”Excel,并做列名标准化、空列丢弃、日期/数字类型转换。
110
- """
111
  header_row = _find_header_row(path)
112
  try:
113
  df = pd.read_excel(path, header=header_row)
114
  except Exception:
115
  df = pd.read_excel(path, header=0)
116
 
117
- # 丢掉全空列
118
  df = df.dropna(axis=1, how="all")
119
- # 标准化列名
120
  df = _normalize_columns(df)
121
 
122
- # --- 筛选逻辑 ---
123
  if "物料名称" in df.columns:
124
  df["物料名称"] = df["物料名称"].astype(str).fillna('')
125
  contains_e = df["物料名称"].str.contains("鹅")
@@ -128,86 +99,59 @@ def read_system_export(path: str) -> pd.DataFrame:
128
  contains_huazhu_special = df["物料名称"].str.contains("华住专用")
129
  to_remove = (contains_e | contains_ya | contains_huazhu) & ~contains_huazhu_special
130
  df = df[~to_remove]
131
- # --- 筛选逻辑结束 ---
132
 
133
- # 转日期
134
  for c in ["请购日期","需求日期","到货日期","入库日期","计划到货日期"]:
135
  if c in df.columns:
136
  df[c] = pd.to_datetime(df[c], errors="coerce")
137
-
138
- # 转数字
139
  for c in ["主数量","到货主数量","入库主数量"]:
140
  if c in df.columns:
141
  df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0)
142
 
143
- # 去掉全空行
144
  df = df.dropna(how="all")
145
  return df
146
 
147
-
148
  def _first_nonnull(series: pd.Series):
149
- for v in series:
150
- if pd.notna(v):
151
- return v
152
- return None
153
-
154
 
155
  def aggregate_for_email(df: pd.DataFrame) -> pd.DataFrame:
156
- """
157
- 业务汇总规则
158
- """
159
  group_keys = [k for k in [
160
- "请购单号","物料编码","物料名称","纱支密度","门幅(CM)","颜色","主单位","供应商" # 已修正
161
  ] if k in df.columns]
162
 
163
  if not group_keys:
164
  raise RuntimeError("找不到用于分组的关键字段(如 请购单号/物料编码 等),请检查导入的表头。")
165
 
166
- agg_map = {}
167
- if "主数量" in df.columns:
168
- agg_map["主数量"] = _first_nonnull
169
- if "请购日期" in df.columns:
170
- agg_map["请购日期"] = _first_nonnull
171
- if "需求日期" in df.columns:
172
- agg_map["需求日期"] = _first_nonnull
173
- if "到货日期" in df.columns:
174
- agg_map["到货日期"] = "max"
175
- if "入库日期" in df.columns:
176
- agg_map["入库日期"] = "max"
177
- if "计划到货日期" in df.columns:
178
- agg_map["计划到货日期"] = "max"
179
- if "到货主数量" in df.columns:
180
- agg_map["到货主数量"] = "sum"
181
- if "入库主数量" in df.columns:
182
- agg_map["入库主数量"] = "sum"
183
-
184
- grouped = df.groupby(group_keys, dropna=False).agg(agg_map).reset_index()
185
-
186
- # 计算 “目前进度”
187
- grouped["目前进度"] = grouped.apply(_calc_progress_row, axis=1)
188
 
189
- # --- 新增修改 1:移除“计划到货日期”列 ---
190
- # 在计算完“目前进度”后,再从DataFrame中移除此列
 
191
  if "计划到货日期" in grouped.columns:
192
  grouped = grouped.drop(columns=["计划到货日期"])
193
 
194
- # 最终列顺序:优先按模板,其次按默认顺序
195
- final = _order_like_template(grouped)
196
-
197
- # --- 新增修改 2:格式化所有日期列为短日期 ---
198
  date_cols_to_format = ["请购日期", "需求日期", "到货日期", "入库日期"]
199
  for col in date_cols_to_format:
200
  if col in final.columns:
201
- # NaT值(空日期)会被保留,非空日期会被格式化为 YYYY/M/D
202
  final[col] = pd.to_datetime(final[col], errors='coerce').dt.strftime('%Y/%-m/%-d')
203
 
204
  return final
205
 
206
-
207
  def _calc_progress_row(row: pd.Series) -> str:
208
- """
209
- 目前进度的业务口径
210
- """
211
  today = pd.Timestamp(_today())
212
  main_qty = float(row.get("主数量", 0) or 0)
213
  arr_qty = float(row.get("到货主数量", 0) or 0)
@@ -215,7 +159,9 @@ def _calc_progress_row(row: pd.Series) -> str:
215
  arrival_date = row.get("到货日期", pd.NaT)
216
  plan_arrival = row.get("计划到货日期", pd.NaT)
217
 
218
- if main_qty > 0 and arr_qty >= main_qty:
 
 
219
  return "完全到货"
220
 
221
  parts: List[str] = []
@@ -223,9 +169,8 @@ def _calc_progress_row(row: pd.Series) -> str:
223
 
224
  if arr_qty > 0:
225
  parts.append(f"部分到货 缺货{shortage:g}米")
226
- else:
227
- if pd.isna(arrival_date) or arr_qty == 0:
228
- parts.append("未到货")
229
 
230
  if (pd.isna(arrival_date) or arr_qty < main_qty) and pd.notna(demand_date):
231
  overdue_days = (today - demand_date).days
@@ -244,30 +189,6 @@ def _calc_progress_row(row: pd.Series) -> str:
244
 
245
  return ";".join(parts)
246
 
247
-
248
- def _order_like_template(df: pd.DataFrame) -> pd.DataFrame:
249
- """
250
- 若能找到“邮件发送的格式.xlsx”,按其表头顺序输出;否则用 EMAIL_COLS_DEFAULT。
251
- """
252
- template_cols = None
253
- for p in TEMPLATE_CANDIDATES:
254
- if os.path.exists(p):
255
- try:
256
- tdf = pd.read_excel(p, nrows=0)
257
- template_cols = list(map(str, tdf.columns))
258
- break
259
- except Exception:
260
- continue
261
-
262
- if template_cols is None:
263
- template_cols = EMAIL_COLS_DEFAULT
264
-
265
- front = [c for c in template_cols if c in df.columns]
266
- tail = [c for c in df.columns if c not in front]
267
- cols = front + tail
268
- return df[cols].copy()
269
-
270
-
271
  def _find_latest_input(input_dir: str) -> Optional[str]:
272
  files = []
273
  for pat in ("*.xlsx", "*.xls"):
@@ -277,70 +198,42 @@ def _find_latest_input(input_dir: str) -> Optional[str]:
277
  files.sort(key=os.path.getmtime, reverse=True)
278
  return files[0]
279
 
280
-
281
  def _df_to_excel_bytes(df: pd.DataFrame) -> bytes:
282
  bio = BytesIO()
283
  df.to_excel(bio, index=False)
284
  bio.seek(0)
285
  return bio.read()
286
 
287
-
288
  def _build_html_body(df: pd.DataFrame, title: str) -> str:
289
- # fillna('') 可以在生成html时将 NaN/NaT 显示为空字符串
290
- table_html = df.fillna('').to_html(index=False, escape=False)
291
- html = f"""<html>
292
- <head>
293
- <meta charset="utf-8" />
294
- <style>
295
- table {{ border-collapse: collapse; font-size: 13px; }}
296
- table, th, td {{ border: 1px solid #ccc; padding: 6px; }}
297
- th {{ background:#f6f6f6; }}
298
- </style>
299
- </head>
300
- <body>
301
- <h3>{title}</h3>
302
- {table_html}
303
  <p style="color:#666;">备注:此邮件由自动化系统生成。</p>
304
- </body>
305
- </html>"""
306
  return html
307
 
308
-
309
- def _send_email_via_resend(subject: str, html_body: str,
310
- attachment_bytes: Optional[bytes],
311
- attachment_name: str) -> Tuple[bool, str]:
312
  if not (RESEND_API_KEY and FROM_EMAIL and TO_EMAIL):
313
  return False, "缺少 Resend 配置(RESEND_API_KEY / FROM_EMAIL / TO_EMAIL)"
314
-
315
  url = "https://api.resend.com/emails"
316
- headers = {
317
- "Authorization": f"Bearer {RESEND_API_KEY}",
318
- "Content-Type": "application/json",
319
- }
320
- payload = {
321
- "from": FROM_EMAIL,
322
- "to": [TO_EMAIL],
323
- "subject": subject,
324
- "html": html_body,
325
- }
326
  if attachment_bytes is not None:
327
- payload["attachments"] = [{
328
- "filename": attachment_name,
329
- "content": base64.b64encode(attachment_bytes).decode("utf-8"),
330
- }]
331
-
332
- resp = requests.post(url, headers=headers, data=json.dumps(payload))
333
- if resp.ok:
334
  return True, resp.text
335
- return False, f"HTTP {resp.status_code}: {resp.text}"
336
-
337
 
338
  # ====== 主流程 ======
339
  def run_once(file_path: Optional[str] = None) -> dict:
340
- """
341
- 单次处理:读入Excel -> 汇总 -> 生成输出 -> 发邮件。
342
- 返回一个 dict 给上层(便于 app.py 返回给前端)。
343
- """
344
  try:
345
  if file_path is None:
346
  file_path = _find_latest_input(INPUT_DIR)
@@ -349,10 +242,9 @@ def run_once(file_path: Optional[str] = None) -> dict:
349
 
350
  raw = read_system_export(file_path)
351
  final = aggregate_for_email(raw)
352
-
353
  out_name = f"邮件发送的格式_{datetime.now().strftime('%Y%m%d')}.xlsx"
354
  out_path = os.path.join(OUTPUT_DIR, out_name)
355
- os.makedirs(OUTPUT_DIR, exist_ok=True)
356
  final.to_excel(out_path, index=False)
357
 
358
  subject = f"采购执行表自动推送 {datetime.now().date()}"
@@ -360,27 +252,19 @@ def run_once(file_path: Optional[str] = None) -> dict:
360
  attach = _df_to_excel_bytes(final)
361
  ok, info = _send_email_via_resend(subject, html, attachment_bytes=attach, attachment_name=out_name)
362
 
363
- return {
364
- "ok": ok,
365
- "msg": "邮件发送成功" if ok else f"邮件发送失败:{info}",
366
- "input": file_path,
367
- "output": out_path,
368
- "rows": len(final),
369
- }
370
  except Exception as e:
371
  import traceback
372
  return {"ok": False, "msg": f"处理文件时发生严重错误: {e}", "traceback": traceback.format_exc()}
373
 
374
-
375
- def main(trigger_file: Optional[str] = None):
376
- result = run_once(trigger_file)
377
- print(json.dumps(result, ensure_ascii=False, indent=2))
378
- # 返回码:成功0,失败1(便于将来做cron/健康检查)
379
  if not result.get("ok"):
380
  sys.exit(1)
381
 
382
-
383
  if __name__ == "__main__":
384
- # 允许命令行传入具体文件路径
385
- arg_file = sys.argv[1] if len(sys.argv) > 1 else None
386
- main(arg_file)
 
13
  import base64
14
  from io import BytesIO
15
  from typing import Optional, Tuple, List
16
+ from datetime import datetime, date
 
17
 
18
  import pandas as pd
19
  import requests
 
29
  TO_EMAIL = os.environ.get("TO_EMAIL")
30
  TIMEZONE = os.environ.get("TZ", "Asia/Shanghai")
31
 
32
+ # ====== 业务相关字段 ======
 
33
  ALIASES = {
34
  "请购日期": ["请购日期", "请购日", "申请日期"],
35
  "请购单号": ["请购单号", "请购单编号", "申请单号"],
36
  "物料编码": ["物料编码", "物料号", "物料代码"],
37
  "物料名称": ["物料名称", "品名", "名称"],
38
  "纱支密度": ["纱支密度", "纱支/密度", "纱支 密度"],
39
+ "门幅(CM)": ["门幅(CM)", "门幅(CM)", "门幅cm", "门幅"],
40
  "颜色": ["颜色", "色号/颜色", "色号"],
41
  "主单位": ["主单位", "单位"],
42
  "主数量": ["主数量", "数量", "请购数量"],
 
49
  "计划到货日期": ["计划到货日期", "预计到货日期", "承诺到货日期", "计划到货日"],
50
  }
51
 
 
52
  EMAIL_COLS_DEFAULT = [
53
+ "请购日期","请购单号","物料编码","物料名称","纱支密度","门幅(CM)","颜色","主单位",
54
  "主数量","需求日期","供应商","到货日期","到货主数量","入库日期","入库主数量","目前进度"
55
  ]
56
 
 
 
 
 
 
 
 
 
 
57
  # ====== 工具函数 ======
58
  def _today() -> date:
 
59
  return datetime.now().date()
60
 
 
61
  def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
62
  mapped = {}
63
  for std_name, variants in ALIASES.items():
64
  for v in df.columns:
 
69
  df = df.rename(columns=mapped)
70
  return df
71
 
 
72
  def _find_header_row(path: str, must_have: List[str] = None, try_rows: int = 10) -> int:
 
 
 
 
73
  must_have = must_have or ["物料编码", "主数量"]
74
  for r in range(try_rows):
75
  try:
 
81
  return r
82
  return 0
83
 
 
84
  def read_system_export(path: str) -> pd.DataFrame:
 
 
 
85
  header_row = _find_header_row(path)
86
  try:
87
  df = pd.read_excel(path, header=header_row)
88
  except Exception:
89
  df = pd.read_excel(path, header=0)
90
 
 
91
  df = df.dropna(axis=1, how="all")
 
92
  df = _normalize_columns(df)
93
 
 
94
  if "物料名称" in df.columns:
95
  df["物料名称"] = df["物料名称"].astype(str).fillna('')
96
  contains_e = df["物料名称"].str.contains("鹅")
 
99
  contains_huazhu_special = df["物料名称"].str.contains("华住专用")
100
  to_remove = (contains_e | contains_ya | contains_huazhu) & ~contains_huazhu_special
101
  df = df[~to_remove]
 
102
 
 
103
  for c in ["请购日期","需求日期","到货日期","入库日期","计划到货日期"]:
104
  if c in df.columns:
105
  df[c] = pd.to_datetime(df[c], errors="coerce")
106
+
 
107
  for c in ["主数量","到货主数量","入库主数量"]:
108
  if c in df.columns:
109
  df[c] = pd.to_numeric(df[c], errors="coerce").fillna(0)
110
 
 
111
  df = df.dropna(how="all")
112
  return df
113
 
 
114
  def _first_nonnull(series: pd.Series):
115
+ return series.dropna().iloc[0] if not series.dropna().empty else None
 
 
 
 
116
 
117
  def aggregate_for_email(df: pd.DataFrame) -> pd.DataFrame:
 
 
 
118
  group_keys = [k for k in [
119
+ "请购单号","物料编码","物料名称","纱支密度","门幅(CM)","颜色","主单位","供应商"
120
  ] if k in df.columns]
121
 
122
  if not group_keys:
123
  raise RuntimeError("找不到用于分组的关键字段(如 请购单号/物料编码 等),请检查导入的表头。")
124
 
125
+ agg_map = {
126
+ "主数量": _first_nonnull,
127
+ "请购日期": _first_nonnull,
128
+ "需求日期": _first_nonnull,
129
+ "到货日期": "max",
130
+ "入库日期": "max",
131
+ "计划到货日期": "max",
132
+ "到货主数量": "sum",
133
+ "入库主数量": "sum"
134
+ }
135
+
136
+ final_agg_map = {k: v for k, v in agg_map.items() if k in df.columns}
 
 
 
 
 
 
 
 
 
 
137
 
138
+ grouped = df.groupby(group_keys, dropna=False).agg(final_agg_map).reset_index()
139
+ grouped["目前进度"] = grouped.apply(_calc_progress_row, axis=1)
140
+
141
  if "计划到货日期" in grouped.columns:
142
  grouped = grouped.drop(columns=["计划到货日期"])
143
 
144
+ final_cols = [col for col in EMAIL_COLS_DEFAULT if col in grouped.columns]
145
+ final = grouped[final_cols]
146
+
 
147
  date_cols_to_format = ["请购日期", "需求日期", "到货日期", "入库日期"]
148
  for col in date_cols_to_format:
149
  if col in final.columns:
 
150
  final[col] = pd.to_datetime(final[col], errors='coerce').dt.strftime('%Y/%-m/%-d')
151
 
152
  return final
153
 
 
154
  def _calc_progress_row(row: pd.Series) -> str:
 
 
 
155
  today = pd.Timestamp(_today())
156
  main_qty = float(row.get("主数量", 0) or 0)
157
  arr_qty = float(row.get("到货主数量", 0) or 0)
 
159
  arrival_date = row.get("到货日期", pd.NaT)
160
  plan_arrival = row.get("计划到货日期", pd.NaT)
161
 
162
+ # --- 逻辑修改:调整“完全到货”的判断标准 ---
163
+ # 当到货数量达到主数量的97%时,也视为完全到货
164
+ if main_qty > 0 and arr_qty >= main_qty * 0.97:
165
  return "完全到货"
166
 
167
  parts: List[str] = []
 
169
 
170
  if arr_qty > 0:
171
  parts.append(f"部分到货 缺货{shortage:g}米")
172
+ elif pd.isna(arrival_date) or arr_qty == 0:
173
+ parts.append("未到货")
 
174
 
175
  if (pd.isna(arrival_date) or arr_qty < main_qty) and pd.notna(demand_date):
176
  overdue_days = (today - demand_date).days
 
189
 
190
  return ";".join(parts)
191
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
192
  def _find_latest_input(input_dir: str) -> Optional[str]:
193
  files = []
194
  for pat in ("*.xlsx", "*.xls"):
 
198
  files.sort(key=os.path.getmtime, reverse=True)
199
  return files[0]
200
 
 
201
  def _df_to_excel_bytes(df: pd.DataFrame) -> bytes:
202
  bio = BytesIO()
203
  df.to_excel(bio, index=False)
204
  bio.seek(0)
205
  return bio.read()
206
 
 
207
  def _build_html_body(df: pd.DataFrame, title: str) -> str:
208
+ table_html = df.fillna('').to_html(index=False, escape=False, na_rep="")
209
+ html = f"""<html><head><meta charset="utf-8" /><style>
210
+ table {{ border-collapse: collapse; font-size: 13px; }}
211
+ table, th, td {{ border: 1px solid #ccc; padding: 6px; }}
212
+ th {{ background:#f6f6f6; }}
213
+ </style></head><body><h3>{title}</h3>{table_html}
 
 
 
 
 
 
 
 
214
  <p style="color:#666;">备注:此邮件由自动化系统生成。</p>
215
+ </body></html>"""
 
216
  return html
217
 
218
+ def _send_email_via_resend(subject: str, html_body: str, attachment_bytes: Optional[bytes], attachment_name: str) -> Tuple[bool, str]:
 
 
 
219
  if not (RESEND_API_KEY and FROM_EMAIL and TO_EMAIL):
220
  return False, "缺少 Resend 配置(RESEND_API_KEY / FROM_EMAIL / TO_EMAIL)"
221
+
222
  url = "https://api.resend.com/emails"
223
+ headers = {"Authorization": f"Bearer {RESEND_API_KEY}", "Content-Type": "application/json"}
224
+ payload = {"from": FROM_EMAIL, "to": [TO_EMAIL], "subject": subject, "html": html_body}
 
 
 
 
 
 
 
 
225
  if attachment_bytes is not None:
226
+ payload["attachments"] = [{"filename": attachment_name, "content": base64.b64encode(attachment_bytes).decode("utf-8")}]
227
+
228
+ try:
229
+ resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=20)
230
+ resp.raise_for_status()
 
 
231
  return True, resp.text
232
+ except requests.exceptions.RequestException as e:
233
+ return False, f"邮件发送请求失败: {e}"
234
 
235
  # ====== 主流程 ======
236
  def run_once(file_path: Optional[str] = None) -> dict:
 
 
 
 
237
  try:
238
  if file_path is None:
239
  file_path = _find_latest_input(INPUT_DIR)
 
242
 
243
  raw = read_system_export(file_path)
244
  final = aggregate_for_email(raw)
245
+
246
  out_name = f"邮件发送的格式_{datetime.now().strftime('%Y%m%d')}.xlsx"
247
  out_path = os.path.join(OUTPUT_DIR, out_name)
 
248
  final.to_excel(out_path, index=False)
249
 
250
  subject = f"采购执行表自动推送 {datetime.now().date()}"
 
252
  attach = _df_to_excel_bytes(final)
253
  ok, info = _send_email_via_resend(subject, html, attachment_bytes=attach, attachment_name=out_name)
254
 
255
+ return {"ok": ok, "msg": "邮件发送成功" if ok else f"邮件发送失败:{info}", "input": file_path, "output": out_path, "rows": len(final)}
256
+
 
 
 
 
 
257
  except Exception as e:
258
  import traceback
259
  return {"ok": False, "msg": f"处理文件时发生严重错误: {e}", "traceback": traceback.format_exc()}
260
 
261
+ def main():
262
+ arg_file = sys.argv[1] if len(sys.argv) > 1 else None
263
+ result = run_once(arg_file)
264
+ print(json.dumps(result, ensure_ascii=False))
265
+
266
  if not result.get("ok"):
267
  sys.exit(1)
268
 
 
269
  if __name__ == "__main__":
270
+ main()