reminder/app/Http/Controllers/PatientController.php

467 lines
16 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 App\Models\Patient;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use Illuminate\Support\Facades\Response;
class PatientController extends Controller
{
/**
* 获取当前用户的患者查询构建器
*/
private function userPatients()
{
return Patient::where('user_id', Auth::id());
}
/**
* 显示患者列表页面
*/
public function index(Request $request)
{
$query = $this->userPatients();
// 搜索功能
if ($request->filled('search')) {
$search = $request->input('search');
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('phone', 'like', "%{$search}%")
->orWhere('address', 'like', "%{$search}%");
});
}
// 诊断筛选
if ($request->filled('diagnosis')) {
$query->where('diagnosis', 'like', "%{$request->input('diagnosis')}%");
}
$patients = $query->orderBy('discharge_date', 'desc')->paginate(20);
return view('patients.index', compact('patients'));
}
/**
* 显示需要随访提醒的患者(只显示当月需要随访的)
*/
public function reminders(Request $request)
{
$patients = $this->userPatients()->get();
// 只筛选当月需要随访的患者
$reminders = $patients->filter(function ($patient) {
return $patient->needsFollowUpThisMonth();
})->map(function ($patient) {
return [
'patient' => $patient,
'follow_up_date' => $patient->getCurrentMonthFollowUpDate(),
'follow_up_number' => $patient->getCurrentMonthFollowUpNumber(),
];
})->sortBy(function ($r) {
return $r['follow_up_date'];
});
// 统计数据
$stats = [
'count' => $reminders->count(),
'completed' => $patients->filter(fn($p) => $p->isCompleted())->count(),
'total' => $patients->count(),
];
return view('patients.reminders', compact('reminders', 'stats'));
}
/**
* 显示导入页面
*/
public function showImport()
{
return view('patients.import');
}
/**
* 处理Excel导入
*/
public function import(Request $request)
{
$request->validate([
'file' => 'required|file|mimes:xlsx,xls,csv|max:10240',
]);
$file = $request->file('file');
$path = $file->getRealPath();
try {
$data = $this->parseExcel($path, $file->getClientOriginalExtension());
$imported = 0;
$errors = [];
$headerSkipped = false;
foreach ($data as $index => $row) {
try {
// 跳过空行
if (empty($row) || !is_array($row)) {
continue;
}
$firstCell = trim((string)($row[0] ?? ''));
// 跳过表头行(第一行或包含"姓名"的行)
if (!$headerSkipped && ($index === 0 || mb_strpos($firstCell, '姓名') !== false)) {
$headerSkipped = true;
continue;
}
// 跳过空行
if (empty($firstCell)) {
continue;
}
// 解析数据
$patientData = $this->parseRow($row);
if ($patientData) {
// 关联到当前用户
$patientData['user_id'] = Auth::id();
Patient::create($patientData);
$imported++;
}
} catch (\Exception $e) {
$errors[] = "" . ($index + 1) . " 行导入失败: " . $e->getMessage();
}
}
$message = "成功导入 {$imported} 条记录。";
if (count($errors) > 0) {
$message .= " " . count($errors) . " 条记录导入失败。";
}
return redirect()->route('patients.index')
->with('success', $message)
->with('errors', $errors);
} catch (\Exception $e) {
return back()->withErrors(['file' => '文件解析失败: ' . $e->getMessage()]);
}
}
/**
* 解析Excel/CSV文件 (使用 xlswriter 扩展)
*/
private function parseExcel(string $path, string $extension): array
{
$data = [];
if ($extension === 'csv') {
// 检测文件编码并转换
$content = file_get_contents($path);
$encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true);
if ($encoding && $encoding !== 'UTF-8') {
$content = mb_convert_encoding($content, 'UTF-8', $encoding);
$tempPath = sys_get_temp_dir() . '/converted_' . time() . '.csv';
file_put_contents($tempPath, $content);
$path = $tempPath;
}
$handle = fopen($path, 'r');
// 跳过 BOM
$bom = fread($handle, 3);
if ($bom !== "\xEF\xBB\xBF") {
rewind($handle);
}
while (($row = fgetcsv($handle)) !== false) {
$data[] = $row;
}
fclose($handle);
// 清理临时文件
if (isset($tempPath) && file_exists($tempPath)) {
@unlink($tempPath);
}
} elseif ($extension === 'xls') {
// .xls 是旧版 Excel 格式xlswriter 不支持读取
// 尝试使用 PhpSpreadsheet 或提示用户转换格式
if (class_exists(\PhpOffice\PhpSpreadsheet\IOFactory::class)) {
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path);
$worksheet = $spreadsheet->getActiveSheet();
foreach ($worksheet->getRowIterator() as $row) {
$rowData = [];
$cellIterator = $row->getCellIterator();
$cellIterator->setIterateOnlyExistingCells(false);
foreach ($cellIterator as $cell) {
$rowData[] = $cell->getValue();
}
$data[] = $rowData;
}
} else {
throw new \Exception('不支持 .xls 格式Excel 97-2003。请将文件另存为 .xlsx 格式Excel 2007+)或 .csv 格式后重新上传。操作方法:在 Excel 中打开文件 → 文件 → 另存为 → 选择"Excel 工作簿 (*.xlsx)"或"CSV UTF-8"');
}
} else {
// 使用 xlswriter 扩展解析 xlsx
if (!class_exists(\Vtiful\Kernel\Excel::class)) {
throw new \Exception('xlswriter 扩展未安装,请安装后重试');
}
$config = ['path' => sys_get_temp_dir()];
$excel = new \Vtiful\Kernel\Excel($config);
// 复制文件到临时目录
$tempFile = sys_get_temp_dir() . '/' . uniqid('import_') . '.' . $extension;
copy($path, $tempFile);
// 读取Excel数据
$data = $excel->openFile(basename($tempFile))
->openSheet()
->getSheetData();
// 如果没有读取到数据,可能是格式问题
if (empty($data)) {
throw new \Exception('无法读取 Excel 文件内容。请确保文件是有效的 .xlsx 格式,或尝试另存为 .csv 格式后重新上传。');
}
// 释放 Excel 对象
unset($excel);
// 清理临时文件 (使用 @ 抑制 Windows 上的文件锁定警告)
if (file_exists($tempFile)) {
@unlink($tempFile);
}
}
return $data;
}
/**
* 解析单行数据
*/
private function parseRow(array $row): ?array
{
// 期望的列顺序: 姓名, 性别, 年龄, 出院诊断, 转诊时间, 户籍地址, 联系方式, 备注
if (count($row) < 5) {
return null;
}
$name = trim((string)($row[0] ?? ''));
if (empty($name) || mb_strpos($name, '姓名') !== false) {
return null;
}
// 解析日期
$dateValue = $row[4] ?? '';
$dischargeDate = $this->parseDate($dateValue);
if (!$dischargeDate) {
throw new \Exception("日期格式无效: {$dateValue}");
}
// 处理年龄(可能是数字或字符串)
$age = $row[2] ?? 0;
if (is_string($age)) {
$age = (int)preg_replace('/[^0-9]/', '', $age);
}
// 处理电话号码(可能是科学计数法或数字)
$phone = $row[6] ?? '';
if (is_numeric($phone)) {
$phone = number_format($phone, 0, '', '');
}
return [
'name' => $name,
'gender' => trim((string)($row[1] ?? '未知')),
'age' => (int)$age,
'diagnosis' => trim((string)($row[3] ?? '')),
'discharge_date' => $dischargeDate,
'address' => trim((string)($row[5] ?? '')),
'phone' => trim((string)$phone),
'remark' => trim((string)($row[7] ?? '')),
'follow_up_count' => 0,
];
}
/**
* 解析日期
*/
private function parseDate($value): ?string
{
if (empty($value)) {
return null;
}
$value = trim((string)$value);
// Excel 日期序列号纯数字且大于25569
if (is_numeric($value) && (float)$value > 25569 && (float)$value < 50000) {
// Excel日期是从1900年1月1日开始的天数
// 25569 是 1970-01-01 的Excel序列号
$unixTimestamp = ((float)$value - 25569) * 86400;
return date('Y-m-d', (int)$unixTimestamp);
}
// 字符串格式日期 - 各种格式
$formats = [
'Y.m.d', // 2025.12.01
'Y-m-d', // 2025-12-01
'Y/m/d', // 2025/12/01
'Y.n.j', // 2025.1.1 (无前导零)
'Y-n-j', // 2025-1-1
'Y/n/j', // 2025/1/1
'd/m/Y', // 01/12/2025
'm/d/Y', // 12/01/2025
];
foreach ($formats as $format) {
$date = \DateTime::createFromFormat($format, $value);
if ($date) {
// 验证日期是否有效
$errors = \DateTime::getLastErrors();
if (empty($errors['warning_count']) && empty($errors['error_count'])) {
return $date->format('Y-m-d');
}
}
}
// 尝试通过 strtotime处理更多格式
$timestamp = strtotime(str_replace('.', '-', $value));
if ($timestamp && $timestamp > 0) {
return date('Y-m-d', $timestamp);
}
return null;
}
/**
* 导出本月随访列表 (使用 xlswriter 生成 Excel)
*/
public function export(Request $request)
{
$patients = $this->userPatients()->get();
// 只筛选本月需要随访的患者
$reminders = $patients->filter(function ($patient) {
return $patient->needsFollowUpThisMonth();
})->map(function ($patient) {
return [
'patient' => $patient,
'follow_up_date' => $patient->getCurrentMonthFollowUpDate(),
'follow_up_number' => $patient->getCurrentMonthFollowUpNumber(),
];
})->sortBy(function ($r) {
return $r['follow_up_date'];
});
// 使用 xlswriter 生成 Excel 文件
$config = ['path' => sys_get_temp_dir()];
$filename = '本月随访_' . date('Y-m') . '.xlsx';
$excel = new \Vtiful\Kernel\Excel($config);
// 创建文件并设置表头
$fileHandle = $excel->fileName($filename)
->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '随访日期', '第几次随访', '户籍地址', '联系方式', '备注']);
// 设置表头样式
$headerStyle = $fileHandle->getHandle();
$format = new \Vtiful\Kernel\Format($headerStyle);
$boldFormat = $format->bold()->toResource();
// 添加数据行
$rows = [];
foreach ($reminders as $reminder) {
$p = $reminder['patient'];
$rows[] = [
$p->name,
$p->gender,
$p->age,
$p->diagnosis,
$p->discharge_date->format('Y-m-d'),
$reminder['follow_up_date']?->format('Y-m-d') ?? '',
'第' . $reminder['follow_up_number'] . '次',
$p->address ?? '',
$p->phone ?? '',
$p->remark ?? '',
];
}
$fileHandle->data($rows)->output();
$filePath = $config['path'] . '/' . $filename;
return Response::download($filePath, $filename, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
])->deleteFileAfterSend(true);
}
/**
* 标记已随访
*/
public function markFollowedUp(Patient $patient)
{
// 验证患者属于当前用户
if ($patient->user_id !== Auth::id()) {
abort(403, '无权操作此患者');
}
if (!$patient->isCompleted()) {
$patient->follow_up_count += 1;
$patient->last_follow_up_date = Carbon::today();
$patient->save();
}
return back()->with('success', "{$patient->name} 已标记为完成第 {$patient->follow_up_count} 次随访");
}
/**
* 删除患者
*/
public function destroy(Patient $patient)
{
// 验证患者属于当前用户
if ($patient->user_id !== Auth::id()) {
abort(403, '无权操作此患者');
}
$name = $patient->name;
$patient->delete();
return back()->with('success', "已删除患者: {$name}");
}
/**
* 下载导入模板 (使用 xlswriter 生成 Excel)
*/
public function downloadTemplate()
{
$config = ['path' => sys_get_temp_dir()];
$filename = '导入模板.xlsx';
$excel = new \Vtiful\Kernel\Excel($config);
// 创建文件
$fileHandle = $excel->fileName($filename)
->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '户籍地址', '联系方式', '备注']);
// 添加示例数据
$data = [
['张三', '男', 65, '脑卒中', '2025.12.01', '北京市朝阳区', '13800138000', '常住'],
['李四', '女', 70, '慢性肾脏病', '2025.11.15', '上海市浦东新区', '13900139000', ''],
['王五', '男', 58, '心肌梗塞', '2025.10.20', '广州市天河区', '13700137000', ''],
];
$fileHandle->data($data)->output();
$filePath = $config['path'] . '/' . $filename;
return Response::download($filePath, $filename, [
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
])->deleteFileAfterSend(true);
}
}