快递应付 → 快递报价 下新增「快递入驻」页:生成入驻二维码发给快递方,
对方免登录在手机上填写主体、联系人、e签宝签署人、结算账户、报价并上传资质,
内部对接人初审、管理员财务复审后生成首版报价,并自动生成报价确认单 PDF
推送到快递填写的 e签宝账号,签署归档后开通。
- 新增三张表 business_courier_onboardings / _files / _events(纯新增,不改旧表)
- 免登录填报页 /onboarding/courier/{token}:40 位随机 token,可暂存、提交、退回后补件,
链接有效期可延长、可作废;银行账号加密存储;附件存 uploads 盘
- 新增 COURIER_ONBOARDING_PUBLIC_URL 配置二维码里的站点地址(留空用当前访问地址)
- 内置纯 PHP 二维码生成(App\Support\QrCode,SVG 输出,无新增依赖)
- 生成二维码时必须定好系统快递名称(唯一,快递方不可见);财务复审沿用该名称,快递所填报价写成报价管理的 CourierQuoteVersion,
复用 CourierQuoteSignatureService 生成报价确认单并推送 e签宝(失败可重推/同步状态);初审人默认不能复审(COURIER_ONBOARDING_ALLOW_SAME_REVIEWER=true 可在测试环境放开)
- 支持公司 / 个人两种入驻:个人填姓名+身份证号(加密)、传身份证和银行卡,报价确认单按个人身份在 e签宝签署
- 扫码先选入驻方式;证件改为 营业执照 / 法人身份证正反面 / 银行卡正反面 各一张(个人营业执照选填),手机端上传前自动压缩,超限回到填报页提示
- 报价支持 4 种计费方式(面单+费率 / 首重+续重 / 按重量段一口价 / 小重量一口价+超出另算,费率可分段)+「其他」上传报价表;附加规则:计泡、起算重量、取整、每票加收(CourierQuoteStructure)
- 不再要求上传盖章报价单,至少填写一行在线报价
- 开通:报价确认单签署归档、或匹配到已签署协议、或填写线下签署说明
- 生成二维码前必填「快递公司代码」(自动大写、不可重复),入驻信息按代码对齐快递资料
- 公司入驻对公(营业执照主体)+ 对私(法定代表人)两个打款账户必填并自动核对;个人入驻对私必填、对公选填
- 变价:报价管理里后台改价 → 审核 → 自动推送快递 e签宝 → 回签;回签单导出 xlsx/zip;误发版本可作废
- 短期(临时)变价:变价单/报价版本加选填「结束日期」(end_date);按日期取价,期间用临时价,到期次日自动恢复原长期报价,
不用重签;报价管理显示 生效中(临时)/ 暂停中 / 已到期,确认单写明执行期间和到期后恢复的版本,审核提醒期间重叠
- 快递资料分四类:快递资料 / 网点 / 打款信息 / 操作日志,支持查询、导出、重置、编辑;
财务系统入驻的快递直接作为快递资料行,合同起止取协议签署里已签署的合作合同;
点开一行在下方看该快递的网点 / 打款信息(完整账号)/ 操作日志
- 编辑存财务系统覆盖值(business_courier_profiles,不回写工作平台),改动记操作日志(business_courier_profile_events)
- 打款信息新增 / 变更(business_courier_payment_accounts,账号加密)保存后跳到协议签署,自动预填收款账户证明 / 收款账户变更,
按签署状态显示 待推送协议 / 签署中 / 已生效;入口在快递资料明细和「打款信息」页签(表格每个账户可直接「变更」)
- ERP 开通:入驻开通后标「待 ERP 开通」,可手动补录(钉钉「新建快递」流程同步待接入)
- 新增 CourierOnboardingWorkflowTest(8 个用例),更新快递报价目录顺序断言
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019gzfsnV3fJ89CoUy1D9V9g
186 lines
16 KiB
PHP
186 lines
16 KiB
PHP
@php
|
||
$ready = (bool) ($onboardingReady ?? false);
|
||
$rows = $onboardingRows ?? null;
|
||
$group = $onboardingGroup ?? 'pending';
|
||
$groups = $onboardingGroups ?? [];
|
||
$counts = $onboardingCounts ?? [];
|
||
$search = $onboardingSearch ?? '';
|
||
$groupUrl = static fn (string $key): string => route('business.pages.show', array_filter([
|
||
'section' => 'courier-payables',
|
||
'page' => 'courier-onboarding',
|
||
'group' => $key,
|
||
'search' => $search !== '' ? $search : null,
|
||
]));
|
||
$badge = static fn (string $status): string => match ($status) {
|
||
'submitted', 'first_approved', 'approved' => 'is-pending',
|
||
'returned' => 'is-warning',
|
||
'activated' => 'is-success',
|
||
'rejected', 'cancelled' => 'is-muted',
|
||
default => 'is-info',
|
||
};
|
||
@endphp
|
||
|
||
<section class="co-page" aria-label="快递入驻">
|
||
<style>
|
||
.co-page { --co-ink:var(--vip-content-ink,#171717); --co-muted:var(--vip-content-muted,#595959); --co-accent:var(--vip-content-accent-dark,#856632); --co-soft:var(--vip-content-accent-soft,#F7F0DE); --co-border:var(--vip-border,#D8D2C7); display:grid; gap:1rem; color:var(--co-ink); font-family:JetBrains Mono,"Microsoft YaHei","PingFang SC",monospace; font-size:.8125rem; }
|
||
.co-page *, .co-page *::before, .co-page *::after { box-sizing:border-box; }
|
||
.co-notice { display:flex; align-items:flex-start; gap:.6rem; padding:.75rem 1rem; border:1px solid #d7c39d; border-radius:.65rem; background:#fffaf0; color:#6a5129; line-height:1.55; }
|
||
.co-notice .material-icons-outlined { color:var(--co-accent); font-size:1.15rem; }
|
||
.co-notice.is-error { border-color:#e3b0b0; background:#fdf1f1; color:#7a1010; } .co-notice.is-error .material-icons-outlined { color:#b80000; }
|
||
.co-hero { display:grid; grid-template-columns:minmax(0,1.4fr) minmax(18rem,1fr); gap:1rem; }
|
||
.co-card { overflow:hidden; border:1px solid var(--co-border); border-radius:.75rem; background:#fff; }
|
||
.co-card__head { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.5rem 1rem; padding:.85rem 1.15rem; border-bottom:1px solid var(--co-border); }
|
||
.co-card__title { display:flex; align-items:center; gap:.45rem; margin:0; font-size:.9rem; font-weight:700; } .co-card__title .material-icons-outlined { color:var(--co-accent); font-size:1.1rem; }
|
||
.co-card__hint { color:var(--co-muted); font-size:.72rem; }
|
||
.co-flow { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:0; margin:0; padding:1rem 1.15rem 1.15rem; list-style:none; counter-reset:step; }
|
||
.co-flow li { position:relative; display:grid; gap:.25rem; padding-right:.75rem; counter-increment:step; }
|
||
.co-flow li::before { display:grid; width:1.6rem; height:1.6rem; place-items:center; border-radius:50%; background:var(--co-soft); color:var(--co-accent); font-weight:700; font-size:.72rem; content:counter(step); }
|
||
.co-flow li:not(:last-child)::after { position:absolute; top:.8rem; left:2rem; right:.5rem; height:1px; background:var(--co-border); content:''; }
|
||
.co-flow strong { font-size:.78rem; } .co-flow span { color:var(--co-muted); font-size:.7rem; line-height:1.5; }
|
||
.co-invite { display:grid; gap:.7rem; padding:1rem 1.15rem 1.15rem; }
|
||
.co-field { display:grid; gap:.3rem; min-width:0; } .co-field label { color:var(--co-muted); font-size:.72rem; font-weight:600; }
|
||
.co-field input, .co-field select { width:100%; height:2.2rem; padding:0 .6rem; border:1px solid var(--co-border); border-radius:.45rem; background:#fff; color:var(--co-ink); font:inherit; }
|
||
.co-field input:focus, .co-field select:focus { border-color:var(--co-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.18); }
|
||
.co-help { color:var(--co-muted); font-size:.68rem; line-height:1.5; } .co-error { color:#b80000; font-size:.72rem; }
|
||
.co-invite__row { display:grid; grid-template-columns:minmax(0,1fr) 7rem; gap:.6rem; }
|
||
.co-button { display:inline-flex; align-items:center; justify-content:center; gap:.35rem; height:2.2rem; padding:0 .8rem; border:1px solid var(--co-border); border-radius:.45rem; background:#fff; color:var(--co-ink); font:inherit; font-weight:600; text-decoration:none; white-space:nowrap; cursor:pointer; }
|
||
.co-button .material-icons-outlined { font-size:1rem; } .co-button--primary { border-color:var(--co-accent); background:var(--co-accent); color:#fff; } .co-button--primary:hover { background:#6a5129; }
|
||
.co-toolbar { display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:.75rem; padding:.75rem 1.15rem; border-bottom:1px solid var(--co-border); background:#fffdf8; }
|
||
.co-tabs { display:flex; flex-wrap:wrap; gap:1.1rem; }
|
||
.co-tab { display:inline-flex; align-items:center; gap:.35rem; min-height:2.1rem; padding:0 .15rem; border-bottom:2px solid transparent; color:var(--co-muted); font-weight:600; text-decoration:none; }
|
||
.co-tab.is-active { border-color:var(--co-accent); color:var(--co-accent); }
|
||
.co-tab__count { min-width:1.4rem; padding:.05rem .4rem; border-radius:999px; background:#f1ede4; color:var(--co-muted); font-size:.68rem; text-align:center; font-variant-numeric:tabular-nums; }
|
||
.co-tab.is-active .co-tab__count { background:var(--co-soft); color:var(--co-accent); }
|
||
.co-search { display:flex; gap:.45rem; } .co-search input { width:16rem; height:2.2rem; padding:0 .6rem; border:1px solid var(--co-border); border-radius:.45rem; font:inherit; }
|
||
.co-table-wrap { overflow:auto; }
|
||
.co-table { width:100%; min-width:62rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
|
||
.co-table th, .co-table td { padding:.62rem .7rem; border-bottom:1px solid var(--co-border); text-align:left; vertical-align:middle; white-space:nowrap; }
|
||
.co-table th { background:var(--co-soft); font-weight:700; }
|
||
.co-table tbody tr:hover td { background:#fcfaf4; }
|
||
.co-table .is-code { font-variant-numeric:tabular-nums; }
|
||
.co-table .is-clamp { max-width:16rem; overflow:hidden; text-overflow:ellipsis; }
|
||
.co-table a { color:var(--co-accent); font-weight:600; text-decoration:none; }
|
||
.co-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; }
|
||
.co-badge.is-pending { background:#fff4df; color:#935f07; } .co-badge.is-warning { background:#fde9d4; color:#8a4a06; } .co-badge.is-success { background:#edf5e8; color:#53793e; } .co-badge.is-muted { background:#f1efea; color:#595959; } .co-badge.is-info { background:#e8f2f8; color:#006395; }
|
||
.co-empty { padding:3rem 1rem; text-align:center; color:var(--co-muted); }
|
||
.co-footer { display:flex; align-items:center; justify-content:space-between; padding:.65rem 1.15rem; color:var(--co-muted); font-size:.72rem; }
|
||
.co-footer nav { display:flex; gap:.35rem; } .co-footer a { display:inline-grid; width:2rem; height:2rem; place-items:center; border:1px solid var(--co-border); border-radius:.4rem; color:var(--co-ink); } .co-footer a.is-disabled { pointer-events:none; opacity:.4; }
|
||
@media (max-width: 1100px) { .co-hero { grid-template-columns:1fr; } .co-flow { grid-template-columns:repeat(2,minmax(0,1fr)); row-gap:1rem; } .co-flow li::after { display:none; } }
|
||
</style>
|
||
|
||
@if (! $ready)
|
||
<div class="co-notice is-error" role="alert">
|
||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||
<span>快递入驻数据表尚未完成迁移,请联系管理员执行数据库迁移后再使用。</span>
|
||
</div>
|
||
@else
|
||
@if (session('success'))
|
||
<div class="co-notice" role="status"><span class="material-icons-outlined" aria-hidden="true">check_circle</span><span>{{ session('success') }}</span></div>
|
||
@endif
|
||
|
||
<div class="co-hero">
|
||
<div class="co-card">
|
||
<div class="co-card__head">
|
||
<h2 class="co-card__title"><span class="material-icons-outlined" aria-hidden="true">route</span>入驻流程</h2>
|
||
<span class="co-card__hint">快递方无需账号,扫码即可填写</span>
|
||
</div>
|
||
<ol class="co-flow">
|
||
<li><strong>生成二维码</strong><span>右侧填个备注,生成后发给快递方</span></li>
|
||
<li><strong>快递扫码填写</strong><span>主体、联系人、结算账户、报价、资质照片</span></li>
|
||
<li><strong>对接人初审</strong><span>资料不全可退回,快递用原码补件</span></li>
|
||
<li><strong>财务复审</strong><span>确认系统快递名称,首版报价进报价管理</span></li>
|
||
<li><strong>签署并开通</strong><span>协议签署后开通,进快递资料「入驻快递」</span></li>
|
||
</ol>
|
||
</div>
|
||
<form class="co-card" id="invite" method="POST" action="{{ route('business.courier-onboardings.store') }}">
|
||
@csrf
|
||
<div class="co-card__head">
|
||
<h2 class="co-card__title"><span class="material-icons-outlined" aria-hidden="true">qr_code_2</span>生成入驻二维码</h2>
|
||
</div>
|
||
<div class="co-invite">
|
||
<div class="co-field">
|
||
<label for="co-expcode">快递公司代码 *</label>
|
||
<input id="co-expcode" name="workplatform_expcode" type="text" maxlength="64" required value="{{ old('workplatform_expcode', request('expcode')) }}" placeholder="如:SQZTKY" autocomplete="off">
|
||
<span class="co-help">与快递资料「快递公司代码」一致,入驻信息按这个代码对齐到快递资料。</span>
|
||
@error('workplatform_expcode')<span class="co-error">{{ $message }}</span>@enderror
|
||
</div>
|
||
<div class="co-field">
|
||
<label for="co-courier-name">系统快递名称 *</label>
|
||
<input id="co-courier-name" name="courier_name" type="text" maxlength="100" required value="{{ old('courier_name', request('courier_name')) }}" placeholder="如:新北中通、小件宿迁圆通">
|
||
<span class="co-help">我方规定的名称,需与报价名、发货数据里的快递名称完全一致;快递方看不到。</span>
|
||
@error('courier_name')<span class="co-error">{{ $message }}</span>@enderror
|
||
</div>
|
||
<div class="co-invite__row">
|
||
<div class="co-field">
|
||
<label for="co-invite-label">备注(快递方也能看到)</label>
|
||
<input id="co-invite-label" name="invite_label" type="text" maxlength="100" value="{{ old('invite_label') }}" placeholder="如:新北中通 · 张经理">
|
||
</div>
|
||
<div class="co-field">
|
||
<label for="co-valid-days">有效天数</label>
|
||
<select id="co-valid-days" name="valid_days">
|
||
@foreach ([7, 15, 30, 60, 90] as $days)
|
||
<option value="{{ $days }}" @selected((int) old('valid_days', 30) === $days)>{{ $days }} 天</option>
|
||
@endforeach
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<button type="submit" class="co-button co-button--primary"><span class="material-icons-outlined" aria-hidden="true">qr_code_2</span>生成二维码</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="co-card">
|
||
<div class="co-toolbar">
|
||
<nav class="co-tabs" aria-label="入驻状态">
|
||
@foreach ($groups as $key => $config)
|
||
<a class="co-tab {{ $group === $key ? 'is-active' : '' }}" href="{{ $groupUrl($key) }}" @if ($group === $key) aria-current="page" @endif>
|
||
{{ $config['label'] }}<span class="co-tab__count">{{ number_format($counts[$key] ?? 0) }}</span>
|
||
</a>
|
||
@endforeach
|
||
</nav>
|
||
<form class="co-search" method="GET" role="search">
|
||
<input type="hidden" name="group" value="{{ $group }}">
|
||
<input type="search" name="search" value="{{ $search }}" placeholder="单号 / 公司 / 网点 / 对接人 / 电话" aria-label="搜索入驻申请">
|
||
<button type="submit" class="co-button"><span class="material-icons-outlined" aria-hidden="true">search</span>查询</button>
|
||
</form>
|
||
</div>
|
||
<div class="co-table-wrap">
|
||
<table class="co-table" aria-label="入驻申请">
|
||
<thead>
|
||
<tr><th>入驻单号</th><th>状态</th><th>备注</th><th>类型</th><th>公司 / 姓名</th><th>网点 / 品牌</th><th>对接人</th><th>系统快递名称</th><th>附件</th><th>提交时间</th><th>最近更新</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
@forelse ($rows ?? [] as $row)
|
||
<tr>
|
||
<td class="is-code"><a href="{{ route('business.courier-onboardings.show', $row) }}">{{ $row->number }}</a></td>
|
||
<td><span class="co-badge {{ $badge($row->status) }}">{{ $row->linkExpired() && in_array($row->status, ['invited', 'returned'], true) ? '链接已过期' : $row->statusLabel() }}</span></td>
|
||
<td class="is-clamp" title="{{ $row->invite_label }}">{{ $row->invite_label ?: '—' }}</td>
|
||
<td>{{ $row->company_full_name ? $row->entityLabel() : '—' }}</td>
|
||
<td class="is-clamp" title="{{ $row->company_full_name }}">{{ $row->company_full_name ?: '—' }}</td>
|
||
<td>{{ $row->network_name ?: '—' }}{{ $row->carrier_brand ? ' · ' . $row->carrier_brand : '' }}</td>
|
||
<td>{{ $row->contact_name ?: '—' }}{{ $row->contact_phone ? ' ' . $row->contact_phone : '' }}</td>
|
||
<td>{{ $row->courier_name ?: '—' }}</td>
|
||
<td class="is-code">{{ $row->files_count }}</td>
|
||
<td class="is-code">{{ $row->submitted_at?->format('Y-m-d H:i') ?? '—' }}</td>
|
||
<td class="is-code">{{ $row->updated_at?->format('Y-m-d H:i') ?? '—' }}</td>
|
||
<td><a href="{{ route('business.courier-onboardings.show', $row) }}">{{ in_array($row->status, ['submitted', 'first_approved', 'approved'], true) ? '去处理' : '查看' }}</a></td>
|
||
</tr>
|
||
@empty
|
||
<tr><td colspan="12" class="co-empty">{{ $search !== '' ? '没有符合条件的入驻申请。' : '这里还没有入驻申请,先在上方生成一个二维码发给快递方。' }}</td></tr>
|
||
@endforelse
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
@if ($rows && $rows->total() > 0)
|
||
<div class="co-footer">
|
||
<span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页,共 {{ number_format($rows->total()) }} 条</span>
|
||
<nav aria-label="分页">
|
||
<a class="{{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->previousPageUrl() ?: '#' }}" aria-label="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||
<a class="{{ $rows->hasMorePages() ? '' : 'is-disabled' }}" href="{{ $rows->nextPageUrl() ?: '#' }}" aria-label="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||
</nav>
|
||
</div>
|
||
@endif
|
||
</div>
|
||
@endif
|
||
</section>
|