weekreport/app/Http/Controllers/WeekReportController.php
2026-01-09 11:00:31 +08:00

430 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Barryvdh\DomPDF\Facade\Pdf;
use Parsedown;
class WeekReportController extends Controller
{
/**
* 显示周报生成页面
*/
public function index()
{
return view('weekreport.index');
}
/**
* 上传任务图片
*/
public function uploadImage(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg,gif,webp|max:10240',
]);
try {
$file = $request->file('image');
$filename = time() . '_' . uniqid() . '.' . $file->getClientOriginalExtension();
// 存储到public目录
$path = $file->storeAs('uploads', $filename, 'public');
return response()->json([
'success' => true,
'path' => $path,
'url' => asset('storage/' . $path),
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '图片上传失败: ' . $e->getMessage(),
], 500);
}
}
/**
* 生成周报
*/
public function generate(Request $request)
{
$request->validate([
'tasks' => 'required|array|min:1',
'tasks.*.description' => 'required|string|max:2000',
'tasks.*.image' => 'nullable|string',
'week_start' => 'nullable|date',
'week_end' => 'nullable|date',
'author' => 'nullable|string|max:100',
'next_week_tasks' => 'nullable|array',
'next_week_tasks.*' => 'nullable|string|max:2000',
]);
$tasks = $request->input('tasks');
$weekStart = $request->input('week_start') ?? date('Y-m-d', strtotime('monday this week'));
$weekEnd = $request->input('week_end') ?? date('Y-m-d', strtotime('sunday this week'));
$author = (string) ($request->input('author') ?? '');
$nextWeekTasks = $request->input('next_week_tasks', []);
// 构建提示词
$taskList = $this->buildTaskList($tasks);
$nextWeekList = $this->buildNextWeekTaskList($nextWeekTasks);
$prompt = $this->buildPrompt($taskList, $weekStart, $weekEnd, $author, $nextWeekList);
try {
// 调用千问API
$reportContent = $this->callQwenApi($prompt);
// 插入图片到报告中
$reportWithImages = $this->insertImagesToReport($reportContent, $tasks);
return response()->json([
'success' => true,
'report' => $reportWithImages,
'raw_report' => $reportContent,
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => '生成周报失败: ' . $e->getMessage(),
], 500);
}
}
/**
* 构建任务列表文本
*/
private function buildTaskList(array $tasks): string
{
$list = [];
foreach ($tasks as $index => $task) {
$num = $index + 1;
$desc = trim($task['description']);
$hasImage = !empty($task['image']) ? '(有截图)' : '';
$list[] = "{$num}. {$desc}{$hasImage}";
}
return implode("\n", $list);
}
/**
* 构建下周任务列表文本
*/
private function buildNextWeekTaskList(array $tasks): string
{
if (empty($tasks)) {
return '';
}
$list = [];
foreach ($tasks as $index => $desc) {
if (!empty(trim($desc))) {
$num = $index + 1;
$list[] = "{$num}. " . trim($desc);
}
}
return implode("\n", $list);
}
/**
* 构建完整提示词
*/
private function buildPrompt(string $taskList, string $weekStart, string $weekEnd, string $author, string $nextWeekList = ''): string
{
$authorInfo = $author ? "作者:{$author}" : "";
// 根据是否有下周计划来调整要求
$nextWeekSection = '';
$nextWeekRequirement = '';
if (!empty(trim($nextWeekList))) {
$nextWeekSection = "\n\n下周计划任务(用户提供):\n{$nextWeekList}";
$nextWeekRequirement = "\n8. 添加\"下周工作计划\"部分,根据用户提供的下周任务列表进行专业化描述和扩展";
} else {
$nextWeekRequirement = "\n8. 不要添加下周工作计划部分(用户未提供)";
}
return <<<PROMPT
你是一位专业的周报撰写助手。请根据以下任务列表生成一份专业、详细的Markdown格式工作周报。
周报时间范围:{$weekStart} 至 {$weekEnd}
{$authorInfo}
本周完成的任务:
{$taskList}{$nextWeekSection}
要求:
1. 使用Markdown格式输出
2. 包含周报标题(使用一级标题)
3. 包含时间范围和作者信息(如果提供)
4. 对每个任务进行合理的分类和扩展描述
5. 每个任务需要包含:
- 任务名称
- 任务详细描述(根据原始描述进行适当扩展和专业化表述)
- 完成情况
6. 添加一个"本周工作总结"部分
7. 【重要】关于图片占位符:只有任务描述中明确标注了"(有截图)"的任务,才在该任务描述末尾单独一行添加占位符"[图片占位符-任务X]"X为任务序号。没有标注"(有截图)"的任务,绝对不要添加任何图片占位符{$nextWeekRequirement}
9. 语言专业、简洁、条理清晰
请直接输出Markdown格式的周报内容不要有任何额外说明。
PROMPT;
}
/**
* 调用千问API
*/
private function callQwenApi(string $prompt): string
{
$apiKey = config('services.qwen.api_key');
$apiUrl = config('services.qwen.api_url');
$model = config('services.qwen.model');
if (empty($apiKey)) {
throw new \Exception('API Key 未配置');
}
$response = Http::timeout(120)
->withoutVerifying()
->withHeaders([
'Authorization' => "Bearer {$apiKey}",
'Content-Type' => 'application/json',
])
->post($apiUrl, [
'model' => $model,
'messages' => [
[
'role' => 'system',
'content' => '你是一位专业的工作周报撰写助手,擅长将简单的任务描述转化为专业、详细的周报内容。'
],
[
'role' => 'user',
'content' => $prompt
]
],
'temperature' => 0.7,
'max_tokens' => 4000,
]);
if (!$response->successful()) {
$error = $response->json('error.message') ?? $response->body();
throw new \Exception("API调用失败: {$error}");
}
$data = $response->json();
if (!isset($data['choices'][0]['message']['content'])) {
throw new \Exception('API返回数据格式错误');
}
return $data['choices'][0]['message']['content'];
}
/**
* 在报告中插入图片
*/
private function insertImagesToReport(string $report, array $tasks): string
{
foreach ($tasks as $index => $task) {
$num = $index + 1;
// 支持多种占位符格式
$placeholders = [
"[图片占位符-任务{$num}]",
"[图片占位符-{$num}]",
"[图片占位符{$num}]",
"【图片占位符-任务{$num}",
"【图片占位符-{$num}",
"【图片占位符{$num}",
];
if (!empty($task['image'])) {
$imageUrl = asset('storage/' . $task['image']);
$imageMarkdown = "\n\n![任务{$num}截图]({$imageUrl})\n";
foreach ($placeholders as $placeholder) {
$report = str_replace($placeholder, $imageMarkdown, $report);
}
} else {
// 没有图片时移除所有占位符
foreach ($placeholders as $placeholder) {
$report = str_replace($placeholder, '', $report);
}
}
}
// 清理所有未使用的占位符(各种格式)
$report = preg_replace('/\[图片占位符[-]?任务?\d*\]/', '', $report);
$report = preg_replace('/【图片占位符[-]?任务?\d*】/', '', $report);
$report = preg_replace('/\[?图片占位符[-—]?\d+\]?/', '', $report);
// 清理可能产生的多余空行
$report = preg_replace('/\n{3,}/', "\n\n", $report);
return $report;
}
/**
* 下载Markdown格式
*/
public function downloadMarkdown(Request $request)
{
$request->validate([
'content' => 'required|string',
'filename' => 'nullable|string|max:100',
]);
$content = $request->input('content');
$filename = $request->input('filename', '周报_' . date('Y-m-d')) . '.md';
return response($content)
->header('Content-Type', 'text/markdown; charset=utf-8')
->header('Content-Disposition', 'attachment; filename="' . $filename . '"');
}
/**
* 下载PDF格式
*/
public function downloadPdf(Request $request)
{
$request->validate([
'content' => 'required|string',
'filename' => 'nullable|string|max:100',
]);
$markdownContent = $request->input('content');
$filename = $request->input('filename', '周报_' . date('Y-m-d')) . '.pdf';
// 将Markdown转换为HTML
$parsedown = new Parsedown();
$htmlContent = $parsedown->text($markdownContent);
// 添加样式
$styledHtml = $this->wrapHtmlWithStyle($htmlContent);
// 生成PDF - 启用远程字体和图片
$pdf = Pdf::loadHTML($styledHtml);
$pdf->setPaper('A4', 'portrait');
$pdf->setOption('isRemoteEnabled', true);
$pdf->setOption('defaultFont', 'sans-serif');
return $pdf->download($filename);
}
/**
* 为HTML添加样式
*/
private function wrapHtmlWithStyle(string $html): string
{
return <<<HTML
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<style>
@font-face {
font-family: 'NotoSansSC';
src: url('https://fonts.gstatic.com/s/notosanssc/v36/k3kCo84MPvpLmixcA63oeAL7Iqp5IZJF9bmaG9_FnYg.woff2') format('woff2');
font-weight: normal;
font-style: normal;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'NotoSansSC', 'SimHei', 'Microsoft YaHei', sans-serif;
font-size: 14px;
line-height: 1.8;
color: #333;
padding: 40px;
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 24px;
color: #1a1a1a;
border-bottom: 3px solid #4a90d9;
padding-bottom: 10px;
margin-bottom: 20px;
}
h2 {
font-size: 18px;
color: #2c3e50;
margin-top: 25px;
margin-bottom: 15px;
padding-left: 10px;
border-left: 4px solid #4a90d9;
}
h3 {
font-size: 16px;
color: #34495e;
margin-top: 20px;
margin-bottom: 10px;
}
p {
margin-bottom: 12px;
text-align: justify;
}
ul, ol {
margin-left: 25px;
margin-bottom: 15px;
}
li {
margin-bottom: 8px;
}
img {
max-width: 100%;
height: auto;
margin: 15px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
code {
background-color: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: Consolas, monospace;
}
pre {
background-color: #f5f5f5;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
margin-bottom: 15px;
}
blockquote {
border-left: 4px solid #ddd;
padding-left: 15px;
color: #666;
margin: 15px 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
hr {
border: none;
border-top: 1px solid #ddd;
margin: 20px 0;
}
</style>
</head>
<body>
{$html}
</body>
</html>
HTML;
}
}