limeng b038909cb0 feat: 发货数据页接入(caiwu 直连聚水潭/奇门)
- 聚水潭奇门客户端与发货同步器骨架:QimenClient(签名+POST)、ShippingDataSynchronizer(erp.sctrade.query 列表 + erp.sradesku.query 明细 + upsert + modified 水位)
- 发货明细扁平展示表 jst_oms_shipping(对齐工作平台发货数据 47 列/顺序,用于快递运费核算);原始表 jst_sctrade/_items/_skus/_receiver + jst_sync_state
- 快递应付新增「发货数据」页:Pages.contentForShippingData + 视图 + business_navigation 导航项(slug=shipping-data)
- ShippingOrderRepository 读展示表;artisan jushuitan:sync-shipping + 每10分钟增量调度(JST_SYNC_ENABLED 默认关)
- 落库连接 jushuitan_local(本地容器,env 可覆盖);演示 Seeder(6 条家具发货样例);.env.jushuitan.example

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nvGZkg27YMEdhRy6EiUwf
2026-09-16 10:57:12 +08:00

135 lines
8.3 KiB
PHP

@php
$filters = $shippingFilters ?? [];
$sync = $shippingSync ?? ['code' => 'unavailable', 'last_succeeded_at' => null];
$statusLabels = ['succeeded' => '数据新鲜', 'running' => '同步中', 'pending' => '等待同步', 'failed' => '同步失败', 'unavailable' => '尚未同步'];
// 按工作平台发货数据界面顺序的 47 列: [列key, 表头, 对齐, 类型]
$columns = [
['express_no','快递单号','left','mono'],
['trade_no','交易单号','left','mono'],
['item_id','商品ID','left','mono'],
['barcode','条形码','left','text'],
['product_name','商品名称','left','text'],
['qty','数量','right','int'],
['receiver_state','省','left','text'],
['buyer_nick','买家昵称','left','text'],
['receiver_name','收货姓名','left','text'],
['shop_name','店铺名称','left','text'],
['express_name1','快递名称1','left','text'],
['warehouse_name','仓库','left','text'],
['ship_time','发货时间','left','text'],
['freight','运费','right','money'],
['online_weight','线上重量','right','num'],
['actual_weight','实称重量','right','num'],
['length_cm','长(cm)','right','num'],
['width_cm','宽(cm)','right','num'],
['height_cm','高(cm)','right','num'],
['volume_cm3','体积(cm³)','right','num'],
['receiver_city','市','left','text'],
['receiver_district','区','left','text'],
['supplier_name','供应商','left','text'],
['receiver_address','收货详细地址','left','text'],
['receiver_mobile','手机号码','left','text'],
['receiver_phone','座机号码','left','text'],
['express_name2','快递名称2','left','text'],
['platform','平台','left','text'],
['buyer_memo','买家留言','left','text'],
['seller_memo','卖家备注','left','text'],
['package_no','配货单号','left','mono'],
['amount','金额','right','money'],
['product_code','货号','left','text'],
['download_time','下载时间','left','text'],
['pay_time','付款时间','left','text'],
['freight_rate','费率','right','num'],
['freight_formula','公式','left','text'],
['weight_kg','重量(kg)','right','num'],
['settle_weight_kg','结算重量(kg)','right','num'],
['settle_volume_cm3','结算体积(cm³)','right','num'],
['recon_weight_kg','对账重量(kg)','right','num'],
['recon_volume_cm3','对账体积(cm³)','right','num'],
['finished_barcode','成品条码','left','text'],
['finished_name','成品名称','left','text'],
['finished_supplier','成品供应商','left','text'],
['settle_freight','结算运费','right','money'],
['front_no','换单前单号','left','mono'],
];
$fmt = function ($v, $type) {
if ($v === null || $v === '') return '';
return match ($type) {
'money' => number_format((float) $v, 2),
'num' => rtrim(rtrim(number_format((float) $v, 4, '.', ''), '0'), '.'),
'int' => (string) (int) $v,
default => (string) $v,
};
};
@endphp
<div class="shipping-data-page">
<style>
.shipping-data-page { color: #29251f; }
.shipping-data-page .sd-control { border: 1px solid #d9d0c2; border-radius: .375rem; padding: .5rem .7rem; font-size: .8125rem; background: #fff; outline: none; }
.shipping-data-page .sd-control:focus { border-color: #b18a4a; box-shadow: 0 0 0 2px rgba(177,138,74,.15); }
.shipping-data-page .sd-button { border-radius: .375rem; padding: .5rem 1rem; font-size: .8125rem; }
.shipping-data-page .sd-button--primary { background: #5d492c; color: #fff; }
.shipping-data-page .sd-button--secondary { border: 1px solid #d9d0c2; color: #655b4e; background: #fff; }
.shipping-data-page .sd-table th { background: #f5f1ea; color: #6f675c; font-weight: 600; position: sticky; top: 0; }
.shipping-data-page .sd-table td, .shipping-data-page .sd-table th { border-bottom: 1px solid #eee8de; border-right: 1px solid #f1ece3; padding: .5rem .6rem; white-space: nowrap; font-size: .78rem; }
.shipping-data-page .sd-table tbody tr:hover { background: #fcfaf7; }
.shipping-data-page .sd-mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: .72rem; color: #4b3a24; }
.shipping-data-page .sd-scroll { overflow-x: auto; max-width: 100%; }
</style>
<div class="flex flex-col gap-4 mb-5 sm:flex-row sm:items-start sm:justify-between">
<div>
<h1 class="text-xl font-semibold text-gray-800">{{ $page['title'] ?? '发货数据' }}</h1>
<p class="mt-1 text-xs text-gray-500">数据来源:聚水潭(聚石塔/奇门)· 销售出库单 · caiwu 直连拉取 · 用于快递运费核算</p>
</div>
<div class="flex items-center gap-2 text-xs">
<span class="rounded-full border border-[#d9d0c2] bg-white px-3 py-1.5 text-[#6f675c]">{{ $statusLabels[$sync['code'] ?? 'unavailable'] ?? '状态未知' }}</span>
@if (!empty($sync['last_succeeded_at'])) <span class="text-gray-500">最近同步 {{ $sync['last_succeeded_at'] }}</span> @endif
</div>
</div>
@if (!empty($shippingError))
<div class="mb-4 rounded border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800">{{ $shippingError }}</div>
@endif
<div class="mb-5 rounded-lg border border-[#e7e0d5] bg-white p-4 shadow-sm">
<form method="GET" class="flex flex-wrap items-end gap-3" aria-label="发货数据筛选">
<label class="flex min-w-[160px] flex-col gap-1 text-xs text-gray-500"><span>快递单号</span><input class="sd-control" type="text" name="express_no" value="{{ $filters['express_no'] ?? '' }}" placeholder="精确匹配"></label>
<label class="flex min-w-[160px] flex-col gap-1 text-xs text-gray-500"><span>交易单号</span><input class="sd-control" type="text" name="sourceno" value="{{ $filters['sourceno'] ?? '' }}" placeholder="精确匹配"></label>
<label class="flex min-w-[145px] flex-col gap-1 text-xs text-gray-500"><span>发货时间起</span><input class="sd-control" type="date" name="ship_from" value="{{ $filters['ship_from'] ?? '' }}"></label>
<label class="flex min-w-[145px] flex-col gap-1 text-xs text-gray-500"><span>发货时间止</span><input class="sd-control" type="date" name="ship_to" value="{{ $filters['ship_to'] ?? '' }}"></label>
<div class="flex gap-2"><button class="sd-button sd-button--primary" type="submit">查询</button><a class="sd-button sd-button--secondary" href="{{ url()->current() }}">重置</a></div>
</form>
</div>
<div class="overflow-hidden rounded-lg border border-[#e7e0d5] bg-white shadow-sm">
<div class="sd-scroll">
<table class="sd-table min-w-full">
<thead><tr>
@foreach ($columns as $c)
<th class="text-{{ $c[2] }}">{{ $c[1] }}</th>
@endforeach
</tr></thead>
<tbody>
@forelse ($shippingRows ?? [] as $row)
<tr>
@foreach ($columns as $c)
@php $val = $fmt($row[$c[0]] ?? null, $c[3]); @endphp
<td class="text-{{ $c[2] }} {{ $c[3] === 'mono' ? 'sd-mono' : '' }} {{ in_array($c[3], ['num','int','money']) ? 'tabular-nums' : '' }}">{{ $val === '' ? '-' : $val }}</td>
@endforeach
</tr>
@empty
<tr><td colspan="{{ count($columns) }}" class="px-4 py-16 text-center text-sm text-gray-500">暂无发货数据(填好聚水潭凭证并运行同步后显示;或先运行演示数据 Seeder 查看界面)</td></tr>
@endforelse
</tbody>
</table>
</div>
@if (($shippingRows ?? null) instanceof \Illuminate\Contracts\Pagination\LengthAwarePaginator && $shippingRows->hasPages())
<div class="flex items-center justify-between border-t border-[#eee8de] px-4 py-3 text-sm text-gray-500"><span>共 {{ $shippingRows->total() }} 条记录</span><div>{{ $shippingRows->onEachSide(1)->links() }}</div></div>
@endif
</div>
</div>