CaiWuXiTong/app/Business/SystemizationTodoWorkbookImporter.php
yueyang 3cff23c595 新增财务系统化待办与完成截图
新增财务系统化待办与完成截图
2026-10-25 16:23:08 +08:00

250 lines
9.2 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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\Business;
use App\Models\Business\SystemizationTodo;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Modules\EcommerceFinance\Services\PayablesWorkbookStreamReader;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
use RuntimeException;
class SystemizationTodoWorkbookImporter
{
private const REQUIRED_HEADERS = [
'category' => '类型',
'section' => '内容',
'title' => '待办事项',
'owner' => '总负责人',
'assignee' => '责任人',
'start' => '开始时间',
'finish' => '完成时间',
'progress' => '完成情况',
'notes' => '备注',
];
/**
* 导入财务系统化待办。只新增系统中不存在的来源行,不覆盖日常维护后的状态与截图。
*
* @return array{created:int,skipped:int,sheet:string}
*/
public function import(string $path, int $companyId, ?int $userId = null): array
{
if (! is_file($path)) {
throw new RuntimeException('找不到待办 Excel 文件。');
}
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if ($extension !== 'xlsx') {
throw new RuntimeException('系统化待办仅支持 .xlsx 文件。');
}
$reader = PayablesWorkbookStreamReader::forFile($path, $extension);
foreach ($reader->sheetNames() as $sheetIndex => $sheetName) {
$headerInfo = $this->headerMap($reader, $sheetIndex);
if ($headerInfo === null) {
continue;
}
$header = $headerInfo['columns'];
$headerRow = $headerInfo['row'];
$created = 0;
$skipped = 0;
DB::transaction(function () use (
$reader,
$sheetIndex,
$sheetName,
$header,
$headerRow,
$companyId,
$userId,
&$created,
&$skipped
): void {
$category = '';
$section = '';
foreach ($reader->rows($sheetIndex) as $rowNumber => $cells) {
if ($rowNumber <= $headerRow) {
continue;
}
$categoryValue = $this->cell($cells, $header['category'] ?? null);
if ($categoryValue !== '') {
$category = $categoryValue;
}
$sectionValue = $this->cell($cells, $header['section'] ?? null);
if ($sectionValue !== '') {
$section = $sectionValue;
}
$title = $this->cell($cells, $header['title'] ?? null);
if ($title === '') {
continue;
}
$owner = $this->cell($cells, $header['owner'] ?? null);
$assignee = $this->cell($cells, $header['assignee'] ?? null);
$sourceKey = hash(
'sha256',
implode('|', [$companyId, $sheetName, $rowNumber, $category, $section, $title, $owner, $assignee])
);
$exists = SystemizationTodo::withoutGlobalScopes()
->where('company_id', $companyId)
->where('source_key', $sourceKey)
->exists();
if ($exists) {
$skipped++;
continue;
}
$startText = $this->cell($cells, $header['start'] ?? null);
$finishText = $this->cell($cells, $header['finish'] ?? null);
[$startDate, $startNote] = $this->dateParts($startText);
[$finishDate, $finishNote] = $this->dateParts($finishText);
$progress = $this->cell($cells, $header['progress'] ?? null);
$status = $this->status($progress, $startText, $finishText);
$completedAt = $status === SystemizationTodo::STATUS_COMPLETED
? ($finishDate ?? now()->toDateString())
: null;
SystemizationTodo::withoutGlobalScopes()->create([
'company_id' => $companyId,
'category' => $category !== '' ? $category : null,
'section' => $section !== '' ? $section : null,
'title' => $title,
'owner' => $owner !== '' ? $owner : null,
'assignee' => $assignee !== '' ? $assignee : null,
'start_date' => $startDate,
'start_note' => $startNote !== '' ? $startNote : null,
'due_date' => $finishDate,
'due_note' => $finishNote !== '' ? $finishNote : null,
'status' => $status,
'completed_at' => $completedAt,
'progress' => $progress !== '' ? $progress : null,
'notes' => ($notes = $this->cell($cells, $header['notes'] ?? null)) !== '' ? $notes : null,
'sort_order' => $rowNumber,
'source_key' => $sourceKey,
'imported_at' => now(),
'created_by' => $userId,
'updated_by' => $userId,
]);
$created++;
}
});
return ['created' => $created, 'skipped' => $skipped, 'sheet' => $sheetName];
}
throw new RuntimeException(
'未找到待办表头。请确认工作表包含:类型、内容、待办事项、总负责人、责任人、开始时间、完成时间、完成情况、备注。'
);
}
/**
* @return array{row:int,columns:array<string,int>}|null
*/
private function headerMap(PayablesWorkbookStreamReader $reader, int $sheetIndex): ?array
{
foreach ($reader->rows($sheetIndex, null, 12) as $rowNumber => $cells) {
$normalized = [];
foreach ($cells as $column => $value) {
$value = $this->normalizeHeader($this->text($value));
if ($value !== '') {
$normalized[$value] = $column;
}
}
$mapped = [];
foreach (self::REQUIRED_HEADERS as $field => $label) {
$needle = $this->normalizeHeader($label);
foreach ($normalized as $header => $column) {
if ($header === $needle || str_starts_with($header, $needle)) {
$mapped[$field] = $column;
break;
}
}
}
if (isset($mapped['title'])
&& isset($mapped['owner'])
&& isset($mapped['assignee'])) {
return ['row' => $rowNumber, 'columns' => $mapped];
}
}
return null;
}
private function normalizeHeader(string $value): string
{
return preg_replace('/[\s ()()]+/u', '', $value) ?? '';
}
/**
* @param array<int,mixed> $cells
*/
private function cell(array $cells, ?int $column): string
{
return $column === null ? '' : $this->text($cells[$column] ?? null);
}
private function text(mixed $value): string
{
if ($value === null) {
return '';
}
if (is_bool($value)) {
return $value ? '1' : '0';
}
return trim(str_replace("\u{00A0}", ' ', (string) $value));
}
/**
* @return array{0:string|null,1:string}
*/
private function dateParts(string $value): array
{
$value = trim($value);
if ($value === '' || $value === '已完成') {
return [null, ''];
}
if (is_numeric($value)) {
$serial = (float) $value;
if ($serial >= 20000 && $serial <= 80000) {
return [ExcelDate::excelToDateTimeObject($serial)->format('Y-m-d'), ''];
}
}
foreach (['Y-m-d', 'Y/m/d', 'Y.m.d', 'Y年n月j日'] as $format) {
try {
$date = Carbon::createFromFormat($format, $value);
if ($date !== false) {
return [$date->format('Y-m-d'), ''];
}
} catch (\Throwable) {
// 不是标准日期,保留原值作为时间备注。
}
}
return [null, mb_substr($value, 0, 100)];
}
private function status(string $progress, string $startNote, string $finishNote): string
{
if (str_contains($progress, '已完成')) {
return SystemizationTodo::STATUS_COMPLETED;
}
if ($progress !== '') {
return str_contains($progress, '延期')
? SystemizationTodo::STATUS_DELAYED
: SystemizationTodo::STATUS_IN_PROGRESS;
}
if (str_contains($startNote, '延期') || str_contains($finishNote, '延期')) {
return SystemizationTodo::STATUS_DELAYED;
}
return SystemizationTodo::STATUS_PENDING;
}
}