CaiWuXiTong/app/Console/Commands/ImportDingAttendanceSchedules.php
renshen c47af23709 fix: 日志漏写按公司排班判断工作日 (#1030)
合并 PR #1030:日志漏写扣分改用钉钉公司排班判断工作日。
2026-11-10 10:57:47 +08:00

178 lines
6.5 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Services\Auth\FinanceRoster;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Modules\EcommerceFinance\Repositories\DingAttendanceMirrorRepository;
use Throwable;
class ImportDingAttendanceSchedules extends Command
{
protected $signature = 'business-mirror:import-ding-attendance-schedules
{--file= : DWS attendance schedule get 返回的 JSON 文件}
{--stdin : 从标准输入读取 JSON}
{--from= : 查询开始日期 YYYY-MM-DD}
{--to= : 查询结束日期 YYYY-MM-DD}';
protected $description = '导入钉钉人员排班,供日志漏写扣分识别公司工作日和休息日';
public function handle(
DingAttendanceMirrorRepository $repository,
FinanceRoster $roster,
): int {
if (! $repository->scheduleAvailable()) {
$this->error('钉钉排班镜像表尚未建立,请先执行数据库迁移。');
return self::FAILURE;
}
$raw = $this->option('stdin')
? stream_get_contents(STDIN)
: (($path = trim((string) $this->option('file'))) !== '' && is_file($path)
? file_get_contents($path)
: '');
if (! is_string($raw) || trim($raw) === '') {
$repository->recordScheduleFailure('dingtalk_schedule_payload_missing');
$this->error('未读取到钉钉排班 JSON。');
return self::FAILURE;
}
try {
$decoded = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
$records = is_array($decoded) && is_array($decoded['result'] ?? null)
? $decoded['result']
: (is_array($decoded) && array_is_list($decoded) ? $decoded : []);
$names = [];
foreach ($roster->cachedFinanceMembers() as $member) {
$userId = trim((string) ($member['userid'] ?? ''));
if ($userId !== '') {
$names[$userId] = trim((string) ($member['name'] ?? ''));
}
}
$now = now();
$rows = [];
$userIds = [];
$dates = [];
foreach ($records as $record) {
if (! is_array($record)) {
continue;
}
$scheduleId = trim((string) ($record['id'] ?? ''));
$userId = trim((string) ($record['userId'] ?? ''));
$workDate = $this->dateValue($record['workDate'] ?? null);
if ($scheduleId === '' || $userId === '' || $workDate === null) {
continue;
}
$userIds[$userId] = $userId;
$dates[$workDate] = $workDate;
$rawRest = strtoupper(trim((string) ($record['isRest'] ?? 'N')));
$rows[$scheduleId] = [
'schedule_id' => mb_substr($scheduleId, 0, 64),
'user_id' => mb_substr($userId, 0, 128),
'user_name' => mb_substr($names[$userId] ?? '', 0, 191),
'work_date' => $workDate,
'group_id' => $this->unsignedInteger($record['groupId'] ?? null),
'class_id' => $this->unsignedInteger($record['classId'] ?? null),
'is_rest' => $rawRest === 'Y' ? 'Y' : 'N',
'check_status' => $this->nullableString($record['checkStatus'] ?? null, 32),
'time_changed' => (bool) ($record['timeChanged'] ?? false),
'source_payload' => json_encode($record, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'synced_at' => $now,
'created_at' => $now,
'updated_at' => $now,
];
}
if ($rows === []) {
$repository->recordScheduleFailure('dingtalk_schedule_empty');
$this->error('钉钉排班 JSON 中没有可导入的排班记录。');
return self::FAILURE;
}
$from = $this->dateOption('from') ?? min($dates);
$to = $this->dateOption('to') ?? max($dates);
if ($from > $to) {
throw new \InvalidArgumentException('排班导入开始日期不能晚于结束日期。');
}
$count = $repository->replaceScheduleWindow(
array_values($rows),
array_values($userIds),
$from,
$to,
);
$repository->recordScheduleSuccess($count, $from, $to);
$this->info(sprintf(
'钉钉排班导入完成:%d 人、%d 天、%d 条排班。',
count($userIds),
count($dates),
$count,
));
return self::SUCCESS;
} catch (Throwable $exception) {
$repository->recordScheduleFailure('dingtalk_schedule_import_failed');
report($exception);
$this->error('钉钉排班导入失败。');
return self::FAILURE;
}
}
private function dateOption(string $name): ?string
{
$value = trim((string) $this->option($name));
if ($value === '') {
return null;
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/D', $value) !== 1) {
throw new \InvalidArgumentException('排班导入日期格式应为 YYYY-MM-DD。');
}
return Carbon::createFromFormat('!Y-m-d', $value, config('app.timezone', 'Asia/Shanghai'))->toDateString();
}
private function dateValue(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
try {
$timezone = (string) config('app.timezone', 'Asia/Shanghai');
if (is_numeric($value)) {
$timestamp = (int) $value;
return ($timestamp > 99999999999
? Carbon::createFromTimestampMs($timestamp, $timezone)
: Carbon::createFromTimestamp($timestamp, $timezone))->toDateString();
}
return Carbon::parse((string) $value, $timezone)->toDateString();
} catch (Throwable) {
return null;
}
}
private function unsignedInteger(mixed $value): ?int
{
return is_numeric($value) && (int) $value > 0 ? (int) $value : null;
}
private function nullableString(mixed $value, int $length): ?string
{
if (! is_scalar($value)) {
return null;
}
$value = trim((string) $value);
return $value === '' ? null : mb_substr($value, 0, $length);
}
}