weekreport/app/Http/Controllers/WeekReportController.php

432 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)
{
// 设置较长的执行时间因为AI生成可能需要较长时间
set_time_limit(300); // 5分钟
$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
你是一位经验丰富的职场人士,擅长撰写简洁实用的工作周报。请根据以下任务信息,用自然、朴实的语言写一份周报。
时间:{$weekStart} 至 {$weekEnd}
{$authorInfo}
本周工作内容:
{$taskList}{$nextWeekSection}
写作要求:
1. 使用Markdown格式
2. 标题简洁,如"工作周报"或"本周工作汇报"
3. 语言要自然朴实,像正常人写的,避免过于华丽或模板化的表述
4. 不要使用"高效完成"、"圆满完成"、"取得显著成效"等套话
5. 每个任务用简短的一两句话描述即可,说清楚做了什么
6. 可以按项目或类型简单分组,但不要过度分类
7. 【重要】只有标注了"(有截图)"的任务才添加"[图片占位符-任务X]",其他任务不要添加{$nextWeekRequirement}
9. 总结部分一两句话概括即可,不要写得太官方
10. 不要出现具体的完成时间、耗时等信息
11. 整体篇幅适中,不要太长
直接输出周报内容:
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(180)
->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;
}
}