connection = $mirror->connection(); } public function ready(): bool { return $this->connection->getSchemaBuilder()->hasTable('business_data_warnings'); } /** * 生成或更新一条预警。同 warning_key 重复触发时只更新风险描述和影响, * 保留首次触发时间、责任人指派和处理留痕。 * * @param array $warning */ public function upsert(array $warning): void { $key = (string) $warning['warning_key']; $existing = $this->connection->table('business_data_warnings')->where('warning_key', $key)->first(); if ($existing !== null) { $this->connection->table('business_data_warnings')->where('warning_key', $key)->update([ 'warning_type' => $warning['warning_type'], 'risk_level' => $warning['risk_level'], 'related_object' => $warning['related_object'] ?? null, 'risk_description' => $warning['risk_description'] ?? null, 'evidence' => $warning['evidence'] ?? null, 'impact' => $warning['impact'] ?? null, 'trigger_rule' => $warning['trigger_rule'] ?? null, 'updated_at' => now(), ]); return; } $this->connection->table('business_data_warnings')->insert([ 'warning_key' => $key, 'warning_number' => $this->nextWarningNumber(), 'warning_type' => $warning['warning_type'], 'risk_level' => $warning['risk_level'], 'related_object' => $warning['related_object'] ?? null, 'risk_description' => $warning['risk_description'] ?? null, 'evidence' => $warning['evidence'] ?? null, 'impact' => $warning['impact'] ?? null, 'trigger_rule' => $warning['trigger_rule'] ?? null, 'triggered_at' => now(), 'responsible_person' => $warning['responsible_person'] ?? null, 'due_at' => $warning['due_at'] ?? null, 'review_status' => 'pending', 'created_at' => now(), 'updated_at' => now(), ]); } /** * 预警页面数据。按触发时间倒序,等待复审的排在前面。 * * @param array $filters * @return array{data: array>, total: int, pending: int} */ public function paginate(int $page, int $perPage, array $filters = []): array { if (! $this->ready()) { return ['data' => [], 'total' => 0, 'pending' => 0]; } $query = $this->connection->table('business_data_warnings'); foreach (['warning_type', 'risk_level', 'review_status'] as $exact) { $value = trim((string) ($filters[$exact] ?? '')); if ($value !== '') { $query->where($exact, '=', $value); } } foreach (['warning_number', 'related_object', 'responsible_person'] as $text) { $value = trim((string) ($filters[$text] ?? '')); if ($value !== '') { $query->where($text, 'like', '%' . addcslashes($value, '%_\\') . '%'); } } $total = (clone $query)->count(); $pending = (clone $query)->where('review_status', '=', 'pending')->count(); $rows = $query ->orderByRaw("case when review_status = 'pending' then 0 else 1 end") ->orderByDesc('triggered_at') ->orderByDesc('id') ->forPage(max($page, 1), $perPage) ->get() ->map(static function (object $row): array { $row = (array) $row; $row['triggered_at'] = $row['triggered_at'] !== null ? Carbon::parse($row['triggered_at'])->format('Y-m-d H:i') : null; $row['reviewed_at'] = $row['reviewed_at'] !== null ? Carbon::parse($row['reviewed_at'])->format('Y-m-d H:i') : null; $row['due_at'] = $row['due_at'] !== null ? Carbon::parse($row['due_at'])->format('Y-m-d') : null; $row['review_status_label'] = match ((string) $row['review_status']) { 'pending' => '待复审', 'confirmed' => '已确认', 'rejected' => '已驳回', default => (string) $row['review_status'], }; return $row; }) ->all(); return ['data' => $rows, 'total' => $total, 'pending' => $pending]; } /** @param array> $warnings */ public function upsertMany(array $warnings): int { $count = 0; foreach ($warnings as $warning) { $this->upsert($warning); $count++; } return $count; } public function find(int $id): ?array { if (! $this->ready()) { return null; } $row = $this->connection->table('business_data_warnings')->where('id', $id)->first(); return $row === null ? null : (array) $row; } /** * 人工复审:确认时保持系统按爬虫生成的结果,驳回时记录依据由后续 * 期间合并任务改回流程数据。 */ public function review(int $id, string $status, string $reviewer, ?string $note, ?string $responsiblePerson = null): bool { if (! $this->ready()) { return false; } $update = [ 'review_status' => $status, 'reviewer' => $reviewer, 'reviewed_at' => now(), 'review_note' => $note, 'updated_at' => now(), ]; if ($responsiblePerson !== null && trim($responsiblePerson) !== '') { $update['responsible_person'] = trim($responsiblePerson); } return $this->connection->table('business_data_warnings')->where('id', $id)->update($update) > 0; } private function nextWarningNumber(): string { $date = now()->format('Ymd'); $prefix = 'WARN-' . $date . '-'; $latest = $this->connection->table('business_data_warnings') ->where('warning_number', 'like', $prefix . '%') ->orderByDesc('warning_number') ->value('warning_number'); $sequence = $latest === null ? 0 : (int) substr((string) $latest, -3); return $prefix . str_pad((string) ($sequence + 1), 3, '0', STR_PAD_LEFT); } }