auto backup 2026-06-26 08:10:49
This commit is contained in:
parent
c81e1b532f
commit
6f80ba99e8
@ -43,3 +43,4 @@ lark-workflow-meeting-summary 7479f4b43979627fde453c48b5b50970a81030d4c74d62185f
|
|||||||
feishu-group-monitor 8c3a459ae1383d51bc43e75e9ec39c910eb14406bda4d8c7875270058f1d3d15
|
feishu-group-monitor 8c3a459ae1383d51bc43e75e9ec39c910eb14406bda4d8c7875270058f1d3d15
|
||||||
lark-send-message-as-bot 5502e41e032a1b86adc91ebe42285735b12d17b9a6be8f5acb922a8a4fa6dad6
|
lark-send-message-as-bot 5502e41e032a1b86adc91ebe42285735b12d17b9a6be8f5acb922a8a4fa6dad6
|
||||||
gpt-image-2-generator e1842d74b29c63ba218764e8156a50a47aa0f3d7be540af54d6d2db9d59c7382
|
gpt-image-2-generator e1842d74b29c63ba218764e8156a50a47aa0f3d7be540af54d6d2db9d59c7382
|
||||||
|
vala-data-export 10b848c9c91084e4a8aab3c3c917225a07ceeaa5510537b40d0f7b5d2a40412f
|
||||||
|
|||||||
BIN
output/知识巩固_音频_23600_20260625_114933.xlsx
Normal file
BIN
output/知识巩固_音频_23600_20260625_114933.xlsx
Normal file
Binary file not shown.
BIN
output/知识巩固_音频_23600_20260625_115353.xlsx
Normal file
BIN
output/知识巩固_音频_23600_20260625_115353.xlsx
Normal file
Binary file not shown.
BIN
output/知识巩固_音频_23600_20260625_115549.xlsx
Normal file
BIN
output/知识巩固_音频_23600_20260625_115549.xlsx
Normal file
Binary file not shown.
BIN
output/知识巩固_音频_23600_20260625_122855.xlsx
Normal file
BIN
output/知识巩固_音频_23600_20260625_122855.xlsx
Normal file
Binary file not shown.
@ -56,7 +56,8 @@ with pg_conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|||||||
""", (user_ids,))
|
""", (user_ids,))
|
||||||
review_rows = cur.fetchall()
|
review_rows = cur.fetchall()
|
||||||
|
|
||||||
# Parse question_list JSON for readable summary
|
# Parse question_list JSON — explode each question into its own row
|
||||||
|
expanded_rows = [] # one row per question
|
||||||
for row in review_rows:
|
for row in review_rows:
|
||||||
ql = row["question_list"]
|
ql = row["question_list"]
|
||||||
if isinstance(ql, str):
|
if isinstance(ql, str):
|
||||||
@ -64,17 +65,34 @@ for row in review_rows:
|
|||||||
ql = json.loads(ql)
|
ql = json.loads(ql)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
questions = []
|
base = {
|
||||||
|
"角色ID": row["user_id"],
|
||||||
|
"Level": row["level"],
|
||||||
|
"Story ID": row["story_id"],
|
||||||
|
"Chapter ID": row["chapter_id"],
|
||||||
|
"Unique ID": row["unique_id"],
|
||||||
|
"更新时间": str(row["updated_at"]),
|
||||||
|
}
|
||||||
if isinstance(ql, list):
|
if isinstance(ql, list):
|
||||||
for item in ql:
|
for item in ql:
|
||||||
if isinstance(item, dict):
|
if isinstance(item, dict):
|
||||||
q = item.get("question", {})
|
q = item.get("question", {})
|
||||||
qtype = q.get("type", "")
|
qtype = q.get("type", "")
|
||||||
qtitle = q.get("title", "")
|
qtitle = q.get("title", "")
|
||||||
user_answer = item.get("userAnswer", "")
|
is_right = item.get("isRight")
|
||||||
score = item.get("score", "")
|
status = "正确" if is_right is True else "错误"
|
||||||
questions.append(f"[{qtype}] {qtitle} | 回答: {user_answer} | 得分: {score}")
|
audio_url = item.get("userAudio", "")
|
||||||
row["question_summary"] = "\n".join(questions)
|
user_answer = item.get("userAnswer", "") or item.get("userAnswers", "")
|
||||||
|
if isinstance(user_answer, list):
|
||||||
|
user_answer = ", ".join(str(a) for a in user_answer)
|
||||||
|
expanded_rows.append({
|
||||||
|
**base,
|
||||||
|
"题目类型": qtype,
|
||||||
|
"题目内容": qtitle,
|
||||||
|
"题目对错": status,
|
||||||
|
"音频URL": audio_url,
|
||||||
|
"用户答案": user_answer,
|
||||||
|
})
|
||||||
row["question_count"] = len(ql) if isinstance(ql, list) else 0
|
row["question_count"] = len(ql) if isinstance(ql, list) else 0
|
||||||
|
|
||||||
pg_conn.close()
|
pg_conn.close()
|
||||||
@ -149,26 +167,25 @@ from openpyxl.styles import Font, Alignment, PatternFill
|
|||||||
|
|
||||||
wb = Workbook()
|
wb = Workbook()
|
||||||
|
|
||||||
# Sheet 1: 课程巩固记录
|
# Sheet 1: 课程巩固记录(每题一行)
|
||||||
ws1 = wb.active
|
ws1 = wb.active
|
||||||
ws1.title = "课程巩固记录"
|
ws1.title = "课程巩固记录"
|
||||||
|
|
||||||
|
# Build DataFrame from expanded_rows
|
||||||
review_data = []
|
review_data = []
|
||||||
for row in review_rows:
|
for row in expanded_rows:
|
||||||
review_data.append({
|
review_data.append({
|
||||||
"角色ID": row["user_id"],
|
"角色ID": row["角色ID"],
|
||||||
"Level": row["level"],
|
"Level": row["Level"],
|
||||||
"Story ID": row["story_id"],
|
"Story ID": row["Story ID"],
|
||||||
"Chapter ID": row["chapter_id"],
|
"Chapter ID": row["Chapter ID"],
|
||||||
"Unique ID": row["unique_id"],
|
"Unique ID": row["Unique ID"],
|
||||||
"得分": row["score"],
|
"题目类型": row["题目类型"],
|
||||||
"评级": row["score_text"],
|
"题目内容": row["题目内容"],
|
||||||
"SP值": row["sp_value"],
|
"题目对错": row["题目对错"],
|
||||||
"经验值": row["exp"],
|
"音频URL": row["音频URL"],
|
||||||
"题目数": row["question_count"],
|
"用户答案": row["用户答案"],
|
||||||
"耗时(秒)": row["play_time"],
|
"更新时间": row["更新时间"],
|
||||||
"题目详情": row["question_summary"],
|
|
||||||
"更新时间": str(row["updated_at"]),
|
|
||||||
"创建时间": str(row["created_at"]),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
df1 = pd.DataFrame(review_data)
|
df1 = pd.DataFrame(review_data)
|
||||||
@ -186,8 +203,16 @@ for cell in ws1[1]:
|
|||||||
|
|
||||||
# Column widths
|
# Column widths
|
||||||
ws1.column_dimensions["A"].width = 10
|
ws1.column_dimensions["A"].width = 10
|
||||||
ws1.column_dimensions["L"].width = 12
|
ws1.column_dimensions["B"].width = 8
|
||||||
ws1.column_dimensions["M"].width = 60
|
ws1.column_dimensions["C"].width = 12
|
||||||
|
ws1.column_dimensions["D"].width = 12
|
||||||
|
ws1.column_dimensions["E"].width = 36
|
||||||
|
ws1.column_dimensions["F"].width = 24
|
||||||
|
ws1.column_dimensions["G"].width = 30
|
||||||
|
ws1.column_dimensions["H"].width = 10
|
||||||
|
ws1.column_dimensions["I"].width = 55
|
||||||
|
ws1.column_dimensions["J"].width = 30
|
||||||
|
ws1.column_dimensions["K"].width = 22
|
||||||
|
|
||||||
# Sheet 2: 音频数据
|
# Sheet 2: 音频数据
|
||||||
ws2 = wb.create_sheet("音频数据")
|
ws2 = wb.create_sheet("音频数据")
|
||||||
@ -244,13 +269,15 @@ ws3["A3"] = "导出时间"
|
|||||||
ws3["B3"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
ws3["B3"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
ws3["A4"] = "角色ID"
|
ws3["A4"] = "角色ID"
|
||||||
ws3["B4"] = ", ".join(str(u) for u in user_ids)
|
ws3["B4"] = ", ".join(str(u) for u in user_ids)
|
||||||
ws3["A5"] = "课程巩固记录数"
|
ws3["A5"] = "课程巩固记录数(题目拆分行)"
|
||||||
ws3["B5"] = len(review_rows)
|
ws3["B5"] = len(expanded_rows)
|
||||||
ws3["A6"] = "音频记录数"
|
ws3["A6"] = "原始巩固回合数"
|
||||||
ws3["B6"] = len(audio_rows)
|
ws3["B6"] = len(review_rows)
|
||||||
|
ws3["A7"] = "音频记录数"
|
||||||
|
ws3["B7"] = len(audio_rows)
|
||||||
|
|
||||||
# Per-user breakdown
|
# Per-user breakdown
|
||||||
row_offset = 8
|
row_offset = 10
|
||||||
ws3[f"A{row_offset}"] = "按角色统计"
|
ws3[f"A{row_offset}"] = "按角色统计"
|
||||||
ws3[f"A{row_offset}"].font = Font(bold=True)
|
ws3[f"A{row_offset}"].font = Font(bold=True)
|
||||||
row_offset += 1
|
row_offset += 1
|
||||||
@ -284,6 +311,83 @@ ws3.column_dimensions["D"].width = 28
|
|||||||
|
|
||||||
wb.save(output_path)
|
wb.save(output_path)
|
||||||
print(f"\n✅ 导出完成: {output_path}")
|
print(f"\n✅ 导出完成: {output_path}")
|
||||||
print(f" Sheet 1 — 课程巩固记录: {len(review_rows)} 行")
|
print(f" Sheet 1 — 课程巩固记录(每题一行): {len(expanded_rows)} 行")
|
||||||
print(f" Sheet 2 — 音频数据: {len(audio_rows)} 行")
|
print(f" Sheet 2 — 音频数据: {len(audio_rows)} 行")
|
||||||
print(f" Sheet 3 — 汇总")
|
print(f" Sheet 3 — 汇总")
|
||||||
|
|
||||||
|
# ── 4. 自动通过飞书发送文件 ──────────────────────────
|
||||||
|
# 默认发送给李若松 (user_id: 4aagb443)
|
||||||
|
# 可通过环境变量 FEISHU_SEND_TO 覆盖目标 user_id
|
||||||
|
SEND_TO = os.environ.get("FEISHU_SEND_TO", "4aagb443")
|
||||||
|
SEND_ENABLED = os.environ.get("FEISHU_SEND_ENABLED", "1")
|
||||||
|
|
||||||
|
if SEND_ENABLED != "1":
|
||||||
|
print("\n⏭️ 飞书自动发送已禁用 (FEISHU_SEND_ENABLED != 1)")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print(f"\n[4/4] 通过飞书发送文件到 user_id={SEND_TO}...")
|
||||||
|
|
||||||
|
FEISHU_APP_ID = g("FEISHU_APP_ID")
|
||||||
|
FEISHU_APP_SECRET = g("FEISHU_APP_SECRET")
|
||||||
|
|
||||||
|
if not FEISHU_APP_ID or not FEISHU_APP_SECRET:
|
||||||
|
print(" ⚠️ 未找到 FEISHU_APP_ID/FEISHU_APP_SECRET,跳过发送")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
def feishu_api(method, path, **kwargs):
|
||||||
|
"""调用飞书 Open API,返回解析后的 JSON"""
|
||||||
|
cmd = ["curl", "-sk", "--connect-timeout", "10", "--max-time", "60"]
|
||||||
|
if method == "POST":
|
||||||
|
cmd += ["-X", "POST"]
|
||||||
|
cmd += ["-H", f"Authorization: Bearer {kwargs.get('token', '')}"]
|
||||||
|
if "json_data" in kwargs:
|
||||||
|
cmd += ["-H", "Content-Type: application/json", "-d", json.dumps(kwargs["json_data"])]
|
||||||
|
if "form_fields" in kwargs:
|
||||||
|
for key, val in kwargs["form_fields"].items():
|
||||||
|
cmd += ["-F", f"{key}={val}"]
|
||||||
|
cmd.append(f"https://open.feishu.cn{path}")
|
||||||
|
r = subprocess.run(cmd, capture_output=True, text=True, timeout=65)
|
||||||
|
try:
|
||||||
|
return json.loads(r.stdout)
|
||||||
|
except:
|
||||||
|
print(f" ❌ API 返回解析失败: {r.stdout[:200]}")
|
||||||
|
return {"code": -1, "msg": r.stdout[:200]}
|
||||||
|
|
||||||
|
# Step 1: 获取 tenant_access_token
|
||||||
|
print(" → 获取 tenant_access_token...")
|
||||||
|
token_resp = feishu_api("POST", "/open-apis/auth/v3/tenant_access_token/internal",
|
||||||
|
json_data={"app_id": FEISHU_APP_ID, "app_secret": FEISHU_APP_SECRET})
|
||||||
|
token = token_resp.get("tenant_access_token")
|
||||||
|
if not token:
|
||||||
|
print(f" ❌ 获取 token 失败: {token_resp}")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f" → Token 获取成功")
|
||||||
|
|
||||||
|
# Step 2: 上传文件
|
||||||
|
file_name = os.path.basename(output_path)
|
||||||
|
file_ext = os.path.splitext(output_path)[1].lower()
|
||||||
|
file_type_map = {".xlsx": "xls", ".xls": "xls", ".pdf": "pdf", ".docx": "doc", ".pptx": "ppt"}
|
||||||
|
file_type = file_type_map.get(file_ext, "stream")
|
||||||
|
|
||||||
|
print(f" → 上传文件: {file_name} (type={file_type})...")
|
||||||
|
upload_resp = feishu_api("POST", "/open-apis/im/v1/files", token=token,
|
||||||
|
form_fields={"file_type": file_type, "file_name": file_name, "file": f"@{output_path}"})
|
||||||
|
file_key = upload_resp.get("data", {}).get("file_key")
|
||||||
|
if not file_key:
|
||||||
|
print(f" ❌ 文件上传失败: {upload_resp}")
|
||||||
|
sys.exit(1)
|
||||||
|
print(f" → 上传成功: {file_key}")
|
||||||
|
|
||||||
|
# Step 3: 发送文件消息
|
||||||
|
print(f" → 发送文件到 user_id={SEND_TO}...")
|
||||||
|
send_resp = feishu_api("POST", "/open-apis/im/v1/messages?receive_id_type=user_id", token=token,
|
||||||
|
json_data={
|
||||||
|
"receive_id": SEND_TO,
|
||||||
|
"msg_type": "file",
|
||||||
|
"content": json.dumps({"file_key": file_key})
|
||||||
|
})
|
||||||
|
msg_id = send_resp.get("data", {}).get("message_id")
|
||||||
|
if msg_id:
|
||||||
|
print(f" ✅ 文件已发送,message_id={msg_id}")
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ 发送可能失败: {send_resp}")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user