178 lines
6.5 KiB
PHP
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);
|
|
}
|
|
}
|