feat: update report detail and verification flow
This commit is contained in:
@@ -461,12 +461,21 @@ class AppraisalTasksController
|
||||
$reportNo = trim((string)$request->input('zhongjian_report_no', ''));
|
||||
$qrInput = trim((string)$request->input('qr_input', ''));
|
||||
$files = $this->evidenceService()->normalize($request->input('report_files', []), $request, true);
|
||||
$productInput = $request->input('product_info', null);
|
||||
$productPayload = is_array($productInput) ? $this->normalizeProductInput($productInput) : null;
|
||||
$attachments = $this->evidenceService()->normalize($request->input('attachments', []), $request, true);
|
||||
$keyPoints = $this->normalizeKeyPointInput($request->input('key_points', []));
|
||||
$resultText = trim((string)$request->input('result_text', ''));
|
||||
$resultDesc = trim((string)$request->input('result_desc', ''));
|
||||
if ($id <= 0) {
|
||||
return api_error('任务 ID 不能为空', 422);
|
||||
}
|
||||
if ($reportNo === '') {
|
||||
return api_error('中检报告编号不能为空', 422);
|
||||
}
|
||||
if ($resultText === '') {
|
||||
return api_error('鉴定结论不能为空', 422);
|
||||
}
|
||||
if (!$files) {
|
||||
return api_error('请至少上传 1 个中检报告文件', 422);
|
||||
}
|
||||
@@ -514,6 +523,15 @@ class AppraisalTasksController
|
||||
$task = array_merge($task, $operatorGuard['task_update']);
|
||||
}
|
||||
|
||||
if ($productPayload !== null) {
|
||||
$this->saveOrderProductSnapshot((int)$task['order_id'], $productPayload, $now);
|
||||
}
|
||||
|
||||
if (!$this->hasSubmittableProductInfo((int)$task['order_id'], $productPayload)) {
|
||||
Db::rollback();
|
||||
return api_error('提交中检报告前请先完善物品信息', 422);
|
||||
}
|
||||
|
||||
Db::name('appraisal_tasks')->where('id', $id)->update([
|
||||
'status' => 'completed',
|
||||
'started_at' => $task['started_at'] ?: $now,
|
||||
@@ -531,14 +549,14 @@ class AppraisalTasksController
|
||||
'task_id' => $id,
|
||||
'order_id' => (int)$task['order_id'],
|
||||
'result_status' => 'zhongjian_report',
|
||||
'result_text' => '以中检报告为准',
|
||||
'result_desc' => '中检报告已回传并由平台录入。',
|
||||
'result_text' => $resultText,
|
||||
'result_desc' => $resultDesc,
|
||||
'condition_grade' => '',
|
||||
'condition_desc' => '',
|
||||
'valuation_min' => 0,
|
||||
'valuation_max' => 0,
|
||||
'valuation_desc' => '',
|
||||
'attachments_json' => json_encode($files, JSON_UNESCAPED_UNICODE),
|
||||
'attachments_json' => $attachments ? json_encode($attachments, JSON_UNESCAPED_UNICODE) : null,
|
||||
'external_remark' => '',
|
||||
'internal_remark' => '中检报告编号:' . $reportNo,
|
||||
'updated_at' => $now,
|
||||
@@ -546,10 +564,12 @@ class AppraisalTasksController
|
||||
$resultId = Db::name('appraisal_task_results')->where('task_id', $id)->value('id');
|
||||
if ($resultId) {
|
||||
Db::name('appraisal_task_results')->where('id', (int)$resultId)->update($resultPayload);
|
||||
$savedResultId = (int)$resultId;
|
||||
} else {
|
||||
$resultPayload['created_at'] = $now;
|
||||
Db::name('appraisal_task_results')->insert($resultPayload);
|
||||
$savedResultId = (int)Db::name('appraisal_task_results')->insertGetId($resultPayload);
|
||||
}
|
||||
$this->saveTaskKeyPoints($savedResultId, $keyPoints, $now);
|
||||
|
||||
$this->createOrUpdateReportDraft((int)$task['order_id'], $task, $resultPayload, $now);
|
||||
$report = $this->findLatestAppraisalReport((int)$task['order_id']);
|
||||
@@ -578,7 +598,7 @@ class AppraisalTasksController
|
||||
'order_id' => (int)$task['order_id'],
|
||||
'node_code' => 'zhongjian_report_entered',
|
||||
'node_text' => '中检报告已录入',
|
||||
'node_desc' => '报告录入人已录入中检报告编号并上传报告文件。',
|
||||
'node_desc' => '报告录入人已补全报告信息、录入中检报告编号并上传报告文件。',
|
||||
'operator_type' => 'admin',
|
||||
'operator_id' => $operatorId,
|
||||
'occurred_at' => $now,
|
||||
@@ -1814,7 +1834,7 @@ class AppraisalTasksController
|
||||
'appraisal_no' => $order['appraisal_no'] ?? '',
|
||||
'report_type' => 'appraisal',
|
||||
'service_provider' => $task['service_provider'],
|
||||
'institution_name' => $task['service_provider'] === 'zhongjian' ? '中检合作机构' : '安心验',
|
||||
'institution_name' => $this->displayInstitutionName((string)$task['service_provider']),
|
||||
'report_title' => $task['service_provider'] === 'zhongjian' ? '中检鉴定报告' : '安心验鉴定报告',
|
||||
'report_status' => 'pending_publish',
|
||||
'publish_time' => null,
|
||||
@@ -1870,7 +1890,7 @@ class AppraisalTasksController
|
||||
|
||||
private function buildAppraisalSnapshot(string $serviceProvider, string $fallbackTime, ?array $firstReviewTask, ?array $finalReviewTask): array
|
||||
{
|
||||
$institutionName = $serviceProvider === 'zhongjian' ? '中检合作机构' : '安心验';
|
||||
$institutionName = $this->displayInstitutionName($serviceProvider);
|
||||
$appraiserName = $this->normalizeAssigneeName($firstReviewTask['assignee_name'] ?? '')
|
||||
?: $this->normalizeAssigneeName($finalReviewTask['assignee_name'] ?? '');
|
||||
$reviewerName = $appraiserName;
|
||||
@@ -1899,6 +1919,11 @@ class AppraisalTasksController
|
||||
return $name;
|
||||
}
|
||||
|
||||
private function displayInstitutionName(string $serviceProvider): string
|
||||
{
|
||||
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
||||
}
|
||||
|
||||
private function guardTaskOperator(Request $request, array $task): array
|
||||
{
|
||||
$adminId = (int)$request->header('x-admin-id', 0);
|
||||
|
||||
@@ -78,7 +78,7 @@ class ReportsController
|
||||
'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']),
|
||||
'institution_name' => $this->defaultInstitutionName((string)$item['service_provider']),
|
||||
'publish_time' => $item['publish_time'],
|
||||
'zhongjian_report_no' => (string)($item['zhongjian_report_no'] ?? ''),
|
||||
'report_entry_admin_name' => (string)($item['report_entry_admin_name'] ?? ''),
|
||||
@@ -163,7 +163,7 @@ class ReportsController
|
||||
'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']),
|
||||
'institution_name' => $this->defaultInstitutionName((string)$report['service_provider']),
|
||||
'publish_time' => $report['publish_time'],
|
||||
'zhongjian_report_no' => (string)($report['zhongjian_report_no'] ?? ''),
|
||||
'report_entry_admin_id' => (int)($report['report_entry_admin_id'] ?? 0),
|
||||
@@ -765,6 +765,6 @@ class ReportsController
|
||||
|
||||
private function defaultInstitutionName(string $serviceProvider): string
|
||||
{
|
||||
return $serviceProvider === 'zhongjian' ? '中检合作机构' : '安心验';
|
||||
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ReportsController
|
||||
'service_provider' => $item['service_provider'],
|
||||
'status' => $published ? '已出报告' : '待出报告',
|
||||
'result_text' => $published ? '正品' : '待出报告',
|
||||
'institution_name' => $item['institution_name'] ?: '安心验',
|
||||
'institution_name' => $this->displayInstitutionName((string)$item['service_provider']),
|
||||
'publish_time' => $item['publish_time'],
|
||||
];
|
||||
}, $rows);
|
||||
@@ -94,7 +94,6 @@ class ReportsController
|
||||
$content = Db::name('report_contents')->where('report_id', $reportData['id'])->find();
|
||||
$verify = Db::name('report_verifies')->where('report_id', $reportData['id'])->find() ?: [];
|
||||
$verify = $this->normalizeVerifyInfo($reportData, $verify);
|
||||
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: []);
|
||||
$evidenceAttachments = $this->evidenceService()->normalize($content['evidence_attachments_json'] ?? null, $request);
|
||||
$zhongjianReportFiles = $this->evidenceService()->normalize($content['zhongjian_report_files_json'] ?? null, $request);
|
||||
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($reportData['report_type'] ?? 'appraisal'));
|
||||
@@ -105,6 +104,17 @@ class ReportsController
|
||||
'valuation_snapshot' => $this->decodeJsonField($content['valuation_snapshot_json'] ?? null),
|
||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : $defaultRiskNotice,
|
||||
];
|
||||
$productDisplay = $this->buildProductDisplay($reportData, $payload['product_snapshot'], $payload['result_snapshot']);
|
||||
$reportMedia = [
|
||||
'images' => $this->filterAssetsByType($evidenceAttachments, 'image'),
|
||||
];
|
||||
$traceInfo = $this->buildTraceInfo(
|
||||
(int)($reportData['order_id'] ?? 0),
|
||||
$payload['appraisal_snapshot'],
|
||||
$evidenceAttachments,
|
||||
$request
|
||||
);
|
||||
$pdfUrl = $this->ensurePdfFile($request, $reportData, $content ?: [], $verify ?: [], $productDisplay, $reportMedia);
|
||||
|
||||
return api_success([
|
||||
'report_header' => [
|
||||
@@ -114,7 +124,7 @@ class ReportsController
|
||||
'report_title' => $reportData['report_title'],
|
||||
'report_status' => $reportData['report_status'],
|
||||
'service_provider' => $reportData['service_provider'],
|
||||
'institution_name' => $reportData['institution_name'],
|
||||
'institution_name' => $this->displayInstitutionName((string)$reportData['service_provider']),
|
||||
'publish_time' => $reportData['publish_time'],
|
||||
'zhongjian_report_no' => (string)($reportData['zhongjian_report_no'] ?? ''),
|
||||
'report_entry_admin_name' => (string)($reportData['report_entry_admin_name'] ?? ''),
|
||||
@@ -126,6 +136,9 @@ class ReportsController
|
||||
'valuation_info' => $payload['valuation_snapshot'],
|
||||
'evidence_attachments' => $evidenceAttachments,
|
||||
'zhongjian_report_files' => $zhongjianReportFiles,
|
||||
'report_media' => $reportMedia,
|
||||
'product_display' => $productDisplay,
|
||||
'trace_info' => $traceInfo,
|
||||
'risk_notice_text' => $payload['risk_notice_text'],
|
||||
'verify_info' => [
|
||||
'report_no' => $reportData['report_no'],
|
||||
@@ -139,6 +152,75 @@ class ReportsController
|
||||
]);
|
||||
}
|
||||
|
||||
public function antiCounterfeitVerify(Request $request)
|
||||
{
|
||||
$reportNo = trim((string)$request->input('report_no', ''));
|
||||
$verifyCode = trim((string)$request->input('verify_code', ''));
|
||||
if ($reportNo === '' || $verifyCode === '') {
|
||||
return api_error('报告编号和防伪查询码不能为空', 422);
|
||||
}
|
||||
if (!preg_match('/^\d{6}$/', $verifyCode)) {
|
||||
return api_error('防伪查询码应为 6 位数字', 422);
|
||||
}
|
||||
|
||||
$report = Db::name('reports')
|
||||
->where('report_no', $reportNo)
|
||||
->where('report_status', 'published')
|
||||
->find();
|
||||
if (!$report) {
|
||||
return api_success([
|
||||
'verify_passed' => false,
|
||||
'verify_message' => '未查询到有效报告,请核对报告编号后重试。',
|
||||
'verify_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$tag = Db::name('material_tag_codes')
|
||||
->where('report_id', (int)$report['id'])
|
||||
->where('bind_status', 'bound')
|
||||
->find();
|
||||
if (!$tag) {
|
||||
return api_success([
|
||||
'verify_passed' => false,
|
||||
'verify_message' => '当前报告尚未绑定防伪吊牌,暂不能完成防伪查询。',
|
||||
'verify_count' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
$batch = Db::name('material_batches')->where('id', (int)$tag['batch_id'])->find();
|
||||
$active = ($tag['status'] ?? 'active') !== 'invalid'
|
||||
&& (!$batch || ($batch['status'] ?? 'active') !== 'invalid');
|
||||
$passed = $active
|
||||
&& hash_equals((string)$tag['verify_code'], $verifyCode)
|
||||
&& (string)($tag['report_no'] ?: $report['report_no']) === $reportNo;
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
if ($passed) {
|
||||
Db::name('material_tag_codes')->where('id', (int)$tag['id'])->update([
|
||||
'verify_count' => (int)$tag['verify_count'] + 1,
|
||||
'last_verified_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
}
|
||||
$this->insertMaterialTagVerifyLog($tag, $reportNo, $verifyCode, $passed, $request, $now);
|
||||
|
||||
if (!$active) {
|
||||
return api_success([
|
||||
'verify_passed' => false,
|
||||
'verify_message' => '该防伪吊牌已失效,不能进行防伪查询。',
|
||||
'verify_count' => (int)$tag['verify_count'],
|
||||
]);
|
||||
}
|
||||
|
||||
return api_success([
|
||||
'verify_passed' => $passed,
|
||||
'verify_message' => $passed
|
||||
? '防伪查询通过,该报告编号与吊牌防伪查询码匹配。'
|
||||
: '防伪查询码与当前报告不匹配,请核对吊牌后重试。',
|
||||
'verify_count' => (int)$tag['verify_count'] + ($passed ? 1 : 0),
|
||||
]);
|
||||
}
|
||||
|
||||
private function decodeJsonField(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
@@ -171,49 +253,41 @@ class ReportsController
|
||||
return $verify;
|
||||
}
|
||||
|
||||
private function ensurePdfFile(Request $request, array $report, array $content, array $verify): string
|
||||
private function ensurePdfFile(Request $request, array $report, array $content, array $verify, array $productDisplay, array $reportMedia): string
|
||||
{
|
||||
$existingFile = Db::name('report_files')
|
||||
->where('report_id', (int)$report['id'])
|
||||
->where('file_type', 'pdf')
|
||||
->find();
|
||||
$publishTime = (string)($report['publish_time'] ?: date('Y-m-d H:i:s'));
|
||||
$relativeDir = 'uploads/reports/' . date('Ymd', strtotime($publishTime));
|
||||
$filename = $report['report_no'] . '-v2.pdf';
|
||||
$relativePath = $relativeDir . '/' . $filename;
|
||||
|
||||
if ($existingFile && !empty($existingFile['file_url'])) {
|
||||
$relativeUrl = ltrim((string)$existingFile['file_url'], '/');
|
||||
if ($this->storage()->exists($relativeUrl)) {
|
||||
if ($relativeUrl === $relativePath && !$this->shouldRebuildPdf($existingFile, $report, $content) && $this->storage()->exists($relativeUrl)) {
|
||||
return $this->storage()->publicUrl($request, $relativeUrl);
|
||||
}
|
||||
}
|
||||
|
||||
$productInfo = $this->decodeJsonField($content['product_snapshot_json'] ?? null);
|
||||
$resultInfo = $this->decodeJsonField($content['result_snapshot_json'] ?? null);
|
||||
$appraisalInfo = $this->decodeJsonField($content['appraisal_snapshot_json'] ?? null);
|
||||
$valuationInfo = $this->decodeJsonField($content['valuation_snapshot_json'] ?? null);
|
||||
|
||||
$publishTime = (string)($report['publish_time'] ?: date('Y-m-d H:i:s'));
|
||||
$defaultRiskNotice = (new ContentService())->getReportRiskNotice((string)($report['report_type'] ?? 'appraisal'));
|
||||
$relativeDir = 'uploads/reports/' . date('Ymd', strtotime($publishTime));
|
||||
$filename = $report['report_no'] . '.pdf';
|
||||
$relativePath = $relativeDir . '/' . $filename;
|
||||
$generator = new ReportPdfGenerator();
|
||||
$pdfBinary = $generator->generate([
|
||||
'report_title' => $report['report_title'] ?? '鉴定报告',
|
||||
'service_provider_text' => ($report['service_provider'] ?? 'anxinyan') === 'zhongjian' ? '中检鉴定' : '实物鉴定',
|
||||
'institution_name' => $report['institution_name'] ?? '安心验',
|
||||
'institution_name' => $this->displayInstitutionName((string)($report['service_provider'] ?? 'anxinyan')),
|
||||
'report_no' => $report['report_no'] ?? '',
|
||||
'publish_time' => $publishTime,
|
||||
'result_text' => $resultInfo['result_text'] ?? '-',
|
||||
'result_desc' => $resultInfo['result_desc'] ?? '-',
|
||||
'product_name' => $productInfo['product_name'] ?? '-',
|
||||
'category_brand' => trim(($productInfo['category_name'] ?? '-') . ' / ' . ($productInfo['brand_name'] ?? '-')),
|
||||
'spec_info' => trim(($productInfo['color'] ?? '-') . ' / ' . ($productInfo['size_spec'] ?? '-')),
|
||||
'appraisers' => trim((string)($appraisalInfo['appraiser_name'] ?? '-')),
|
||||
'condition_grade' => $valuationInfo['condition_grade'] ?? '-',
|
||||
'valuation_range' => sprintf(
|
||||
'¥%s - ¥%s',
|
||||
$valuationInfo['valuation_min'] ?? 0,
|
||||
$valuationInfo['valuation_max'] ?? 0
|
||||
),
|
||||
'product_items' => $productDisplay['items'] ?? [],
|
||||
'hero_image' => $this->firstPdfHeroImage($reportMedia['images'] ?? []),
|
||||
'hero_image_labels' => $this->assetNameList($reportMedia['images'] ?? []),
|
||||
'verify_info' => sprintf(
|
||||
'%s / %s',
|
||||
$verify['report_no'] ?? ($report['report_no'] ?? '-'),
|
||||
@@ -222,7 +296,7 @@ class ReportsController
|
||||
'risk_notice_text' => ($content['risk_notice_text'] ?? '') !== '' ? $content['risk_notice_text'] : ($defaultRiskNotice !== '' ? $defaultRiskNotice : '-'),
|
||||
]);
|
||||
|
||||
$this->storage()->putContents($relativePath, $pdfBinary);
|
||||
$this->storage()->putContentsWithMimeType($relativePath, $pdfBinary, 'application/pdf');
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
$filePayload = [
|
||||
@@ -243,6 +317,268 @@ class ReportsController
|
||||
return $this->storage()->publicUrl($request, $relativePath);
|
||||
}
|
||||
|
||||
private function buildProductDisplay(array $report, array $productInfo, array $resultInfo): array
|
||||
{
|
||||
$items = [
|
||||
[
|
||||
'label' => '检测结论',
|
||||
'value' => $this->textValue($resultInfo['result_text'] ?? '') ?: '-',
|
||||
'remark' => $this->textValue($resultInfo['result_desc'] ?? ''),
|
||||
],
|
||||
[
|
||||
'label' => '品牌',
|
||||
'value' => $this->textValue($productInfo['brand_name'] ?? '') ?: '-',
|
||||
'remark' => '',
|
||||
],
|
||||
];
|
||||
|
||||
foreach (($resultInfo['key_points'] ?? []) as $point) {
|
||||
if (!is_array($point)) {
|
||||
continue;
|
||||
}
|
||||
$label = $this->textValue($point['point_name'] ?? '');
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'label' => $label,
|
||||
'value' => $this->textValue($point['point_value'] ?? '') ?: '-',
|
||||
'remark' => $this->textValue($point['point_remark'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'product_name' => $this->textValue($productInfo['product_name'] ?? '') ?: '-',
|
||||
'institution_name' => $this->displayInstitutionName((string)($report['service_provider'] ?? 'anxinyan')),
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildTraceInfo(int $orderId, array $appraisalInfo, array $evidenceAttachments, Request $request): array
|
||||
{
|
||||
$logs = $orderId > 0
|
||||
? Db::name('order_transfer_flow_logs')
|
||||
->where('order_id', $orderId)
|
||||
->whereIn('action_code', ['inbound_received', 'return_shipped'])
|
||||
->order('id', 'asc')
|
||||
->select()
|
||||
->toArray()
|
||||
: [];
|
||||
|
||||
$inboundLog = $this->findFlowLog($logs, 'inbound_received');
|
||||
$returnLog = $this->findFlowLog($logs, 'return_shipped');
|
||||
$inboundPayload = $this->decodeJsonObject($inboundLog['payload_json'] ?? null);
|
||||
$returnPayload = $this->decodeJsonObject($returnLog['payload_json'] ?? null);
|
||||
$appraisalFinishedAt = $this->appraisalFinishedAt($orderId, $appraisalInfo);
|
||||
$appraisalVideoAssets = $this->filterAssetsByType($evidenceAttachments, 'video');
|
||||
|
||||
$nodes = [
|
||||
[
|
||||
'code' => 'inbound',
|
||||
'title' => '入仓',
|
||||
'occurred_at' => (string)($inboundLog['created_at'] ?? ''),
|
||||
'status' => $inboundLog ? 'completed' : 'pending',
|
||||
'assets' => $this->evidenceService()->normalize($inboundPayload['inbound_attachments'] ?? [], $request),
|
||||
],
|
||||
[
|
||||
'code' => 'appraisal',
|
||||
'title' => '鉴定',
|
||||
'occurred_at' => $appraisalFinishedAt,
|
||||
'status' => ($appraisalFinishedAt !== '' || $appraisalVideoAssets) ? 'completed' : 'pending',
|
||||
'assets' => $appraisalVideoAssets,
|
||||
],
|
||||
[
|
||||
'code' => 'return',
|
||||
'title' => '寄回',
|
||||
'occurred_at' => (string)($returnLog['created_at'] ?? ''),
|
||||
'status' => $returnLog ? 'completed' : 'pending',
|
||||
'assets' => $this->evidenceService()->normalize($returnPayload['packing_attachments'] ?? [], $request),
|
||||
],
|
||||
];
|
||||
|
||||
return ['nodes' => $nodes];
|
||||
}
|
||||
|
||||
private function findFlowLog(array $logs, string $actionCode): ?array
|
||||
{
|
||||
$matched = null;
|
||||
foreach ($logs as $log) {
|
||||
if (($log['action_code'] ?? '') === $actionCode) {
|
||||
$matched = $log;
|
||||
}
|
||||
}
|
||||
return $matched;
|
||||
}
|
||||
|
||||
private function appraisalFinishedAt(int $orderId, array $appraisalInfo): string
|
||||
{
|
||||
if ($orderId > 0) {
|
||||
$submittedAt = Db::name('appraisal_tasks')
|
||||
->where('order_id', $orderId)
|
||||
->whereRaw('submitted_at IS NOT NULL')
|
||||
->order('submitted_at', 'desc')
|
||||
->value('submitted_at');
|
||||
if ($submittedAt) {
|
||||
return (string)$submittedAt;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->textValue($appraisalInfo['appraisal_time'] ?? '');
|
||||
}
|
||||
|
||||
private function filterAssetsByType(array $assets, string $fileType): array
|
||||
{
|
||||
return array_values(array_filter($assets, fn (array $asset) => (string)($asset['file_type'] ?? '') === $fileType));
|
||||
}
|
||||
|
||||
private function shouldRebuildPdf(array $existingFile, array $report, array $content): bool
|
||||
{
|
||||
$fileUpdatedAt = strtotime((string)($existingFile['updated_at'] ?? '')) ?: 0;
|
||||
if ($fileUpdatedAt <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ([$report['updated_at'] ?? '', $content['updated_at'] ?? ''] as $timestamp) {
|
||||
if ((strtotime((string)$timestamp) ?: 0) > $fileUpdatedAt) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function firstPdfHeroImage(array $images): ?array
|
||||
{
|
||||
foreach ($images as $image) {
|
||||
$binary = $this->readAssetBinary((string)($image['file_url'] ?? ''));
|
||||
if ($binary === '' || strlen($binary) > 5 * 1024 * 1024) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$info = @getimagesizefromstring($binary);
|
||||
if (!$info) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((int)($info[2] ?? 0) !== IMAGETYPE_JPEG) {
|
||||
$converted = $this->convertImageBinaryToJpeg($binary);
|
||||
if ($converted === '') {
|
||||
continue;
|
||||
}
|
||||
$binary = $converted;
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $binary,
|
||||
'width' => (int)$info[0],
|
||||
'height' => (int)$info[1],
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function convertImageBinaryToJpeg(string $binary): string
|
||||
{
|
||||
if (!function_exists('imagecreatefromstring')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$image = @imagecreatefromstring($binary);
|
||||
if (!$image) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$width = imagesx($image);
|
||||
$height = imagesy($image);
|
||||
$canvas = imagecreatetruecolor($width, $height);
|
||||
if (!$canvas) {
|
||||
imagedestroy($image);
|
||||
return '';
|
||||
}
|
||||
|
||||
$background = imagecolorallocate($canvas, 255, 255, 255);
|
||||
imagefill($canvas, 0, 0, $background);
|
||||
imagecopy($canvas, $image, 0, 0, 0, 0, $width, $height);
|
||||
ob_start();
|
||||
imagejpeg($canvas, null, 86);
|
||||
$jpeg = (string)ob_get_clean();
|
||||
imagedestroy($canvas);
|
||||
imagedestroy($image);
|
||||
|
||||
return $jpeg;
|
||||
}
|
||||
|
||||
private function readAssetBinary(string $fileUrl): string
|
||||
{
|
||||
$fileUrl = trim($fileUrl);
|
||||
if ($fileUrl === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$relativePath = $this->storage()->storagePath($fileUrl);
|
||||
$localPath = public_path() . '/' . $relativePath;
|
||||
if (is_file($localPath)) {
|
||||
return (string)file_get_contents($localPath);
|
||||
}
|
||||
|
||||
if (preg_match('/^https?:\/\//i', $fileUrl)) {
|
||||
$context = stream_context_create([
|
||||
'http' => ['timeout' => 3],
|
||||
'ssl' => ['verify_peer' => false, 'verify_peer_name' => false],
|
||||
]);
|
||||
return (string)(@file_get_contents($fileUrl, false, $context) ?: '');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function assetNameList(array $assets): array
|
||||
{
|
||||
return array_values(array_filter(array_map(function (array $asset) {
|
||||
return $this->textValue($asset['name'] ?? '') ?: $this->textValue($asset['file_id'] ?? '');
|
||||
}, $assets)));
|
||||
}
|
||||
|
||||
private function insertMaterialTagVerifyLog(array $tag, string $reportNo, string $verifyCode, bool $passed, Request $request, string $now): void
|
||||
{
|
||||
Db::name('material_tag_scan_logs')->insert([
|
||||
'tag_code_id' => (int)$tag['id'],
|
||||
'batch_id' => (int)$tag['batch_id'],
|
||||
'report_id' => (int)($tag['report_id'] ?? 0) ?: null,
|
||||
'report_no' => $reportNo,
|
||||
'verify_type' => 'report_page_code',
|
||||
'verify_code_input' => mb_substr($verifyCode, 0, 16),
|
||||
'verify_passed' => $passed ? 1 : 0,
|
||||
'ip' => (string)$request->getRealIp(),
|
||||
'user_agent' => mb_substr((string)$request->header('user-agent', ''), 0, 500),
|
||||
'scanned_at' => $now,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
|
||||
private function decodeJsonObject(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 textValue(mixed $value): string
|
||||
{
|
||||
return trim((string)($value ?? ''));
|
||||
}
|
||||
|
||||
private function displayInstitutionName(string $serviceProvider): string
|
||||
{
|
||||
return $serviceProvider === 'zhongjian' ? '中检鉴定中心' : '安心检验';
|
||||
}
|
||||
|
||||
private function buildPublicPageUrl(string $pagePath, array $query = []): string
|
||||
{
|
||||
$baseUrl = $this->normalizeH5BaseUrl($this->getSystemConfigValue('h5', 'page_base_url'));
|
||||
|
||||
@@ -46,6 +46,7 @@ class AppAuthMiddleware implements MiddlewareInterface
|
||||
'/api/app/help-center',
|
||||
'/api/app/help-article/detail',
|
||||
'/api/app/report/detail',
|
||||
'/api/app/report/anti-counterfeit/verify',
|
||||
'/api/app/verify',
|
||||
'/api/app/material-tag',
|
||||
'/api/app/material-tag/verify',
|
||||
|
||||
@@ -6,49 +6,59 @@ class ReportPdfGenerator
|
||||
{
|
||||
public function generate(array $payload): string
|
||||
{
|
||||
$content = $this->buildContentStream($payload);
|
||||
$heroImage = $this->normalizeHeroImage($payload['hero_image'] ?? null);
|
||||
$content = $this->buildContentStream($payload, $heroImage);
|
||||
$resources = '<< /Font << /F1 5 0 R >>';
|
||||
if ($heroImage) {
|
||||
$resources .= ' /XObject << /Im1 7 0 R >>';
|
||||
}
|
||||
$resources .= ' >>';
|
||||
|
||||
$objects = [
|
||||
1 => '<< /Type /Catalog /Pages 2 0 R >>',
|
||||
2 => '<< /Type /Pages /Kids [3 0 R] /Count 1 >>',
|
||||
3 => '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources << /Font << /F1 5 0 R >> >> /Contents 4 0 R >>',
|
||||
3 => sprintf('<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] /Resources %s /Contents 4 0 R >>', $resources),
|
||||
4 => sprintf("<< /Length %d >>\nstream\n%s\nendstream", strlen($content), $content),
|
||||
5 => '<< /Type /Font /Subtype /Type0 /BaseFont /STSong-Light /Encoding /UniGB-UCS2-H /DescendantFonts [6 0 R] >>',
|
||||
6 => '<< /Type /Font /Subtype /CIDFontType0 /BaseFont /STSong-Light /CIDSystemInfo << /Registry (Adobe) /Ordering (GB1) /Supplement 4 >> /DW 1000 >>',
|
||||
];
|
||||
if ($heroImage) {
|
||||
$objects[7] = sprintf(
|
||||
"<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>\nstream\n%s\nendstream",
|
||||
$heroImage['width'],
|
||||
$heroImage['height'],
|
||||
strlen($heroImage['data']),
|
||||
$heroImage['data']
|
||||
);
|
||||
}
|
||||
|
||||
return $this->renderPdf($objects);
|
||||
}
|
||||
|
||||
private function buildContentStream(array $payload): string
|
||||
private function buildContentStream(array $payload, ?array $heroImage): string
|
||||
{
|
||||
$title = $this->normalizeText((string)($payload['report_title'] ?? '鉴定报告'));
|
||||
$serviceProviderText = $this->normalizeText((string)($payload['service_provider_text'] ?? '-'));
|
||||
$institutionName = $this->normalizeText((string)($payload['institution_name'] ?? '-'));
|
||||
$reportNo = $this->normalizeText((string)($payload['report_no'] ?? '-'));
|
||||
$publishTime = $this->normalizeText((string)($payload['publish_time'] ?? '-'));
|
||||
$resultText = $this->normalizeText((string)($payload['result_text'] ?? '-'));
|
||||
$resultDesc = $this->normalizeText((string)($payload['result_desc'] ?? '-'));
|
||||
$productName = $this->normalizeText((string)($payload['product_name'] ?? '-'));
|
||||
$categoryBrand = $this->normalizeText((string)($payload['category_brand'] ?? '-'));
|
||||
$specInfo = $this->normalizeText((string)($payload['spec_info'] ?? '-'));
|
||||
$appraisers = $this->normalizeText((string)($payload['appraisers'] ?? '-'));
|
||||
$conditionGrade = $this->normalizeText((string)($payload['condition_grade'] ?? '-'));
|
||||
$valuationRange = $this->normalizeText((string)($payload['valuation_range'] ?? '-'));
|
||||
$verifyInfo = $this->normalizeText((string)($payload['verify_info'] ?? '-'));
|
||||
$riskNotice = $this->normalizeText((string)($payload['risk_notice_text'] ?? '-'));
|
||||
$productItems = is_array($payload['product_items'] ?? null) ? $payload['product_items'] : [];
|
||||
$heroImageLabels = is_array($payload['hero_image_labels'] ?? null) ? $payload['hero_image_labels'] : [];
|
||||
|
||||
$blocks = [];
|
||||
$y = 790;
|
||||
|
||||
$blocks[] = $this->textBlock($title, 52, $y, 20);
|
||||
$y -= 32;
|
||||
$blocks[] = $this->textBlock('正式报告凭证,请以编号验真结果为准。', 52, $y, 10);
|
||||
$blocks[] = $this->textBlock('正式报告凭证,请以报告页防伪查询结果为准。', 52, $y, 10);
|
||||
$y -= 30;
|
||||
|
||||
foreach ([
|
||||
sprintf('报告编号:%s', $reportNo),
|
||||
sprintf('出具机构:%s', $institutionName),
|
||||
sprintf('检测机构:%s', $institutionName),
|
||||
sprintf('出具时间:%s', $publishTime),
|
||||
sprintf('服务类型:%s', $serviceProviderText),
|
||||
] as $line) {
|
||||
@@ -56,29 +66,56 @@ class ReportPdfGenerator
|
||||
$y -= 22;
|
||||
}
|
||||
|
||||
$y -= 8;
|
||||
$blocks[] = $this->textBlock(sprintf('鉴定结论:%s', $resultText), 52, $y, 16);
|
||||
$y -= 26;
|
||||
|
||||
foreach ($this->wrapText('结果说明:' . $resultDesc, 30) as $line) {
|
||||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||||
$y -= 18;
|
||||
if ($heroImage) {
|
||||
$maxWidth = 230;
|
||||
$maxHeight = 150;
|
||||
$scale = min($maxWidth / $heroImage['width'], $maxHeight / $heroImage['height'], 1);
|
||||
$drawWidth = (int)round($heroImage['width'] * $scale);
|
||||
$drawHeight = (int)round($heroImage['height'] * $scale);
|
||||
$y -= $drawHeight + 6;
|
||||
$blocks[] = $this->imageBlock('Im1', 52, $y, $drawWidth, $drawHeight);
|
||||
$y -= 24;
|
||||
} elseif ($heroImageLabels) {
|
||||
foreach ($this->wrapText('鉴定图片:' . implode('、', array_slice($heroImageLabels, 0, 3)), 34) as $line) {
|
||||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||||
$y -= 18;
|
||||
}
|
||||
$y -= 8;
|
||||
}
|
||||
|
||||
$y -= 8;
|
||||
foreach ([
|
||||
sprintf('商品名称:%s', $productName),
|
||||
sprintf('品类 / 品牌:%s', $categoryBrand),
|
||||
sprintf('颜色 / 规格:%s', $specInfo),
|
||||
sprintf('鉴定师:%s', $appraisers),
|
||||
sprintf('成色评级:%s', $conditionGrade),
|
||||
sprintf('估值区间:%s', $valuationRange),
|
||||
sprintf('验真信息:%s', $verifyInfo),
|
||||
] as $line) {
|
||||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||||
$y -= 20;
|
||||
$blocks[] = $this->textBlock('产品信息', 52, $y, 15);
|
||||
$y -= 24;
|
||||
$blocks[] = $this->textBlock(sprintf('产品名称:%s', $productName), 52, $y, 12);
|
||||
$y -= 22;
|
||||
|
||||
foreach ($productItems as $item) {
|
||||
if (!is_array($item)) {
|
||||
continue;
|
||||
}
|
||||
$label = $this->normalizeText((string)($item['label'] ?? ''));
|
||||
if ($label === '') {
|
||||
continue;
|
||||
}
|
||||
$value = $this->normalizeText((string)($item['value'] ?? '-'));
|
||||
$remark = $this->normalizeText((string)($item['remark'] ?? ''));
|
||||
foreach ($this->wrapText(sprintf('%s:%s', $label, $value !== '' ? $value : '-'), 34) as $line) {
|
||||
$blocks[] = $this->textBlock($line, 52, $y, 11);
|
||||
$y -= 18;
|
||||
}
|
||||
if ($remark !== '') {
|
||||
foreach ($this->wrapText('说明:' . $remark, 34) as $line) {
|
||||
$blocks[] = $this->textBlock($line, 72, $y, 10);
|
||||
$y -= 16;
|
||||
}
|
||||
}
|
||||
$y -= 2;
|
||||
}
|
||||
|
||||
$y -= 6;
|
||||
$blocks[] = $this->textBlock(sprintf('验真信息:%s', $verifyInfo), 52, $y, 11);
|
||||
$y -= 22;
|
||||
|
||||
$y -= 8;
|
||||
foreach ($this->wrapText('风险说明:' . $riskNotice, 30) as $line) {
|
||||
$blocks[] = $this->textBlock($line, 52, $y, 10);
|
||||
@@ -86,12 +123,32 @@ class ReportPdfGenerator
|
||||
}
|
||||
|
||||
if ($y > 48) {
|
||||
$blocks[] = $this->textBlock('安心验鉴定平台', 52, 42, 9);
|
||||
$blocks[] = $this->textBlock('安心检验鉴定平台', 52, 42, 9);
|
||||
}
|
||||
|
||||
return implode("\n", array_filter($blocks));
|
||||
}
|
||||
|
||||
private function normalizeHeroImage(mixed $image): ?array
|
||||
{
|
||||
if (!is_array($image)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = (string)($image['data'] ?? '');
|
||||
$width = (int)($image['width'] ?? 0);
|
||||
$height = (int)($image['height'] ?? 0);
|
||||
if ($data === '' || $width <= 0 || $height <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'width' => $width,
|
||||
'height' => $height,
|
||||
];
|
||||
}
|
||||
|
||||
private function wrapText(string $text, int $maxUnits): array
|
||||
{
|
||||
$normalized = $this->normalizeText($text);
|
||||
@@ -137,6 +194,11 @@ class ReportPdfGenerator
|
||||
);
|
||||
}
|
||||
|
||||
private function imageBlock(string $name, int $x, int $y, int $width, int $height): string
|
||||
{
|
||||
return sprintf("q\n%d 0 0 %d %d %d cm\n/%s Do\nQ", $width, $height, $x, $y, $name);
|
||||
}
|
||||
|
||||
private function normalizeText(string $text): string
|
||||
{
|
||||
$text = trim(str_replace(["\r\n", "\r", "\n", "\t"], [' ', ' ', ' ', ' '], $text));
|
||||
|
||||
Reference in New Issue
Block a user