更新:models/routes/services/templates/docs
This commit is contained in:
+118
-189
@@ -1,63 +1,62 @@
|
||||
# PDF生成服务 - 支持中文
|
||||
# PDF生成服务 - 支持中文和富文本
|
||||
|
||||
import os
|
||||
import re
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib import colors
|
||||
from reportlab.platypus import (
|
||||
SimpleDocTemplate,
|
||||
Paragraph,
|
||||
Spacer,
|
||||
Table,
|
||||
TableStyle,
|
||||
PageBreak,
|
||||
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
|
||||
)
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
||||
|
||||
# 注册中文字体(包含常规和粗体)
|
||||
FONT_PATH = r"C:\Windows\Fonts\msyh.ttc"
|
||||
FONT_BOLD_PATH = r"C:\Windows\Fonts\msyhbd.ttc"
|
||||
|
||||
def register_chinese_font():
|
||||
"""注册中文字体"""
|
||||
# 尝试注册系统自带的中文字体
|
||||
font_paths = [
|
||||
# Windows常用字体路径
|
||||
r"C:\Windows\Fonts\simsun.ttc", # 宋体
|
||||
r"C:\Windows\Fonts\msyh.ttc", # 微软雅黑
|
||||
r"C:\Windows\Fonts\STSONG.TTF", # 华文宋体
|
||||
r"C:\Windows\Fonts\simhei.ttf", # 黑体
|
||||
# Linux容器字体路径
|
||||
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",
|
||||
"/usr/share/fonts/noto/NotoSansCJK-Regular.ttc",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
]
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont("Chinese", FONT_PATH))
|
||||
pdfmetrics.registerFont(TTFont("Chinese-Bold", FONT_BOLD_PATH))
|
||||
CHINESE_FONT_OK = True
|
||||
except Exception as e:
|
||||
CHINESE_FONT_OK = False
|
||||
|
||||
# 尝试注册一个可用的字体
|
||||
for font_path in font_paths:
|
||||
if os.path.exists(font_path):
|
||||
try:
|
||||
pdfmetrics.registerFont(TTFont("Chinese", font_path))
|
||||
return True
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# 尝试注册中文字体
|
||||
_has_chinese_font = register_chinese_font()
|
||||
|
||||
|
||||
def clean_text(text):
|
||||
"""清理文本,移除不支持的字符"""
|
||||
def md_to_xml(text):
|
||||
"""将markdown转换为reportlab XML markup"""
|
||||
if not text:
|
||||
return ""
|
||||
# 移除多余空白
|
||||
text = re.sub(r"\s+", " ", text)
|
||||
return text.strip()
|
||||
result = []
|
||||
i = 0
|
||||
while i < len(text):
|
||||
# 处理 bold **text** - 使用中文字体粗体
|
||||
if text[i:i+2] == '**':
|
||||
end = text.find('**', i+2)
|
||||
if end != -1:
|
||||
if CHINESE_FONT_OK:
|
||||
result.append(f'<font name="Chinese-Bold">{text[i+2:end]}</font>')
|
||||
else:
|
||||
result.append(f'<b>{text[i+2:end]}</b>')
|
||||
i = end + 2
|
||||
continue
|
||||
# 处理 italic *text*
|
||||
if text[i] == '*' and (i == 0 or text[i-1] not in '*_'):
|
||||
end = text.find('*', i+1)
|
||||
if end != -1 and text[end-1] != '*':
|
||||
result.append(f'<i>{text[i+1:end]}</i>')
|
||||
i = end + 1
|
||||
continue
|
||||
# 处理 inline code `text`
|
||||
if text[i] == '`':
|
||||
end = text.find('`', i+1)
|
||||
if end != -1:
|
||||
result.append(f'<font name="Courier">{text[i+1:end]}</font>')
|
||||
i = end + 1
|
||||
continue
|
||||
result.append(text[i])
|
||||
i += 1
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
class PianoPDF:
|
||||
@@ -65,141 +64,117 @@ class PianoPDF:
|
||||
self.elements = []
|
||||
self.styles = getSampleStyleSheet()
|
||||
|
||||
# 创建中文字体样式
|
||||
if _has_chinese_font:
|
||||
if CHINESE_FONT_OK:
|
||||
self.base_font = "Chinese"
|
||||
self.bold_font = "Chinese-Bold"
|
||||
else:
|
||||
self.base_font = "Helvetica"
|
||||
self.bold_font = "Helvetica-Bold"
|
||||
|
||||
# 标题样式
|
||||
self.title_style = ParagraphStyle(
|
||||
"CustomTitle",
|
||||
parent=self.styles["Heading1"],
|
||||
fontName=self.base_font,
|
||||
fontName=self.bold_font,
|
||||
fontSize=18,
|
||||
spaceAfter=10 * mm,
|
||||
spaceAfter=10*mm,
|
||||
alignment=TA_CENTER,
|
||||
)
|
||||
|
||||
# 副标题样式
|
||||
# 二级标题
|
||||
self.heading_style = ParagraphStyle(
|
||||
"CustomHeading",
|
||||
parent=self.styles["Heading2"],
|
||||
fontName=self.base_font,
|
||||
fontName=self.bold_font,
|
||||
fontSize=14,
|
||||
spaceAfter=6 * mm,
|
||||
spaceBefore=6 * mm,
|
||||
spaceAfter=6*mm,
|
||||
spaceBefore=6*mm,
|
||||
textColor=colors.HexColor("#2c3e50"),
|
||||
)
|
||||
|
||||
# 三级标题
|
||||
self.h3_style = ParagraphStyle(
|
||||
"CustomH3",
|
||||
parent=self.styles["Heading3"],
|
||||
fontName=self.bold_font,
|
||||
fontSize=12,
|
||||
spaceAfter=4*mm,
|
||||
spaceBefore=4*mm,
|
||||
textColor=colors.HexColor("#34495e"),
|
||||
)
|
||||
|
||||
# 正文样式
|
||||
self.body_style = ParagraphStyle(
|
||||
"CustomBody",
|
||||
parent=self.styles["Normal"],
|
||||
fontName=self.base_font,
|
||||
fontSize=10,
|
||||
spaceAfter=4 * mm,
|
||||
spaceAfter=4*mm,
|
||||
leading=14,
|
||||
)
|
||||
|
||||
# 表格样式
|
||||
self.table_style = TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#3498db")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
||||
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
||||
("FONTNAME", (0, 0), (-1, 0), self.base_font),
|
||||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 8),
|
||||
("BACKGROUND", (0, 1), (-1, -1), colors.white),
|
||||
("FONTNAME", (0, 1), (-1, -1), self.base_font),
|
||||
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
(
|
||||
"ROWBACKGROUNDS",
|
||||
(0, 1),
|
||||
(-1, -1),
|
||||
[colors.white, colors.HexColor("#f8f9fa")],
|
||||
),
|
||||
]
|
||||
)
|
||||
self.table_style = TableStyle([
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#3498db")),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
||||
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
||||
("FONTNAME", (0, 0), (-1, 0), self.bold_font),
|
||||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 8),
|
||||
("BACKGROUND", (0, 1), (-1, -1), colors.white),
|
||||
("FONTNAME", (0, 1), (-1, -1), self.base_font),
|
||||
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||||
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
|
||||
("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f8f9fa")]),
|
||||
])
|
||||
|
||||
def add_title(self, text):
|
||||
"""添加标题"""
|
||||
self.elements.append(Paragraph(clean_text(text), self.title_style))
|
||||
self.elements.append(Spacer(1, 5 * mm))
|
||||
self.elements.append(Paragraph(md_to_xml(text), self.title_style))
|
||||
self.elements.append(Spacer(1, 5*mm))
|
||||
|
||||
def add_heading(self, text):
|
||||
"""添加副标题"""
|
||||
self.elements.append(Paragraph(clean_text(text), self.heading_style))
|
||||
def add_heading(self, text, level=2):
|
||||
if level == 3:
|
||||
self.elements.append(Paragraph(md_to_xml(text), self.h3_style))
|
||||
else:
|
||||
self.elements.append(Paragraph(md_to_xml(text), self.heading_style))
|
||||
|
||||
def add_paragraph(self, text):
|
||||
"""添加段落"""
|
||||
if text:
|
||||
self.elements.append(Paragraph(clean_text(text), self.body_style))
|
||||
self.elements.append(Spacer(1, 3 * mm))
|
||||
self.elements.append(Paragraph(md_to_xml(text), self.body_style))
|
||||
self.elements.append(Spacer(1, 3*mm))
|
||||
|
||||
def add_list(self, items):
|
||||
"""添加列表"""
|
||||
for item in items:
|
||||
if item:
|
||||
self.elements.append(
|
||||
Paragraph(f"• {clean_text(item)}", self.body_style)
|
||||
)
|
||||
self.elements.append(Spacer(1, 3 * mm))
|
||||
self.elements.append(Paragraph(f"• {md_to_xml(item)}", self.body_style))
|
||||
self.elements.append(Spacer(1, 3*mm))
|
||||
|
||||
def add_table(self, data):
|
||||
"""添加表格"""
|
||||
if not data or len(data) < 2:
|
||||
return
|
||||
|
||||
# 确保所有数据都是字符串
|
||||
table_data = []
|
||||
for row in data:
|
||||
table_data.append([clean_text(str(cell)) for cell in row])
|
||||
|
||||
table_data.append([md_to_xml(str(cell)) if cell else "" for cell in row])
|
||||
if table_data:
|
||||
table = Table(table_data)
|
||||
table.setStyle(self.table_style)
|
||||
self.elements.append(table)
|
||||
self.elements.append(Spacer(1, 5 * mm))
|
||||
|
||||
def add_spacer(self, height=5 * mm):
|
||||
"""添加间距"""
|
||||
self.elements.append(Spacer(1, height))
|
||||
self.elements.append(Spacer(1, 5*mm))
|
||||
|
||||
|
||||
def generate_pdf(plan_id, student_name, content, output_dir, rendered_report=None):
|
||||
"""
|
||||
使用reportlab生成PDF文件
|
||||
|
||||
Args:
|
||||
plan_id: 方案ID
|
||||
student_name: 学员姓名
|
||||
content: 方案内容字典
|
||||
output_dir: 输出目录
|
||||
rendered_report: 可选的预渲染报告(Markdown格式),如果提供则使用模板渲染整个报告
|
||||
|
||||
Returns:
|
||||
str: PDF文件路径
|
||||
"""
|
||||
"""生成PDF文件"""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
output_path = os.path.join(output_dir, f"plan_{plan_id}.pdf")
|
||||
|
||||
# 创建PDF文档
|
||||
doc = SimpleDocTemplate(
|
||||
output_path,
|
||||
pagesize=A4,
|
||||
rightMargin=20 * mm,
|
||||
leftMargin=20 * mm,
|
||||
topMargin=20 * mm,
|
||||
bottomMargin=20 * mm,
|
||||
output_path, pagesize=A4,
|
||||
rightMargin=20*mm, leftMargin=20*mm,
|
||||
topMargin=20*mm, bottomMargin=20*mm,
|
||||
)
|
||||
|
||||
pdf = PianoPDF()
|
||||
|
||||
# 如果提供了预渲染的报告,使用模板渲染整个报告
|
||||
if rendered_report:
|
||||
# 解析Markdown并添加到PDF
|
||||
lines = rendered_report.split('\n')
|
||||
@@ -216,62 +191,47 @@ def generate_pdf(plan_id, student_name, content, output_dir, rendered_report=Non
|
||||
continue
|
||||
|
||||
if line.startswith('# ') and not line.startswith('## '):
|
||||
# 一级标题作为主标题
|
||||
title = line.replace('# ', '').replace('*', '')
|
||||
pdf.add_title(title)
|
||||
elif line.startswith('## '):
|
||||
# 二级标题
|
||||
if in_table and table_data:
|
||||
pdf.add_table(table_data)
|
||||
table_data = []
|
||||
in_table = False
|
||||
heading = line.replace('## ', '').replace('**', '')
|
||||
pdf.add_heading(heading)
|
||||
pdf.add_title(line.replace('# ', ''))
|
||||
elif line.startswith('## '):
|
||||
if in_table and table_data:
|
||||
pdf.add_table(table_data)
|
||||
table_data = []
|
||||
in_table = False
|
||||
pdf.add_heading(line.replace('## ', ''))
|
||||
elif line.startswith('### '):
|
||||
pdf.add_heading(line.replace('### ', ''), level=3)
|
||||
elif line.startswith('|') and '|' in line[1:]:
|
||||
# 表格行
|
||||
in_table = True
|
||||
cells = [c.strip() for c in line.split('|')[1:-1]]
|
||||
if cells and any(cells):
|
||||
# 跳过表头分隔行
|
||||
if not all(c.strip().startswith('-') for c in cells if c.strip()):
|
||||
table_data.append(cells)
|
||||
raw_cells = [c.strip() for c in line.split('|')[1:-1]]
|
||||
cells = [md_to_xml(c) if c else "" for c in raw_cells]
|
||||
if cells and not all(c and c.strip().startswith('-') for c in cells if c):
|
||||
table_data.append(cells)
|
||||
elif line.startswith('- '):
|
||||
# 列表项
|
||||
pdf.add_paragraph(line)
|
||||
pdf.add_paragraph(f"• {line[2:]}")
|
||||
elif line and not in_table:
|
||||
# 普通段落
|
||||
pdf.add_paragraph(line.replace('**', '').replace('*', ''))
|
||||
# 表格收尾
|
||||
pdf.add_paragraph(line)
|
||||
|
||||
if in_table and table_data:
|
||||
pdf.add_table(table_data)
|
||||
else:
|
||||
# 使用原有结构化方式生成PDF
|
||||
# 标题
|
||||
# 使用结构化内容
|
||||
pdf.add_title(f"钢琴练习方案 - {student_name}")
|
||||
|
||||
# 学员信息
|
||||
pdf.add_heading("📋 学员信息")
|
||||
pdf.add_heading("学员信息")
|
||||
pdf.add_paragraph(f"学员姓名:{student_name}")
|
||||
pdf.add_paragraph(f"每日练习时间:{content.get('practice_time', 'N/A')}")
|
||||
pdf.add_paragraph(f"生成时间:{content.get('generated_at', '')}")
|
||||
|
||||
# 问题诊断
|
||||
pdf.add_heading("🔍 问题诊断")
|
||||
for problem in content.get("problems", []):
|
||||
name = problem.get("name", "")
|
||||
severity = problem.get("severity", "")
|
||||
focus = problem.get("focus", {})
|
||||
pdf.add_paragraph(
|
||||
f"• {name} ({severity}) - 基础练习: {focus.get('basic', 0)}分钟, 技术练习: {focus.get('tech', 0)}分钟"
|
||||
)
|
||||
|
||||
# AI报告(如果有)
|
||||
if content.get("ai_report"):
|
||||
pdf.add_heading("📝 AI个性化练习报告")
|
||||
# 将AI报告分段添加
|
||||
pdf.add_heading("AI个性化练习报告")
|
||||
for line in content["ai_report"].split("\n"):
|
||||
line = line.strip()
|
||||
if line.startswith("## "):
|
||||
if line.startswith("### "):
|
||||
pdf.add_heading(line.replace("### ", ""), level=3)
|
||||
elif line.startswith("## "):
|
||||
pdf.add_heading(line.replace("## ", ""))
|
||||
elif line.startswith("# "):
|
||||
pdf.add_title(line.replace("# ", ""))
|
||||
@@ -280,36 +240,5 @@ def generate_pdf(plan_id, student_name, content, output_dir, rendered_report=Non
|
||||
elif line:
|
||||
pdf.add_paragraph(line)
|
||||
|
||||
# 每日练习计划
|
||||
pdf.add_heading("📅 每日练习计划")
|
||||
total_minutes = content.get("total_daily_minutes", 0)
|
||||
pdf.add_paragraph(f"总时长:{total_minutes} 分钟")
|
||||
|
||||
# 表格
|
||||
table_data = [["阶段", "时长", "内容", "目的"]]
|
||||
for item in content.get("daily_schedule", []):
|
||||
table_data.append(
|
||||
[
|
||||
item.get("phase", ""),
|
||||
item.get("duration", ""),
|
||||
item.get("content", ""),
|
||||
item.get("purpose", ""),
|
||||
]
|
||||
)
|
||||
pdf.add_table(table_data)
|
||||
|
||||
# 练习建议
|
||||
pdf.add_heading("💡 练习建议")
|
||||
tips = [
|
||||
"每天在固定时间练习,养成良好习惯",
|
||||
"练习前先热身,放松手指",
|
||||
"先难后易,先练习重点部分",
|
||||
"录音回听,发现问题",
|
||||
"保持耐心,进步需要时间",
|
||||
]
|
||||
pdf.add_list(tips)
|
||||
|
||||
# 生成PDF
|
||||
doc.build(pdf.elements)
|
||||
|
||||
return output_path
|
||||
return output_path
|
||||
@@ -3,11 +3,12 @@
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from app.config import load_api_config
|
||||
|
||||
|
||||
def generate_practice_plan(
|
||||
student_name, problems, practice_time="30-60分钟"
|
||||
student_name, problems, practice_time="30-60分钟", goals=None
|
||||
):
|
||||
"""
|
||||
根据学员问题和练习时间生成针对性练习方案
|
||||
@@ -16,6 +17,7 @@ def generate_practice_plan(
|
||||
student_name: 学员姓名
|
||||
problems: 问题列表 [{problem_id, problem_name, severity, level, content}]
|
||||
practice_time: 练习时间描述
|
||||
goals: 目标列表 [{goal_id, goal_name, goal_content, status, mastery_level}],默认为空列表
|
||||
|
||||
Returns:
|
||||
dict: 包含方案内容的字典
|
||||
@@ -41,6 +43,7 @@ def generate_practice_plan(
|
||||
{
|
||||
"name": p["problem_name"],
|
||||
"severity": p["severity"],
|
||||
"level": p.get("level", ""),
|
||||
"content": _extract_key_sections(content) if content else {"problem": f"针对{p['problem_name']}的练习"},
|
||||
"time_allocation": _calculate_time_allocation(
|
||||
p["severity"], time_config
|
||||
@@ -48,9 +51,6 @@ def generate_practice_plan(
|
||||
}
|
||||
)
|
||||
|
||||
# 生成每日练习计划
|
||||
daily_plan = _generate_daily_schedule(time_config, problem_contents)
|
||||
|
||||
# 生成方案
|
||||
plan = {
|
||||
"student_name": student_name,
|
||||
@@ -60,12 +60,12 @@ def generate_practice_plan(
|
||||
{
|
||||
"name": p["name"],
|
||||
"severity": p["severity"],
|
||||
"level": p.get("level", ""),
|
||||
"focus": p["time_allocation"],
|
||||
}
|
||||
for p in problem_contents
|
||||
],
|
||||
"daily_schedule": daily_plan,
|
||||
"generated_at": "2026-04-17",
|
||||
"generated_at": datetime.now().strftime("%Y-%m-%d"),
|
||||
}
|
||||
|
||||
return plan
|
||||
@@ -143,85 +143,6 @@ def _calculate_time_allocation(severity, time_config):
|
||||
}
|
||||
|
||||
|
||||
def _generate_daily_schedule(time_config, problem_contents):
|
||||
"""生成每日练习计划"""
|
||||
total = time_config["total"]
|
||||
|
||||
# 基础练习(哈农+音阶)
|
||||
basic_time = time_config["basic"]
|
||||
|
||||
# 技术练习(针对问题)
|
||||
tech_time = time_config["tech"]
|
||||
|
||||
# 曲目练习
|
||||
piece_time = time_config["piece"]
|
||||
|
||||
schedule = []
|
||||
|
||||
# 1. 热身
|
||||
schedule.append(
|
||||
{
|
||||
"phase": "热身",
|
||||
"duration": "3分钟",
|
||||
"content": "手部放松操 + 呼吸调节",
|
||||
"purpose": "放松肌肉,进入状态",
|
||||
}
|
||||
)
|
||||
|
||||
# 2. 基础练习
|
||||
if basic_time > 0:
|
||||
schedule.append(
|
||||
{
|
||||
"phase": "基础练习",
|
||||
"duration": f"{basic_time}分钟",
|
||||
"content": "哈农练习曲 + 音阶练习",
|
||||
"purpose": "建立基本功",
|
||||
}
|
||||
)
|
||||
|
||||
# 3. 技术练习(针对问题)
|
||||
if tech_time > 0 and problem_contents:
|
||||
tech_items = []
|
||||
for p in problem_contents:
|
||||
practices = p.get("content", {}).get("practices", [])
|
||||
if practices:
|
||||
# 取前2个练习
|
||||
for practice in practices[:2]:
|
||||
tech_items.append(practice["name"])
|
||||
|
||||
schedule.append(
|
||||
{
|
||||
"phase": "技术练习",
|
||||
"duration": f"{tech_time}分钟",
|
||||
"content": "、".join(tech_items[:3]) if tech_items else "针对性练习",
|
||||
"purpose": "解决具体问题",
|
||||
}
|
||||
)
|
||||
|
||||
# 4. 曲目练习
|
||||
if piece_time > 0:
|
||||
schedule.append(
|
||||
{
|
||||
"phase": "曲目练习",
|
||||
"duration": f"{piece_time}分钟",
|
||||
"content": "复习所学曲目 + 预习新曲目",
|
||||
"purpose": "提升演奏能力",
|
||||
}
|
||||
)
|
||||
|
||||
# 5. 总结
|
||||
schedule.append(
|
||||
{
|
||||
"phase": "总结",
|
||||
"duration": "3分钟",
|
||||
"content": "练习记录 + 问题检查 + 明日计划",
|
||||
"purpose": "巩固学习成果",
|
||||
}
|
||||
)
|
||||
|
||||
return schedule
|
||||
|
||||
|
||||
def _get_severity_advice(severity):
|
||||
"""根据严重程度给出建议"""
|
||||
advice = {
|
||||
@@ -236,7 +157,7 @@ def _get_severity_advice(severity):
|
||||
|
||||
|
||||
def generate_ai_report(
|
||||
student_name, wechat_nickname, problems, practice_time, time_config, template_id=None, dry_run=False
|
||||
student_name, wechat_nickname, problems, practice_time, time_config, template_id=None, dry_run=False, goals=None
|
||||
):
|
||||
"""
|
||||
使用AI生成个性化的练习报告
|
||||
@@ -249,6 +170,11 @@ def generate_ai_report(
|
||||
time_config: 时间配置 {total, basic, tech, piece}
|
||||
template_id: 可选的AI提示词模板ID
|
||||
dry_run: 如果为True,只返回提示词不调用API
|
||||
goals: 目标列表 [{goal_id, goal_name, goal_content, status, mastery_level}]
|
||||
practice_time: 练习时间描述
|
||||
time_config: 时间配置 {total, basic, tech, piece}
|
||||
template_id: 可选的AI提示词模板ID
|
||||
dry_run: 如果为True,只返回提示词不调用API
|
||||
|
||||
Returns:
|
||||
tuple: (prompt, ai_report, error) - 提示词、AI报告内容、错误信息
|
||||
@@ -324,14 +250,25 @@ def generate_ai_report(
|
||||
f"**详细内容和练习方法**:\n{content}"
|
||||
)
|
||||
|
||||
# 构建学员未达成目标列表(只包含未完成的目标)
|
||||
student_goals = []
|
||||
if goals:
|
||||
for g in goals:
|
||||
# 只处理未完成的目标
|
||||
if g.get("status") != "已完成":
|
||||
student_goals.append(
|
||||
f"- **{g['goal_name']}**\n 内容:{g.get('goal_content', '未提供具体内容')}"
|
||||
)
|
||||
|
||||
# 使用配置的模板,如果为空则使用默认模板
|
||||
if prompt_template:
|
||||
prompt = prompt_template.format(
|
||||
student_name=student_name,
|
||||
wechat_nickname=wechat_nickname or "未设置",
|
||||
practice_time=practice_time,
|
||||
student_problems="\n".join(student_problems),
|
||||
problems="\n\n".join(problems_full),
|
||||
student_problems="\n".join(student_problems) if student_problems else "无",
|
||||
problems="\n\n".join(problems_full) if problems_full else "无",
|
||||
student_goals="\n".join(student_goals) if student_goals else "无",
|
||||
)
|
||||
else:
|
||||
# 默认模板(向后兼容)
|
||||
@@ -363,7 +300,15 @@ def generate_ai_report(
|
||||
|
||||
# 如果是 dry_run(预览模式),直接返回提示词不调用API
|
||||
if dry_run:
|
||||
return prompt, None, None
|
||||
# 计算各部分字数
|
||||
student_problems_text = "\n".join(student_problems) if student_problems else "无"
|
||||
problems_text = "\n\n".join(problems_full) if problems_full else "无"
|
||||
student_goals_text = "\n".join(student_goals) if student_goals else "无"
|
||||
return prompt, None, None, {
|
||||
"student_problems_length": len(student_problems_text),
|
||||
"problems_length": len(problems_text),
|
||||
"student_goals_length": len(student_goals_text),
|
||||
}
|
||||
|
||||
# 调用API
|
||||
headers = {
|
||||
@@ -407,7 +352,7 @@ def generate_ai_report(
|
||||
content = (
|
||||
result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
)
|
||||
return prompt, content, None
|
||||
return prompt, content, None, None
|
||||
else:
|
||||
error_msg = f"API错误: {response.status_code}"
|
||||
try:
|
||||
@@ -415,9 +360,9 @@ def generate_ai_report(
|
||||
error_msg = error_detail.get("error", {}).get("message", error_msg)
|
||||
except:
|
||||
pass
|
||||
return prompt, None, error_msg
|
||||
return prompt, None, error_msg, None
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return prompt, None, "API请求超时,请稍后重试"
|
||||
return prompt, None, "API请求超时,请稍后重试", None
|
||||
except Exception as e:
|
||||
return prompt, None, f"调用API失败: {str(e)}"
|
||||
return prompt, None, f"调用API失败: {str(e)}", None
|
||||
|
||||
Reference in New Issue
Block a user