ensureCanManage(); $request->merge(['partner_type' => $request->input('partner_type', 'courier')]); $request->validate(['partner_type' => ['required', 'in:courier,supplier']]); if ($request->input('partner_type') === 'supplier') { if ($request->input('document_type') === 'cooperation-contract') { $request->validate(['supplier_contract_template' => ['required', Rule::in(array_keys(SupplierCooperationTemplates::TEMPLATES))]]); } $request->validate(['supplier_id' => ['required', 'integer', 'min:1'], 'document_type' => ['required', 'in:account-proof,account-change,entity-change,authorization,cooperation-contract']]); $supplier = app(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class) ->signingSupplier((int) $request->input('supplier_id')); if (! $supplier) throw ValidationException::withMessages(['supplier_id' => '供应商资料暂不可用,请重新选择供应商。']); $request->merge(['courier_name' => trim((string) $supplier->scmname)]); if (! $request->filled('esign_account')) $request->merge(['esign_account' => trim((string) $supplier->accountbankno)]); } abort_unless(EsignClient::configured(), 422, 'E 签宝沙箱尚未配置 App ID 或 AppSecret。'); $requestedType = (string) $request->input('document_type'); if ($requestedType === 'quote-confirmation') { abort_unless( Schema::hasTable('business_courier_quote_versions') && Schema::hasTable('business_courier_quote_signatures'), 503, '报价签署数据表尚未完成迁移,请先执行数据库迁移。', ); } else { abort_unless(Schema::hasTable('business_courier_agreement_signatures'), 503, '协议签署数据表尚未完成迁移,请先执行数据库迁移。'); } if ($request->input('document_type') === 'cooperation-contract' && $request->input('partner_type') === 'courier') { // 合作合同统一由我方固定 E 签宝账号完成第二签署方签署。 $request->merge([ 'secondary_esign_account' => self::COOPERATION_SECONDARY_ESIGN_ACCOUNT, ]); } if (in_array($requestedType, ['account-proof', 'account-change', 'entity-change', 'cooperation-contract', 'authorization', 'shared-contract'], true) && ! $request->filled('counterparty_signer_type')) { // Existing integrations omit this field and historically meant an // enterprise signer, so keep that behavior as the compatibility default. $request->merge(['counterparty_signer_type' => 'organization']); } $data = $request->validate($this->rules(), [ 'secondary_operator_name.required_if' => '请填写我方 E 签宝账号绑定的实名认证姓名。', ]); $type = $data['document_type']; if ($type === 'authorization' && count($data['authorization_items'] ?? []) === 0) { throw ValidationException::withMessages(['authorization_items' => '授权委托书至少选择一项授权事项。']); } if ($type === 'shared-contract') { $data['contract_partner_name'] = trim((string) $data['contract_partner_name']); if ($data['counterparty_signer_type'] === 'person') { $data['operator_name'] = $data['contract_partner_name']; } elseif (blank($data['operator_name'] ?? null)) { throw ValidationException::withMessages(['operator_name' => '请填写物流商归属企业经办人的 E 签宝实名认证姓名。']); } $sharedCouriers = collect($data['shared_courier_names'] ?? []) ->map(static fn ($name): string => trim((string) $name)) ->filter() ->unique() ->values() ->all(); if ($sharedCouriers === [] && filled($data['shared_courier_name'] ?? null)) { $sharedCouriers = [trim((string) $data['shared_courier_name'])]; } if ($sharedCouriers === []) { throw ValidationException::withMessages(['shared_courier_names' => '请至少填写一家共用合同的快递公司。']); } $data['shared_courier_names'] = $sharedCouriers; // Keep the first name for old records and integrations that still read the scalar field. $data['shared_courier_name'] = $sharedCouriers[0]; $data['shared_note'] = trim((string) ($data['shared_note'] ?? '')) ?: null; } if (in_array($type, ['account-proof', 'account-change'], true)) { $data['counterparty_signer_type'] = $data['counterparty_signer_type'] ?? 'organization'; $data['operator_name'] = trim((string) ($data['operator_name'] ?? '')) ?: null; if ($data['counterparty_signer_type'] === 'organization' && blank($data['operator_name'])) { throw ValidationException::withMessages(['operator_name' => '企业签署主体必须填写 E 签宝实名认证姓名。']); } if ($data['counterparty_signer_type'] === 'person') { // A personal signer uses the same real name for the signer and // the E-sign identity; the UI should not require it twice. $data['operator_name'] = trim((string) $data['counterparty_organization_name']); } } if ($type === 'entity-change') { $formerCompanyDeregistered = filter_var( $data['entity_change_old_company_deregistered'] ?? false, FILTER_VALIDATE_BOOLEAN, ); $data['entity_change_old_company_deregistered'] = $formerCompanyDeregistered; $data['counterparty_signer_type'] = $formerCompanyDeregistered ? 'person' : 'organization'; if (blank($data['operator_name'] ?? null)) { throw ValidationException::withMessages(['operator_name' => $formerCompanyDeregistered ? '请填写原公司法人的 E 签宝实名认证姓名。' : '请填写变更前主体签署人的 E 签宝实名认证姓名。']); } if (blank($data['secondary_esign_account'] ?? null)) { throw ValidationException::withMessages(['secondary_esign_account' => '请填写变更后主体的 E 签宝账号。']); } if (blank($data['secondary_operator_name'] ?? null)) { throw ValidationException::withMessages(['secondary_operator_name' => '请填写变更后主体签署人的 E 签宝实名认证姓名。']); } } if ($type === 'cooperation-contract') { $data['counterparty_signer_type'] = $data['counterparty_signer_type'] ?? 'organization'; if ($data['counterparty_signer_type'] === 'person') { $data['operator_name'] = trim((string) $data['cooperation_party_b_name']); } elseif (blank($data['operator_name'] ?? null)) { $party = $data['partner_type'] === 'supplier' ? '供应商' : '快递方'; throw ValidationException::withMessages(['operator_name' => "请填写{$party}企业经办人的 E 签宝实名认证姓名。"]); } if (blank($data['secondary_operator_name'] ?? null)) { throw ValidationException::withMessages(['secondary_operator_name' => '请填写我方 E 签宝账号绑定的实名认证姓名。']); } $companyNames = [trim($data['cooperation_party_a_name'])]; if ($data['counterparty_signer_type'] === 'organization') { $companyNames[] = trim($data['cooperation_party_b_name']); } foreach (['operator_name' => $data['partner_type'] === 'supplier' ? '供应商' : '快递方', 'secondary_operator_name' => '我方'] as $field => $party) { $data[$field] = trim($data[$field]); if (in_array($data[$field], $companyNames, true)) { throw ValidationException::withMessages([ $field => "{$party}实名认证姓名应填写账号绑定的个人姓名,不能填写甲乙方公司名称。", ]); } } } if ($type === 'authorization') { $data['counterparty_signer_type'] = $data['counterparty_signer_type'] ?? 'organization'; $data['secondary_signer_type'] = $data['secondary_signer_type'] ?? 'organization'; if ($data['counterparty_signer_type'] === 'person') { $data['operator_name'] = trim((string) $data['principal_name']); } elseif (blank($data['operator_name'] ?? null)) { throw ValidationException::withMessages(['operator_name' => '请填写委托方企业经办人的 E 签宝实名认证姓名。']); } if ($data['secondary_signer_type'] === 'person') { $data['secondary_operator_name'] = trim((string) $data['trustee_name']); } elseif (blank($data['secondary_operator_name'] ?? null)) { throw ValidationException::withMessages(['secondary_operator_name' => '请填写受托方企业经办人的 E 签宝实名认证姓名。']); } } $this->validateEsignAccount($data['esign_account'], 'esign_account'); if (! empty($data['secondary_esign_account'])) { $this->validateEsignAccount($data['secondary_esign_account'], 'secondary_esign_account'); } if ($type === 'quote-confirmation') { return $this->sendQuoteConfirmation($data); } // signing_subject identifies our initiating company. The E-sign // organization must be the counterparty that receives this request. $data['organization_name'] = $this->counterpartyOrganizationName($data); $data['operator_name'] = trim((string) ($data['operator_name'] ?? '')) ?: null; $data['operator_account'] = $data['esign_account']; $data['payload'] = $this->payload($data); $signature = $this->service->send($data, user_id()); return redirect() ->route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks']) ->with('success', $signature->status === CourierAgreementSignature::STATUS_SIGNING ? "{$signature->documentLabel()}已推送到 E 签宝,等待对方签署。" : "{$signature->documentLabel()}签署任务已受理。") ->with('agreement_signature_id', $signature->id); } /** * Create a signing subject for the current tenant. * * The subject is deliberately kept separate from the global companies table: * one tenant may need to sign agreements using more than one legal entity. */ public function storeSubject(Request $request): JsonResponse { $this->ensureCanManage(); if ($response = $this->subjectsTableUnavailable()) { return $response; } $data = $this->subjectData($request); $subject = DB::transaction(function () use ($data): CourierSigningSubject { $this->ensureSubjectNameAvailable($data['name']); $isFirst = ! CourierSigningSubject::query()->exists(); return CourierSigningSubject::create([ 'company_id' => company_id(), 'name' => $data['name'], 'unified_social_credit_code' => $data['unified_social_credit_code'], 'is_default' => $isFirst, 'created_by' => user_id(), 'updated_by' => user_id(), ]); }); return $this->subjectResponse($subject, '公司主体已新增。'); } public function updateSubject(Request $request, CourierSigningSubject $subject): JsonResponse { $this->ensureCanManage(); $this->ensureSubjectBelongsToCurrentCompany($subject); if ($response = $this->subjectsTableUnavailable()) { return $response; } $data = $this->subjectData($request); $this->ensureSubjectNameAvailable($data['name'], $subject); $subject->update([ 'name' => $data['name'], 'unified_social_credit_code' => $data['unified_social_credit_code'], 'updated_by' => user_id(), ]); return $this->subjectResponse($subject->fresh(), '公司主体已更新。'); } public function setDefaultSubject(CourierSigningSubject $subject): JsonResponse { $this->ensureCanManage(); $this->ensureSubjectBelongsToCurrentCompany($subject); if ($response = $this->subjectsTableUnavailable()) { return $response; } DB::transaction(function () use ($subject): void { CourierSigningSubject::query() ->where('is_default', true) ->update([ 'is_default' => false, 'updated_by' => user_id(), 'updated_at' => now(), ]); $subject->forceFill([ 'is_default' => true, 'updated_by' => user_id(), ])->save(); }); return $this->subjectResponse($subject->fresh(), '默认签署主体已切换。'); } public function destroySubject(Request $request, CourierSigningSubject $subject): JsonResponse { $this->ensureCanManage(); $this->ensureSubjectBelongsToCurrentCompany($subject); if ($response = $this->subjectsTableUnavailable()) { return $response; } $request->validate(['delete_confirmed' => ['required', 'accepted']]); DB::transaction(function () use ($subject): void { $subject = CourierSigningSubject::query()->disableCache()->lockForUpdate()->findOrFail($subject->id); $wasDefault = $subject->is_default; $subject->forceFill(['is_default' => false, 'updated_by' => user_id()])->save(); $subject->delete(); if ($wasDefault) { $nextDefault = CourierSigningSubject::query()->disableCache()->orderBy('id')->lockForUpdate()->first(); $nextDefault?->forceFill(['is_default' => true, 'updated_by' => user_id()])->save(); } }); return $this->subjectResponse(null, '公司主体已删除,历史协议的主体快照保留。'); } public function synchronize(CourierAgreementSignature $agreementSignature): RedirectResponse { $this->ensureCanManage(); $signature = $this->service->synchronize($agreementSignature, user_id()); return redirect() ->route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks']) ->with('success', "{$signature->documentLabel()}状态已同步:{$signature->statusLabel()}。"); } public function archive(Request $request, CourierAgreementSignature $agreementSignature): RedirectResponse { $this->ensureCanManage(); abort_unless($agreementSignature->status === CourierAgreementSignature::STATUS_COMPLETED && $agreementSignature->remote_status === '2', 422, '只有签署完成的协议可以人工归档。'); $data = $request->validate([ 'archive_reason_code' => ['required', 'in:completed,confirmed,backfill,other'], 'archive_note' => ['nullable', 'string', 'max:200'], ]); $reasonLabel = match ($data['archive_reason_code']) { 'completed' => '签署完成', 'confirmed' => '财务已确认', 'backfill' => '历史补录', 'other' => '其他', }; $note = trim((string) ($data['archive_note'] ?? '')); if ($data['archive_reason_code'] === 'other' && $note === '') { throw ValidationException::withMessages(['archive_note' => '选择“其他”时请填写归档说明。']); } $reason = $note === '' ? $reasonLabel : $reasonLabel . ':' . $note; $signature = $this->service->archive($agreementSignature, null, user_id(), $reason); return redirect() ->route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks']) ->with('success', "{$signature->documentLabel()}已人工确认并归档。"); } public function destroy(Request $request, CourierAgreementSignature $agreementSignature): JsonResponse|RedirectResponse { return $this->deleteTask($request, $agreementSignature); } public function destroyQuote(Request $request, CourierQuoteSignature $quoteSignature): JsonResponse|RedirectResponse { return $this->deleteTask($request, $quoteSignature); } private function deleteTask(Request $request, CourierAgreementSignature|CourierQuoteSignature $task): JsonResponse|RedirectResponse { $this->ensureCanManage(); abort_unless($task->company_id === (int) company_id(), 404); $request->validate(['delete_confirmed' => ['required', 'accepted']]); DB::transaction(function () use ($task): void { $signature = $task::query()->disableCache()->lockForUpdate()->findOrFail($task->id); // Soft-delete only the local task, retaining its files, remote flow and audit records. $signature->updated_by = user_id(); $signature->save(); $signature->delete(); }); $message = '签署任务已删除。'; return $request->expectsJson() ? response()->json(['message' => $message]) : redirect()->route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks']) ->with('success', $message); } public function download(CourierAgreementSignature $agreementSignature, string $kind): StreamedResponse|RedirectResponse { $this->ensureCanManage(); try { [$path, $suffix] = match ($kind) { 'source' => [$this->agreementFileArchive->sourcePath($agreementSignature), '签署原文'], 'signed' => [$agreementSignature->signed_pdf_path, '已签署协议'], 'certificate' => [$agreementSignature->certificate_path, '签署完成证书'], default => abort(404), }; } catch (RuntimeException $exception) { return redirect() ->route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks', ]) ->with('error', $exception->getMessage()); } abort_if(blank($path) || ! \Storage::disk('uploads')->exists($path), 404, '归档文件不存在。'); return \Storage::disk('uploads')->download($path, $agreementSignature->business_no . '-' . $suffix . '.pdf', ['Content-Type' => 'application/pdf']); } public function synchronizeQuote(CourierQuoteSignature $quoteSignature): RedirectResponse { $this->ensureCanManage(); $signature = $this->quoteSignatureService->synchronize($quoteSignature, user_id()); return redirect() ->route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks']) ->with('success', "报价确认单状态已同步:{$signature->statusLabel()}。"); } public function downloadQuote(CourierQuoteSignature $quoteSignature, string $kind): StreamedResponse { $this->ensureCanManage(); [$path, $suffix] = match ($kind) { 'source' => [$quoteSignature->source_pdf_path, '报价确认单原文'], 'signed' => [$quoteSignature->signed_pdf_path, '已签署报价确认单'], 'certificate' => [$quoteSignature->certificate_path, '签署完成证书'], default => abort(404), }; abort_if(blank($path) || ! \Storage::disk('uploads')->exists($path), 404, '归档文件不存在。'); $versionCode = $quoteSignature->version?->version_code ?: (string) $quoteSignature->quote_version_id; return \Storage::disk('uploads')->download($path, $versionCode . '-' . $suffix . '.pdf', ['Content-Type' => 'application/pdf']); } public function previewQuote(CourierQuoteVersion $quoteVersion): JsonResponse { $this->ensureCanManage(); abort_unless($quoteVersion->status === 'approved', 422, '只有已审核报价版本可以生成报价确认单。'); return response()->json($this->quoteConfirmation->data($quoteVersion)); } private function rules(): array { $date = ['required', 'date', 'after_or_equal:today', 'before_or_equal:' . now()->addDays(90)->toDateString()]; $supplierContract = request('partner_type') === 'supplier' && request('document_type') === 'cooperation-contract'; $supplierFields = []; foreach (SupplierCooperationTemplates::FIELDS as $key => $field) { $supplierFields['supplier_contract_fields.' . $key] = [ Rule::requiredIf($supplierContract && ($field['required'] ?? false)), 'nullable', 'string', 'max:' . ($key === 'supply_price' ? 1000 : 255), ]; if (($field['type'] ?? '') === 'email') { $supplierFields['supplier_contract_fields.' . $key][] = 'email'; } } return [ 'supplier_contract_template' => [Rule::requiredIf($supplierContract), 'nullable', Rule::in(array_keys(SupplierCooperationTemplates::TEMPLATES)), Rule::prohibitedIf(! $supplierContract)], 'supplier_contract_fields' => ['nullable', 'array:' . implode(',', array_keys(SupplierCooperationTemplates::FIELDS))], 'partner_type' => ['required', 'in:courier,supplier'], 'supplier_id' => ['required_if:partner_type,supplier', 'nullable', 'integer', 'min:1'], 'idempotency_key' => ['required', 'string', 'max:80', 'regex:/^[A-Za-z0-9_-]+$/'], 'courier_name' => ['required', 'string', 'max:255'], 'document_type' => ['required', 'in:' . implode(',', array_keys(CourierAgreementSignature::DOCUMENTS))], 'quote_version_id' => ['required_if:document_type,quote-confirmation', 'nullable', 'integer', 'min:1'], 'signing_subject' => ['required', 'string', 'min:2', 'max:255'], 'company_full_name' => ['required', 'string', 'min:2', 'max:255'], 'signature_date' => ['required', 'date'], 'signing_deadline' => $date, 'esign_account' => ['required', 'string', 'max:255'], 'secondary_esign_account' => ['required_if:document_type,cooperation-contract,authorization', 'nullable', 'string', 'max:255'], 'secondary_operator_name' => ['nullable', 'string', 'min:2', 'max:100'], 'operator_name' => ['nullable', 'string', 'min:2', 'max:100'], 'counterparty_organization_name' => ['required_if:document_type,account-proof,account-change', 'nullable', 'string', 'min:2', 'max:255'], 'counterparty_signer_type' => ['required_if:document_type,account-proof,account-change,entity-change,cooperation-contract,authorization,shared-contract', 'nullable', 'in:organization,person'], 'secondary_signer_type' => ['nullable', 'in:organization,person'], 'entity_change_old_company_deregistered' => ['nullable', 'boolean'], 'archive_mode' => ['required', 'in:auto,manual'], 'account_proof_effective_date' => ['required_if:document_type,account-proof', 'date'], 'account_proof_account_name' => ['required_if:document_type,account-proof', 'string', 'max:255'], 'account_proof_bank_name' => ['required_if:document_type,account-proof', 'string', 'max:255'], 'account_proof_bank_account' => ['required_if:document_type,account-proof', 'string', 'max:64', 'regex:/^[0-9A-Za-z-]+$/'], 'account_change_date' => ['required_if:document_type,account-change', 'date'], 'old_account_name' => ['required_if:document_type,account-change', 'string', 'max:255'], 'old_account_number' => ['required_if:document_type,account-change', 'string', 'max:64', 'regex:/^[0-9A-Za-z-]+$/'], 'old_account_stop_date' => ['required_if:document_type,account-change', 'date'], 'new_account_effective_date' => ['required_if:document_type,account-change', 'date'], 'new_account_name' => ['required_if:document_type,account-change', 'string', 'max:255'], 'new_bank_name' => ['required_if:document_type,account-change', 'string', 'max:255'], 'new_bank_account' => ['required_if:document_type,account-change', 'string', 'max:64', 'regex:/^[0-9A-Za-z-]+$/'], 'former_company_name' => ['required_if:document_type,entity-change', 'string', 'max:255'], 'entity_change_date' => ['required_if:document_type,entity-change', 'date'], 'changed_company_name' => ['required_if:document_type,entity-change', 'string', 'max:255'], 'changed_company_start_date' => ['required_if:document_type,entity-change', 'date'], 'contract_partner_name' => ['required_if:document_type,shared-contract', 'string', 'min:2', 'max:255'], 'shared_courier_names' => ['nullable', 'array', 'min:1'], 'shared_courier_names.*' => ['required', 'string', 'max:255'], 'shared_courier_name' => ['nullable', 'string', 'max:255'], 'shared_note' => ['nullable', 'string', 'max:2000'], 'cooperation_party_a_name' => ['required_if:document_type,cooperation-contract', 'string', 'max:255'], 'cooperation_party_a_phone' => ['required_if:document_type,cooperation-contract', 'string', 'max:64'], 'cooperation_party_b_name' => ['required_if:document_type,cooperation-contract', 'string', 'max:255'], 'cooperation_party_b_phone' => ['required_if:document_type,cooperation-contract', 'string', 'max:64'], 'cooperation_start_date' => ['required_if:document_type,cooperation-contract', 'date'], 'cooperation_end_date' => ['required_if:document_type,cooperation-contract', 'date', 'after_or_equal:cooperation_start_date'], 'principal_name' => ['required_if:document_type,authorization', 'string', 'max:100'], 'principal_id_number' => ['required_if:document_type,authorization', 'string', 'max:64'], 'trustee_name' => ['required_if:document_type,authorization', 'string', 'max:100'], 'trustee_id_number' => ['required_if:document_type,authorization', 'string', 'max:64'], 'authorization_items' => ['nullable', 'array'], 'authorization_items.*' => ['in:1,2,3,4'], 'bill_counterparty' => ['required_if:document_type,authorization', 'string', 'max:255'], 'loan_counterparty' => ['required_if:document_type,authorization', 'string', 'max:255'], 'payment_counterparty' => ['required_if:document_type,authorization', 'string', 'max:255'], 'authorization_start_date' => ['required_if:document_type,authorization', 'date'], 'authorization_end_date' => ['required_if:document_type,authorization', 'date', 'after_or_equal:authorization_start_date'], ] + $supplierFields; } private function payload(array $data): array { $allowed = array_keys($this->rules()); $payload = array_intersect_key($data, array_flip($allowed)); unset($payload['esign_account'], $payload['idempotency_key'], $payload['operator_name']); $payload['authorization_items'] = array_values(array_map('strval', $data['authorization_items'] ?? [])); if (($data['document_type'] ?? null) === 'entity-change') { $payload['former_legal_representative'] = trim((string) ($data['operator_name'] ?? '')); $payload['entity_proof_date'] = $data['signature_date'] ?? null; } if (($data['partner_type'] ?? 'courier') === 'supplier' && ($data['document_type'] ?? '') === 'cooperation-contract') { $payload['supplier_contract_signer_name'] = $data['operator_name']; } return $payload; } private function sendQuoteConfirmation(array $data): RedirectResponse { $version = CourierQuoteVersion::query() ->where('status', 'approved') ->findOrFail((int) $data['quote_version_id']); $this->ensureQuoteVersionMatchesCourier($version, $data['courier_name']); $signature = $this->quoteSignatureService->prepareVersion($version, user_id(), $data['signature_date']); $account = trim((string) $data['esign_account']); $signature = $this->quoteSignatureService->send($signature, [ 'organization_name' => $version->carrier_name, 'operator_name' => trim((string) ($data['operator_name'] ?? '')) ?: $version->carrier_name, 'operator_account' => $account, 'notice_type' => filter_var($account, FILTER_VALIDATE_EMAIL) !== false ? '2' : '1', 'expires_at' => \Illuminate\Support\Carbon::parse($data['signing_deadline'])->endOfDay(), ], user_id()); return redirect() ->route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'tasks']) ->with('success', "报价确认单 {$version->version_code} 已推送到 E 签宝,等待快递方签署并自动回传归档。") ->with('quote_signature_id', $signature->id); } private function ensureQuoteVersionMatchesCourier(CourierQuoteVersion $version, string $courier): void { abort_unless( trim($version->carrier_name) === trim($courier), 422, '所选报价版本与快递公司不一致,请重新选择。', ); } private function counterpartyOrganizationName(array $data): string { return match ($data['document_type']) { 'account-proof', 'account-change' => trim((string) $data['counterparty_organization_name']), 'shared-contract' => trim((string) $data['contract_partner_name']), 'entity-change' => trim((string) ($data['former_company_name'] ?? $data['courier_name'])), 'authorization' => trim((string) ($data['principal_name'] ?? $data['courier_name'])), default => trim((string) $data['courier_name']), }; } private function ensureCanManage(): void { abort_unless( in_array(SystemAccess::roleName(user()), [SystemAccess::SUPER_ADMIN, SystemAccess::ADMIN], true), 403, '当前账号没有协议签署管理权限。', ); } private function subjectData(Request $request): array { $data = $request->validate([ 'name' => ['required', 'string', 'max:255'], 'unified_social_credit_code' => ['nullable', 'string', 'max:32', 'regex:/^[0-9A-Za-z]{18}$/'], ]); $data['name'] = trim($data['name']); if (mb_strlen($data['name']) < 2) { throw ValidationException::withMessages(['name' => '公司名称至少填写 2 个字符。']); } $data['unified_social_credit_code'] = trim((string) ($data['unified_social_credit_code'] ?? '')) ?: null; return $data; } private function ensureSubjectNameAvailable(string $name, ?CourierSigningSubject $except = null): void { $query = CourierSigningSubject::query()->where('name', $name); if ($except) { $query->where($query->getModel()->getQualifiedKeyName(), '<>', $except->getKey()); } if ($query->exists()) { throw ValidationException::withMessages(['name' => '当前公司下已有同名签署主体。']); } } private function subjectResponse(?CourierSigningSubject $subject, string $message): JsonResponse { return response()->json([ 'message' => $message, 'subject' => $subject ? $this->serializeSubject($subject) : null, 'subjects' => CourierSigningSubject::query() ->orderByDesc('is_default') ->orderBy('name') ->get() ->map(fn (CourierSigningSubject $item): array => $this->serializeSubject($item)) ->values(), ]); } private function serializeSubject(CourierSigningSubject $subject): array { return [ 'id' => $subject->id, 'name' => $subject->name, 'unified_social_credit_code' => $subject->unified_social_credit_code, 'is_default' => (bool) $subject->is_default, ]; } private function subjectsTableUnavailable(): ?JsonResponse { if (Schema::hasTable('business_courier_signing_subjects')) { return null; } return response()->json([ 'message' => '公司主体数据表尚未完成迁移,请先执行数据库迁移。', ], 503); } private function ensureSubjectBelongsToCurrentCompany(CourierSigningSubject $subject): void { abort_unless((int) $subject->company_id === (int) company_id(), 404, '签署主体不存在。'); } private function validateEsignAccount(string $account, string $field): void { $valid = preg_match('/^1\d{10}$/', $account) === 1 || filter_var($account, FILTER_VALIDATE_EMAIL) !== false; if (! $valid) { throw ValidationException::withMessages([$field => 'E签宝账号必须是 11 位手机号或有效邮箱。']); } } }