input('keyword', '')); $status = trim((string)$request->input('status', '')); $serviceProvider = trim((string)$request->input('service_provider', '')); $query = Db::name('reports') ->alias('r') ->leftJoin('orders o', 'o.id = r.order_id') ->leftJoin('order_products p', 'p.order_id = r.order_id') ->field([ 'r.id', 'r.report_no', 'r.order_id', 'r.appraisal_no', 'r.report_type', 'r.report_title', 'r.report_status', 'r.service_provider', 'r.institution_name', 'r.publish_time', 'o.order_no', 'p.product_name', 'p.category_name', 'p.brand_name', ]) ->order('r.id', 'desc'); if ($status !== '') { $query->where('r.report_status', $status); } if ($serviceProvider !== '') { $query->where('r.service_provider', $serviceProvider); } $rows = $query->select()->toArray(); $contentMap = $this->loadReportContentMap(array_map(fn(array $item) => (int)$item['id'], $rows)); $list = []; foreach ($rows as $item) { $productSnapshot = $contentMap[(int)$item['id']]['product_snapshot'] ?? []; $mapped = [ 'id' => (int)$item['id'], 'order_id' => (int)($item['order_id'] ?? 0), 'order_no' => $item['order_no'] ?? '', 'appraisal_no' => $item['appraisal_no'] ?? '', 'report_no' => $item['report_no'], 'report_type' => $item['report_type'] ?: 'appraisal', 'report_type_text' => $this->reportTypeText($item['report_type'] ?: 'appraisal'), 'report_title' => $item['report_title'], 'report_status' => $item['report_status'], 'report_status_text' => $this->reportStatusText($item['report_status']), 'service_provider' => $item['service_provider'], 'service_provider_text' => $this->serviceProviderText($item['service_provider']), 'institution_name' => $item['institution_name'] ?: $this->defaultInstitutionName($item['service_provider']), 'publish_time' => $item['publish_time'], 'product_name' => $item['product_name'] ?: (string)($productSnapshot['product_name'] ?? ''), 'category_name' => $item['category_name'] ?: (string)($productSnapshot['category_name'] ?? ''), 'brand_name' => $item['brand_name'] ?: (string)($productSnapshot['brand_name'] ?? ''), ]; if ($keyword !== '' && !$this->matchKeyword($mapped, $keyword)) { continue; } $list[] = $mapped; } return api_success(['list' => $list]); } public function detail(Request $request) { $id = (int)$request->input('id', 0); if (!$id) { return api_error('报告 ID 不能为空', 422); } $report = Db::name('reports')->where('id', $id)->find(); if (!$report) { return api_error('报告不存在', 404); } $content = Db::name('report_contents')->where('report_id', $id)->find(); $productSnapshot = $this->decodeJsonField($content['product_snapshot_json'] ?? null); $resultSnapshot = $this->decodeJsonField($content['result_snapshot_json'] ?? null); $appraisalSnapshot = $this->decodeJsonField($content['appraisal_snapshot_json'] ?? null); $valuationSnapshot = $this->decodeJsonField($content['valuation_snapshot_json'] ?? null); $appraisalSnapshot = $this->enrichAppraisalSnapshot($report, $appraisalSnapshot); $evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request); $verify = Db::name('report_verifies')->where('report_id', $id)->find() ?: []; if (($report['report_status'] ?? '') === 'published') { $verify = $this->createOrUpdateVerifyRecord($report, date('Y-m-d H:i:s')); } $reportPageUrl = $this->buildPublicPageUrl('/pages/report/detail', ['report_no' => $report['report_no']]); $verifyUrl = $this->buildPublicPageUrl('/pages/verify/result', ['report_no' => $report['report_no']]); if (!$verify) { $verify = []; } $verify['report_page_url'] = $verify['report_page_url'] ?? $reportPageUrl; $verify['verify_qrcode_url'] = $verify['verify_qrcode_url'] ?? $reportPageUrl; $verify['verify_url'] = $verify['verify_url'] ?? $verifyUrl; $defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($report['report_type'] ?? 'appraisal')); return api_success([ 'report_header' => [ 'id' => (int)$report['id'], 'order_id' => (int)($report['order_id'] ?? 0), 'report_no' => $report['report_no'], 'report_type' => $report['report_type'] ?: 'appraisal', 'report_type_text' => $this->reportTypeText($report['report_type'] ?: 'appraisal'), 'report_title' => $report['report_title'], 'report_status' => $report['report_status'], 'report_status_text' => $this->reportStatusText($report['report_status']), 'service_provider' => $report['service_provider'], 'service_provider_text' => $this->serviceProviderText($report['service_provider']), 'institution_name' => $report['institution_name'] ?: $this->defaultInstitutionName($report['service_provider']), 'publish_time' => $report['publish_time'], ], 'product_info' => $productSnapshot, 'result_info' => $resultSnapshot, 'appraisal_info' => $appraisalSnapshot, 'valuation_info' => $valuationSnapshot, 'evidence_attachments' => $evidenceAttachments, 'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : $defaultRiskNotice, 'verify_info' => [ 'verify_status' => $verify['verify_status'] ?? (($report['report_status'] ?? '') === 'published' ? 'valid' : 'pending'), 'verify_url' => $verify['verify_url'] ?? $verifyUrl, 'verify_qrcode_url' => $verify['verify_qrcode_url'] ?? $reportPageUrl, 'report_page_url' => $verify['report_page_url'] ?? $reportPageUrl, 'verify_count' => (int)($verify['verify_count'] ?? 0), ], ]); } public function saveInspection(Request $request) { $id = (int)$request->input('id', 0); $header = $request->input('report_header', []); $productInfo = $request->input('product_info', []); $resultInfo = $request->input('result_info', []); $appraisalInfo = $request->input('appraisal_info', []); $valuationInfo = $request->input('valuation_info', []); $riskNoticeText = trim((string)$request->input('risk_notice_text', '')); if (!is_array($header) || !is_array($productInfo) || !is_array($resultInfo) || !is_array($appraisalInfo) || !is_array($valuationInfo)) { return api_error('检查单参数格式错误', 422); } $serviceProvider = trim((string)($header['service_provider'] ?? 'anxinyan')); if (!in_array($serviceProvider, ['anxinyan', 'zhongjian'], true)) { return api_error('服务类型不正确', 422); } $reportStatus = trim((string)($header['report_status'] ?? 'pending_publish')); if (!in_array($reportStatus, ['draft', 'pending_publish', 'published'], true)) { return api_error('报告状态不正确', 422); } $productName = trim((string)($productInfo['product_name'] ?? '')); $resultText = trim((string)($resultInfo['result_text'] ?? '')); if ($productName === '') { return api_error('商品名称不能为空', 422); } if ($resultText === '') { return api_error('鉴定结论不能为空', 422); } $now = date('Y-m-d H:i:s'); Db::startTrans(); try { $existing = null; if ($id > 0) { $existing = Db::name('reports')->where('id', $id)->find(); if (!$existing || (($existing['report_type'] ?? 'appraisal') !== 'inspection')) { Db::rollback(); return api_error('检查单不存在', 404); } if (($existing['report_status'] ?? '') === 'published') { Db::rollback(); return api_error('已发布的检查单不支持直接编辑,请复制后重新补录', 422); } } $reportNo = trim((string)($header['report_no'] ?? ($existing['report_no'] ?? ''))); if ($reportNo === '') { $reportNo = $this->generateUniqueReportNo('inspection'); } $conflict = Db::name('reports') ->where('report_no', $reportNo) ->when($id > 0, fn($query) => $query->where('id', '<>', $id)) ->find(); if ($conflict) { Db::rollback(); return api_error('检查单编号已存在,请更换后重试', 422); } $reportTitle = trim((string)($header['report_title'] ?? '')); if ($reportTitle === '') { $reportTitle = $this->defaultReportTitle($serviceProvider, 'inspection'); } $institutionName = trim((string)($header['institution_name'] ?? '')); if ($institutionName === '') { $institutionName = $this->defaultInstitutionName($serviceProvider); } $publishTime = $reportStatus === 'published' ? trim((string)($header['publish_time'] ?? ($existing['publish_time'] ?? $now))) : null; $reportPayload = [ 'report_no' => $reportNo, 'order_id' => 0, 'appraisal_no' => $existing['appraisal_no'] ?? $this->generateUniqueAppraisalNo('inspection'), 'report_type' => 'inspection', 'service_provider' => $serviceProvider, 'institution_name' => $institutionName, 'report_title' => $reportTitle, 'report_status' => $reportStatus, 'report_version' => $existing ? ((int)$existing['report_version'] + 1) : 1, 'publish_time' => $publishTime ?: null, 'invalid_reason' => '', 'updated_at' => $now, ]; if ($existing) { Db::name('reports')->where('id', $id)->update($reportPayload); $reportId = $id; } else { $reportPayload['created_at'] = $now; $reportId = (int)Db::name('reports')->insertGetId($reportPayload); } $normalizedProductInfo = [ 'product_name' => $productName, 'category_name' => trim((string)($productInfo['category_name'] ?? '')), 'brand_name' => trim((string)($productInfo['brand_name'] ?? '')), 'color' => trim((string)($productInfo['color'] ?? '')), 'size_spec' => trim((string)($productInfo['size_spec'] ?? '')), 'serial_no' => trim((string)($productInfo['serial_no'] ?? '')), ]; $normalizedResultInfo = [ 'result_status' => trim((string)($resultInfo['result_status'] ?? 'authentic')), 'result_text' => $resultText, 'result_desc' => trim((string)($resultInfo['result_desc'] ?? '')), ]; $normalizedAppraisalInfo = [ 'service_provider' => $serviceProvider, 'institution_name' => $institutionName, 'appraiser_name' => trim((string)($appraisalInfo['appraiser_name'] ?? '')), 'reviewer_name' => trim((string)($appraisalInfo['reviewer_name'] ?? '')), 'appraisal_time' => trim((string)($appraisalInfo['appraisal_time'] ?? ($publishTime ?: $now))), ]; $normalizedValuationInfo = [ 'condition_grade' => trim((string)($valuationInfo['condition_grade'] ?? '')), 'condition_desc' => trim((string)($valuationInfo['condition_desc'] ?? '')), 'valuation_min' => (float)($valuationInfo['valuation_min'] ?? 0), 'valuation_max' => (float)($valuationInfo['valuation_max'] ?? 0), 'valuation_desc' => trim((string)($valuationInfo['valuation_desc'] ?? '')), ]; $contentPayload = [ 'report_id' => $reportId, 'product_snapshot_json' => json_encode($normalizedProductInfo, JSON_UNESCAPED_UNICODE), 'result_snapshot_json' => json_encode($normalizedResultInfo, JSON_UNESCAPED_UNICODE), 'appraisal_snapshot_json' => json_encode($normalizedAppraisalInfo, JSON_UNESCAPED_UNICODE), 'valuation_snapshot_json' => json_encode($normalizedValuationInfo, JSON_UNESCAPED_UNICODE), 'risk_notice_text' => $riskNoticeText !== '' ? $riskNoticeText : (new ContentService())->getReportRiskNotice('inspection'), 'updated_at' => $now, ]; $content = Db::name('report_contents')->where('report_id', $reportId)->find(); if ($content) { Db::name('report_contents')->where('report_id', $reportId)->update($contentPayload); } else { $contentPayload['created_at'] = $now; Db::name('report_contents')->insert($contentPayload); } $reportRecord = Db::name('reports')->where('id', $reportId)->find(); $verifyInfo = [ 'verify_url' => '', 'report_page_url' => '', ]; if ($reportStatus === 'published' && $reportRecord) { $verifyInfo = $this->createOrUpdateVerifyRecord($reportRecord, $now); } else { Db::name('report_verifies')->where('report_id', $reportId)->delete(); } Db::commit(); return api_success([ 'id' => $reportId, 'report_status' => $reportStatus, 'publish_time' => $publishTime ?: '', 'verify_url' => $verifyInfo['verify_url'] ?? '', 'report_page_url' => $verifyInfo['report_page_url'] ?? '', ], $existing ? '检查单已更新' : '检查单已补录'); } catch (\Throwable $e) { Db::rollback(); return api_error('检查单保存失败', 500, [ 'detail' => $e->getMessage(), ]); } } public function publish(Request $request) { $id = (int)$request->input('id', 0); if (!$id) { return api_error('报告 ID 不能为空', 422); } $now = date('Y-m-d H:i:s'); Db::startTrans(); try { $report = Db::name('reports')->where('id', $id)->find(); if (!$report) { Db::rollback(); return api_error('报告不存在', 404); } if (!in_array($report['report_status'], ['draft', 'pending_publish', 'updated', 'published'], true)) { Db::rollback(); return api_error('当前报告状态不支持发布', 422); } $effectivePublishTime = $report['publish_time'] ?: $now; if ($report['report_status'] !== 'published') { Db::name('reports')->where('id', $id)->update([ 'report_status' => 'published', 'publish_time' => $effectivePublishTime, 'updated_at' => $now, ]); $report['report_status'] = 'published'; $report['publish_time'] = $effectivePublishTime; } if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) { $this->refreshAppraisalSnapshot((int)$report['id'], (int)$report['order_id'], $report['service_provider'], $now); } $verify = $this->createOrUpdateVerifyRecord($report, $now); if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) { Db::name('orders')->where('id', $report['order_id'])->update([ 'order_status' => 'report_published', 'display_status' => '报告已出具', 'updated_at' => $now, ]); $order = Db::name('orders')->where('id', $report['order_id'])->find(); $product = Db::name('order_products')->where('order_id', $report['order_id'])->find(); $timelineExists = Db::name('order_timelines') ->where('order_id', $report['order_id']) ->where('node_code', 'report_published') ->where('node_text', '报告已出具') ->find(); if (!$timelineExists) { Db::name('order_timelines')->insert([ 'order_id' => $report['order_id'], 'node_code' => 'report_published', 'node_text' => '报告已出具', 'node_desc' => '正式报告已发布,用户可查看报告并进行验真。', 'operator_type' => 'admin', 'operator_id' => 1, 'occurred_at' => $now, 'created_at' => $now, ]); } (new MessageDispatcher())->sendInboxEvent('report_published', [ 'user_id' => (int)($order['user_id'] ?? 0), 'biz_type' => 'report', 'biz_id' => (int)$report['id'], 'report_no' => $report['report_no'], 'report_title' => $report['report_title'], 'product_name' => $product['product_name'] ?? '', 'publish_time' => $report['publish_time'] ?: $now, 'verify_url' => $verify['verify_url'], 'fallback_title' => '报告已出具', 'fallback_content' => '您的正式报告已生成,可前往报告中心查看并完成验真。', ]); } Db::commit(); if (($report['report_type'] ?? 'appraisal') === 'appraisal' && (int)($report['order_id'] ?? 0) > 0) { (new EnterpriseWebhookService())->recordOrderEvent((int)$report['order_id'], 'report_published', [ 'report_id' => $id, 'report_no' => (string)$report['report_no'], 'report_title' => (string)$report['report_title'], 'publish_time' => $effectivePublishTime, 'verify_url' => (string)($verify['verify_url'] ?? ''), 'report_page_url' => (string)($verify['report_page_url'] ?? ''), ]); } return api_success([ 'id' => $id, 'report_status' => 'published', 'publish_time' => $effectivePublishTime, 'verify_url' => $verify['verify_url'], 'report_page_url' => $verify['report_page_url'], ], '报告已发布'); } catch (\Throwable $e) { Db::rollback(); return api_error('报告发布失败', 500, [ 'detail' => $e->getMessage(), ]); } } private function reportStatusText(string $status): string { return match ($status) { 'draft' => '草稿中', 'pending_publish' => '待发布', 'published' => '已发布', 'updated' => '已更新', 'invalid' => '已作废', default => $status, }; } private function reportTypeText(string $reportType): string { return match ($reportType) { 'inspection' => '补录检查单', default => '订单报告', }; } private function serviceProviderText(string $serviceProvider): string { return $serviceProvider === 'zhongjian' ? '中检鉴定' : '实物鉴定'; } private function decodeJsonField(mixed $value): array { if (is_array($value)) { return $value; } if (is_string($value) && $value !== '') { $decoded = json_decode($value, true); return is_array($decoded) ? $decoded : []; } return []; } private function loadReportContentMap(array $reportIds): array { if (!$reportIds) { return []; } $rows = Db::name('report_contents')->whereIn('report_id', $reportIds)->select()->toArray(); $map = []; foreach ($rows as $row) { $map[(int)$row['report_id']] = [ 'product_snapshot' => $this->decodeJsonField($row['product_snapshot_json'] ?? null), 'result_snapshot' => $this->decodeJsonField($row['result_snapshot_json'] ?? null), ]; } return $map; } private function matchKeyword(array $item, string $keyword): bool { $needle = mb_strtolower($keyword); foreach (['report_no', 'report_title', 'product_name', 'brand_name', 'institution_name', 'order_no', 'appraisal_no'] as $field) { if (str_contains(mb_strtolower((string)($item[$field] ?? '')), $needle)) { return true; } } return false; } private function createOrUpdateVerifyRecord(array $report, string $now): array { $reportNo = (string)$report['report_no']; $verifyToken = 'verify_' . strtolower((string)preg_replace('/[^a-zA-Z0-9]/', '', $reportNo)); $verifyUrl = $this->buildPublicPageUrl('/pages/verify/result', ['report_no' => $reportNo]); $reportPageUrl = $this->buildPublicPageUrl('/pages/report/detail', ['report_no' => $reportNo]); $payload = [ 'report_id' => (int)$report['id'], 'report_no' => $reportNo, 'verify_token' => $verifyToken, 'verify_qrcode_url' => $reportPageUrl, 'verify_url' => $verifyUrl, 'verify_status' => 'valid', 'updated_at' => $now, ]; $verify = Db::name('report_verifies')->where('report_id', $report['id'])->find(); if ($verify) { Db::name('report_verifies')->where('id', $verify['id'])->update($payload); } else { $payload['last_verified_at'] = null; $payload['verify_count'] = 0; $payload['created_at'] = $now; Db::name('report_verifies')->insert($payload); } $fresh = Db::name('report_verifies')->where('report_id', $report['id'])->find() ?: $payload; $fresh['report_page_url'] = $reportPageUrl; return $fresh; } private function buildPublicPageUrl(string $pagePath, array $query = []): string { $baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url')); $page = ltrim($pagePath, '/'); $queryString = http_build_query($query); $hashPath = '/#/' . $page; if ($queryString !== '') { $hashPath .= '?' . $queryString; } if ($baseUrl === '') { return $hashPath; } return $baseUrl . $hashPath; } private function normalizeH5BaseUrl(string $value): string { $baseUrl = trim($value); if ($baseUrl === '') { return ''; } $hashPos = strpos($baseUrl, '#'); if ($hashPos !== false) { $baseUrl = substr($baseUrl, 0, $hashPos); } if (!preg_match('/^https?:\/\//i', $baseUrl)) { $baseUrl = 'https://' . ltrim($baseUrl, '/'); } return rtrim($baseUrl, '/'); } private function enrichAppraisalSnapshot(array $report, array $snapshot): array { if (($report['report_type'] ?? 'appraisal') !== 'appraisal' || (int)($report['order_id'] ?? 0) <= 0) { return $snapshot; } $tasks = Db::name('appraisal_tasks') ->where('order_id', (int)$report['order_id']) ->order('id', 'asc') ->select() ->toArray(); $firstReviewTask = null; $finalReviewTask = null; foreach ($tasks as $task) { if (($task['task_stage'] ?? '') === 'first_review') { $firstReviewTask = $task; } if (($task['task_stage'] ?? '') === 'final_review') { $finalReviewTask = $task; } } $institutionName = $snapshot['institution_name'] ?? ($report['institution_name'] ?: $this->defaultInstitutionName($report['service_provider'])); $appraiserName = $this->normalizeAssigneeName($snapshot['appraiser_name'] ?? '') ?: $this->normalizeAssigneeName($firstReviewTask['assignee_name'] ?? '') ?: $this->normalizeAssigneeName($finalReviewTask['assignee_name'] ?? ''); $reviewerName = $appraiserName; $appraisalTime = $snapshot['appraisal_time'] ?? ($firstReviewTask['submitted_at'] ?? $firstReviewTask['started_at'] ?? $finalReviewTask['submitted_at'] ?? $finalReviewTask['started_at'] ?? ''); $snapshot['service_provider'] = $snapshot['service_provider'] ?? $report['service_provider']; $snapshot['institution_name'] = $institutionName; $snapshot['appraiser_name'] = $appraiserName; $snapshot['reviewer_name'] = $reviewerName; $snapshot['appraisal_time'] = $appraisalTime; return $snapshot; } private function refreshAppraisalSnapshot(int $reportId, int $orderId, string $serviceProvider, string $now): void { $content = Db::name('report_contents')->where('report_id', $reportId)->find(); if (!$content) { return; } $snapshot = $this->enrichAppraisalSnapshot( [ 'report_type' => 'appraisal', 'order_id' => $orderId, 'service_provider' => $serviceProvider, 'institution_name' => '', ], $this->decodeJsonField($content['appraisal_snapshot_json'] ?? null), ); Db::name('report_contents')->where('report_id', $reportId)->update([ 'appraisal_snapshot_json' => json_encode($snapshot, JSON_UNESCAPED_UNICODE), 'updated_at' => $now, ]); } private function normalizeAssigneeName(?string $value): string { $name = trim((string)$value); if ($name === '' || $name === '未分配') { return ''; } return $name; } private function evidenceService(): AppraisalEvidenceService { return new AppraisalEvidenceService(); } private function getSystemConfigValue(string $groupCode, string $configKey): string { $row = Db::name('system_configs') ->where('config_group', $groupCode) ->where('config_key', $configKey) ->find(); return trim((string)($row['config_value'] ?? '')); } private function generateUniqueReportNo(string $reportType): string { $prefix = $reportType === 'inspection' ? 'AXY-CHK' : 'AXY-R'; for ($i = 0; $i < 20; $i++) { $candidate = sprintf('%s-%s-%04d', $prefix, date('Ymd'), random_int(0, 9999)); if (!Db::name('reports')->where('report_no', $candidate)->find()) { return $candidate; } } return sprintf('%s-%s-%s', $prefix, date('YmdHis'), random_int(1000, 9999)); } private function generateUniqueAppraisalNo(string $reportType): string { $prefix = $reportType === 'inspection' ? 'AXY-CHECK' : 'AXY-APP'; for ($i = 0; $i < 20; $i++) { $candidate = sprintf('%s-%s-%04d', $prefix, date('Ymd'), random_int(0, 9999)); if (!Db::name('reports')->where('appraisal_no', $candidate)->find()) { return $candidate; } } return sprintf('%s-%s-%s', $prefix, date('YmdHis'), random_int(1000, 9999)); } private function defaultReportTitle(string $serviceProvider, string $reportType): string { if ($reportType === 'inspection') { return $serviceProvider === 'zhongjian' ? '中检检查单' : '安心验检查单'; } return $serviceProvider === 'zhongjian' ? '中检鉴定报告' : '安心验鉴定报告'; } private function defaultInstitutionName(string $serviceProvider): string { return $serviceProvider === 'zhongjian' ? '中检合作机构' : '安心验'; } }