feat: 接入工作平台快递资料界面
This commit is contained in:
parent
2b723d88e6
commit
66d90edeef
@ -18,6 +18,7 @@ use Illuminate\Support\Collection;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException;
|
||||
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Repositories\InventoryRepository;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
@ -48,6 +49,7 @@ class Pages extends Controller
|
||||
private readonly NonJingxiRefundMirrorRepository $nonJingxiRefunds,
|
||||
private readonly ProductCatalogMirrorRepository $productCatalog,
|
||||
private readonly ProductPriceMirrorRepository $productPrices,
|
||||
private readonly CourierInformationMirrorRepository $courierInformation,
|
||||
private readonly RawBoardPayablesRepository $rawBoardPayables,
|
||||
private readonly DingPayableWorkflowMirrorRepository $dingPayableWorkflows,
|
||||
private readonly SupplierPayableReportRepository $supplierPayableReports,
|
||||
@ -288,6 +290,7 @@ class Pages extends Controller
|
||||
'zhongcai-reconciliation' => $this->contentForZhongcaiReconciliation(),
|
||||
'normal-products' => $this->contentForNormalProducts(),
|
||||
'product-prices' => $this->contentForProductPrices(),
|
||||
'courier-information' => $this->contentForCourierInformation(),
|
||||
'board-deductions' => $this->contentForBoardDeductions(),
|
||||
'supplier-payables-report' => $this->contentForSupplierPayablesReport(),
|
||||
'payable-cloud-warehouse-vehicle-fee' => $this->contentForDingPayableWorkflows(),
|
||||
@ -1166,6 +1169,66 @@ class Pages extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportCourierInformation(Request $request): StreamedResponse
|
||||
{
|
||||
abort_unless($this->courierInformation->available() && $this->courierInformation->ready(), 503);
|
||||
|
||||
$filters = $this->courierInformationFilters($request);
|
||||
|
||||
return response()->streamDownload(function () use ($filters): void {
|
||||
$csv = fopen('php://output', 'w');
|
||||
if ($csv === false) {
|
||||
throw new \RuntimeException('无法创建快递资料导出流。');
|
||||
}
|
||||
|
||||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
fputcsv($csv, [
|
||||
'ID', '快递公司代码', '快递公司名称', '外部编码', '杉橙快递代码', '运费打款方式',
|
||||
'是否启用', '可达服务', '是否快运', '自动寻号', '配货单号寻号', '数量拆单', '重量0寻号',
|
||||
'停运时间', '合同开始时间', '合同结束时间', '联系人', '联系电话', '自定义1', '自定义2', '自定义3',
|
||||
'体积0寻号', '手工单号', '解密', '最后修改',
|
||||
]);
|
||||
|
||||
$bool = static fn (mixed $value): string => in_array(strtolower((string) $value), ['1', 'true', 'yes', 'y'], true) || $value === true ? '是' : '否';
|
||||
$date = static fn (mixed $value): string => $value === null || $value === '' ? '' : substr((string) $value, 0, 10);
|
||||
|
||||
foreach ($this->courierInformation->exportRows($filters) as $row) {
|
||||
fputcsv($csv, [
|
||||
$row->Id,
|
||||
$this->csvText($row->expcode),
|
||||
$this->csvText($row->expname),
|
||||
$this->csvText($row->outcode),
|
||||
$this->csvText($row->scexpcode),
|
||||
$this->csvText($row->paymethod),
|
||||
$bool($row->status),
|
||||
$bool($row->addvalidsgn),
|
||||
$bool($row->kuaiyunsgn),
|
||||
$bool($row->autonosgn),
|
||||
$bool($row->packnosgn),
|
||||
$bool($row->skucountsplitsgn),
|
||||
$bool($row->weight0sgn),
|
||||
$date($row->stopdate),
|
||||
$date($row->tradebtime),
|
||||
$date($row->tradeetime),
|
||||
$this->csvText($row->linker),
|
||||
$this->csvText($row->linkmobile),
|
||||
$this->csvText($row->undefined1),
|
||||
$this->csvText($row->undefined2),
|
||||
$this->csvText($row->undefined3),
|
||||
$bool($row->volume0sgn),
|
||||
$bool($row->handnosgn),
|
||||
$bool($row->decryptsgn),
|
||||
$date($row->modified),
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($csv);
|
||||
}, '快递资料_' . now()->format('Ymd_His') . '.csv', [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function normalProductFilters(Request $request): array
|
||||
{
|
||||
@ -1236,6 +1299,129 @@ class Pages extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
protected function contentForCourierInformation(): array
|
||||
{
|
||||
$filters = $this->courierInformationFilters(request());
|
||||
$tab = (string) request()->query('courier_tab', 'express');
|
||||
$tab = in_array($tab, ['express', 'networks', 'accounts', 'logs'], true) ? $tab : 'express';
|
||||
$page = max((int) request()->query('page', 1), 1);
|
||||
$limit = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||||
$rows = new LengthAwarePaginator([], 0, $limit, $page, [
|
||||
'path' => request()->url(),
|
||||
'query' => request()->except('page'),
|
||||
]);
|
||||
$error = null;
|
||||
$pending = false;
|
||||
$state = null;
|
||||
$syncStatus = ['code' => 'unavailable', 'lag_minutes' => null];
|
||||
$relatedAvailability = $this->courierInformation->relatedAvailability();
|
||||
|
||||
try {
|
||||
if ($tab === 'express') {
|
||||
if (! $this->courierInformation->available()) {
|
||||
$error = '共享业务镜像尚未建立,快递资料暂时无法读取。';
|
||||
} else {
|
||||
$state = $this->courierInformation->state();
|
||||
$syncStatus = $this->courierInformation->syncStatus($state);
|
||||
if (! $this->courierInformation->ready()) {
|
||||
$pending = true;
|
||||
$error = '快递资料正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||||
} else {
|
||||
$result = $this->courierInformation->paginate($filters, $page, $limit);
|
||||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||||
}
|
||||
}
|
||||
} elseif ($tab === 'networks') {
|
||||
if (! ($relatedAvailability['networks'] ?? false)) {
|
||||
$error = '快递网点镜像尚未建立,请先完成 wp_express_expNet 的首次同步。';
|
||||
} elseif (! ($relatedAvailability['network_ready'] ?? false)) {
|
||||
$pending = true;
|
||||
$error = '快递网点正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||||
} else {
|
||||
$result = $this->courierInformation->paginateNetworks($filters['expcode'] ?: null, $page, $limit);
|
||||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||||
}
|
||||
} elseif ($tab === 'accounts') {
|
||||
if (! ($relatedAvailability['accounts'] ?? false)) {
|
||||
$error = '打款账户镜像尚未建立,请先完成 wp_express_account 的首次同步。';
|
||||
} elseif (! ($relatedAvailability['account_ready'] ?? false)) {
|
||||
$pending = true;
|
||||
$error = '打款账户正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||||
} else {
|
||||
$result = $this->courierInformation->paginateAccounts($filters['expcode'] ?: null, $page, $limit);
|
||||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||||
}
|
||||
} else {
|
||||
if (! ($relatedAvailability['logs'] ?? false)) {
|
||||
$error = '操作日志镜像尚未建立,请先完成 wp_express_extension 的首次同步。';
|
||||
} elseif (! ($relatedAvailability['log_ready'] ?? false)) {
|
||||
$pending = true;
|
||||
$error = '操作日志正在执行首次全量同步,完成前不会把未同步数据误判为无数据。';
|
||||
} else {
|
||||
$result = $this->courierInformation->paginateLogs($filters['expcode'] ?: null, $page, $limit);
|
||||
$rows = $this->courierPaginator($result['data'] ?? [], $result['total'] ?? 0, $limit, $page);
|
||||
}
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$error = '快递资料共享镜像暂时无法读取,请稍后重试。';
|
||||
}
|
||||
|
||||
return [
|
||||
'view' => 'business.pages.courier-payables.courier-information',
|
||||
'courierTab' => $tab,
|
||||
'courierFilters' => $filters,
|
||||
'courierRows' => $rows,
|
||||
'courierState' => $state,
|
||||
'courierSyncStatus' => $syncStatus,
|
||||
'courierError' => $error,
|
||||
'courierPending' => $pending,
|
||||
'courierRelatedAvailability' => $relatedAvailability,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function courierInformationFilters(Request $request): array
|
||||
{
|
||||
$filters = [
|
||||
'expcode' => mb_substr(trim((string) $request->query('expcode', '')), 0, 64),
|
||||
'expname' => mb_substr(trim((string) $request->query('expname', '')), 0, 100),
|
||||
'undefined1' => mb_substr(trim((string) $request->query('undefined1', '')), 0, 100),
|
||||
'undefined2' => mb_substr(trim((string) $request->query('undefined2', '')), 0, 100),
|
||||
'undefined3' => mb_substr(trim((string) $request->query('undefined3', '')), 0, 100),
|
||||
'tradebtime_from' => $this->dateFilter($request->query('tradebtime_from')),
|
||||
'tradebtime_to' => $this->dateFilter($request->query('tradebtime_to')),
|
||||
'status' => in_array((string) $request->query('status', ''), ['0', '1'], true)
|
||||
? (string) $request->query('status')
|
||||
: '',
|
||||
];
|
||||
|
||||
if ($filters['tradebtime_from'] !== ''
|
||||
&& $filters['tradebtime_to'] !== ''
|
||||
&& $filters['tradebtime_from'] > $filters['tradebtime_to']) {
|
||||
[$filters['tradebtime_from'], $filters['tradebtime_to']] = [
|
||||
$filters['tradebtime_to'],
|
||||
$filters['tradebtime_from'],
|
||||
];
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function courierPaginator(array $rows, int $total, int $limit, int $page): LengthAwarePaginator
|
||||
{
|
||||
return new LengthAwarePaginator(
|
||||
Collection::make($rows),
|
||||
$total,
|
||||
$limit,
|
||||
$page,
|
||||
[
|
||||
'path' => request()->url(),
|
||||
'query' => request()->except('page'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
protected function contentForProductPrices(): array
|
||||
{
|
||||
$filters = [
|
||||
|
||||
@ -418,6 +418,7 @@ return [
|
||||
'icon' => 'local_shipping',
|
||||
'pages' => [
|
||||
['slug' => 'quote-management', 'title' => '报价管理'],
|
||||
['slug' => 'courier-information', 'title' => '快递资料'],
|
||||
['slug' => 'agreement-signing', 'title' => '协议签署'],
|
||||
['slug' => 'freight-reconciliation', 'title' => '运费对账'],
|
||||
['slug' => 'bookkeeping-details', 'title' => '记账明细'],
|
||||
|
||||
@ -55,6 +55,10 @@ return [
|
||||
'reconcile_window_hours' => (int) env('PRODUCT_PURCHASE_PRICE_MIRROR_RECONCILE_WINDOW_HOURS', 6),
|
||||
],
|
||||
|
||||
'courier_information' => [
|
||||
'stale_after_minutes' => (int) env('COURIER_INFORMATION_MIRROR_STALE_MINUTES', 60),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 上游原表镜像目录
|
||||
@ -174,5 +178,53 @@ return [
|
||||
'jiansun.dbo.fys_supplierGoods_sttlement_purchase' => ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'fys_supplierGoods_sttlement_purchase', 'target' => 'js_supplier_goods_purchase', 'keys' => ['Id'], 'watermark' => 'createTime'],
|
||||
'jiansun.dbo.fys_supplierGoods_sttlement_sale' => ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'fys_supplierGoods_sttlement_sale', 'target' => 'js_supplier_goods_sale', 'keys' => ['Id'], 'watermark' => 'createTime'],
|
||||
'jiansun.dbo.pm_purchaseIn' => ['source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'pm_purchaseIn', 'target' => 'js_pm_purchase_in', 'keys' => ['id'], 'watermark' => 'updateTime'],
|
||||
|
||||
// 工作平台 2.0:快递资料及其网点、打款账户。页面只读取共享镜像,
|
||||
// 首次使用前需由刷新节点显式执行对应数据集的 --full 同步。
|
||||
'jiansun.dbo.wp_express' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'wp_express',
|
||||
'target' => 'js_wp_express',
|
||||
'keys' => ['Id'],
|
||||
'watermark' => 'modified',
|
||||
'indexes' => [
|
||||
['expcode'],
|
||||
['status', 'Id'],
|
||||
],
|
||||
],
|
||||
'jiansun.dbo.wp_express_expNet' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'wp_express_expNet',
|
||||
'target' => 'js_wp_express_exp_net',
|
||||
'keys' => ['id'],
|
||||
'indexes' => [
|
||||
['expid', 'id'],
|
||||
['status', 'id'],
|
||||
],
|
||||
],
|
||||
'jiansun.dbo.wp_express_account' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'wp_express_account',
|
||||
'target' => 'js_wp_express_account',
|
||||
'keys' => ['Id'],
|
||||
'indexes' => [
|
||||
['expcode', 'Id'],
|
||||
['accountType', 'Id'],
|
||||
],
|
||||
],
|
||||
'jiansun.dbo.wp_express_extension' => [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => 'wp_express_extension',
|
||||
'target' => 'js_wp_express_extension',
|
||||
'keys' => ['Id'],
|
||||
'watermark' => 'modifyTime',
|
||||
'indexes' => [
|
||||
['expcode', 'Id'],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## 结论
|
||||
|
||||
项目不是只使用 `rpt.Bulk_Price_Limit` 一张表。当前主线代码直接读取两套上游中的约 40 个表或视图,覆盖组织资料、订单、退款、营销账户、供应商应付、板材、封边条和库存。
|
||||
项目不是只使用 `rpt.Bulk_Price_Limit` 一张表。当前主线代码直接读取两套上游中的约 43 个表或视图,覆盖组织资料、订单、退款、营销账户、供应商应付、快递资料、板材、封边条和库存。
|
||||
|
||||
`caiwu_business_mirror` 的目标是把项目实际使用的上游原表数据保存为共享业务镜像。网页只读镜像,后台刷新节点在低压、可断点的条件下完成首次全量和后续增量;不得再按页码或筛选条件临时生成不完整的数据副本。
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
| 已完成 | `operations_replica/rpt.jst_sku` 已进入 `ops_jst_sku`,普通商品资料页面固定读取 `sku_type = normal` 的共享镜像,并支持稳定分页和业务筛选。 |
|
||||
| 已接入校准 | 聚水潭“普通商品资料”完整 XLSX 导出通过 `product-catalog:import-authoritative-export` 流式导入;导出中不存在的经营分析副本历史行写入 `product_catalog_exclusions`,页面与导出统一排除,但不删除 `ops_jst_sku` 原始镜像行。 |
|
||||
| 本次接入 | `jiansun/dbo.jst_purchase + dbo.jst_purchaseItem` 通过固定联表查询进入 `product_purchase_price_mirror_rows`;普通商品资料只读取 `Finished` 且带“1688一键代发”标签的最近采购单价,并与原商品采购价分别标注来源。 |
|
||||
| 本次接入 | 工作平台快递资料页面读取 `wp_express` 主表,并预留网点 `wp_express_expNet`、打款账户 `wp_express_account` 标签;三张表均登记为独立镜像数据集,首次同步完成前页面明确显示同步状态。 |
|
||||
| 本次建立 | `config/upstream_mirrors.php` 中登记全部当前代码引用的原表;通用表级同步框架负责建表、稳定游标、断点续传、增量重叠和全量校准。 |
|
||||
| 已开始逐域切换 | 公司、店铺、商标页面在对应镜像完成首次全量后自动改读完整镜像表;首次全量前保留旧快照作为兼容回滚。其他业务域仍需逐项切换,不得宣称全项目已经完成。 |
|
||||
|
||||
@ -42,6 +43,7 @@
|
||||
| 封边条 | `dbo.pm_edgeBandData`、`dbo.pm_edgeBandApplyDetail`、`dbo.pm_edgeBandProfitInfo`、`dbo.finance_edgeBandDeductio`、`dbo.pm_averageUnitPrice` |
|
||||
| 库存与货款对账 | `dbo.pm_boardInventoryDetail_snapshot`、`dbo.fys_supplierGoods_sttlement_purchase`、`dbo.fys_supplierGoods_sttlement_sale`、`dbo.pm_purchaseIn` |
|
||||
| 采购单价 | `dbo.jst_purchase`、`dbo.jst_purchaseItem`(受控字段联表镜像,不复制其他采购业务字段) |
|
||||
| 快递资料 | `dbo.wp_express`、`dbo.wp_express_expNet`、`dbo.wp_express_account`、`dbo.wp_express_extension` |
|
||||
|
||||
其中 `fys_supplierGoods_sttlement_sale` 当前约 280 万行,是首次全量同步的最大对象。必须在低峰期小批量、可断点执行,禁止把全量同步塞进发布流程。
|
||||
|
||||
@ -60,8 +62,9 @@
|
||||
|
||||
1. 公司、店铺、商标等小型维表。
|
||||
2. 营销账户五张单表账单。
|
||||
3. 供应商应付与货款对账。
|
||||
4. 订单、退款和推广费用。
|
||||
5. 板材、封边条和库存的跨表查询。
|
||||
3. 快递资料、网点和打款账户。
|
||||
4. 供应商应付与货款对账。
|
||||
5. 订单、退款和推广费用。
|
||||
6. 板材、封边条和库存的跨表查询。
|
||||
|
||||
每个阶段都必须先完成首次全量和数量核对,再将对应 Repository 切到镜像;任一阶段失败时保留上一版镜像和旧只读链路用于回滚。
|
||||
|
||||
@ -0,0 +1,319 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 工作平台 2.0 的快递资料只读镜像。
|
||||
*
|
||||
* 这里不直接连接 JianSun;所有查询都落在 caiwu_business_mirror 中,
|
||||
* 由 business-mirror:sync-tables 负责生成和刷新镜像表。
|
||||
*/
|
||||
class CourierInformationMirrorRepository
|
||||
{
|
||||
public const DATASET_KEY = 'jiansun.dbo.wp_express';
|
||||
|
||||
public const NETWORK_DATASET_KEY = 'jiansun.dbo.wp_express_expNet';
|
||||
|
||||
public const ACCOUNT_DATASET_KEY = 'jiansun.dbo.wp_express_account';
|
||||
|
||||
public const LOG_DATASET_KEY = 'jiansun.dbo.wp_express_extension';
|
||||
|
||||
private readonly ConnectionInterface $connection;
|
||||
|
||||
public function __construct(
|
||||
private readonly BusinessMirrorDatasetRegistry $datasets,
|
||||
private readonly GenericBusinessMirrorRepository $mirror,
|
||||
) {
|
||||
$this->connection = $mirror->connection();
|
||||
}
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
$schema = Schema::connection($this->connection->getName());
|
||||
|
||||
return $schema->hasTable('business_mirror_table_states')
|
||||
&& $schema->hasTable($this->dataset()->targetTable());
|
||||
}
|
||||
|
||||
public function ready(): bool
|
||||
{
|
||||
return $this->mirror->ready($this->dataset());
|
||||
}
|
||||
|
||||
/** @return array<string,mixed>|null */
|
||||
public function state(): ?array
|
||||
{
|
||||
return $this->mirror->state($this->dataset());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed>|null $state
|
||||
* @return array{code:string,lag_minutes:?int}
|
||||
*/
|
||||
public function syncStatus(?array $state = null, ?CarbonInterface $at = null): array
|
||||
{
|
||||
$state ??= $this->state();
|
||||
if (! is_array($state)) {
|
||||
return ['code' => 'unavailable', 'lag_minutes' => null];
|
||||
}
|
||||
|
||||
$lastSucceeded = $this->timestamp($state['last_succeeded_at'] ?? null);
|
||||
$lagMinutes = $lastSucceeded === null
|
||||
? null
|
||||
: (int) ceil(max(0, ($at ?? now())->getTimestamp() - $lastSucceeded) / 60);
|
||||
$status = (string) ($state['status'] ?? 'pending');
|
||||
|
||||
if (($state['initial_completed_at'] ?? null) === null) {
|
||||
$code = $status === 'failed'
|
||||
? 'initial_failed'
|
||||
: ($status === 'running' ? 'initializing' : 'pending');
|
||||
} elseif ($status === 'failed') {
|
||||
$code = 'failed';
|
||||
} elseif ($status === 'running') {
|
||||
$code = ($state['run_mode'] ?? null) === 'full' ? 'reconciling' : 'syncing';
|
||||
} elseif ($lagMinutes === null
|
||||
|| $lagMinutes > max((int) config('upstream_mirrors.courier_information.stale_after_minutes', 60), 1)) {
|
||||
$code = 'delayed';
|
||||
} else {
|
||||
$code = 'healthy';
|
||||
}
|
||||
|
||||
return ['code' => $code, 'lag_minutes' => $lagMinutes];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $filters
|
||||
* @return array{data:list<object>,total:int}
|
||||
*/
|
||||
public function paginate(array $filters, int $page, int $perPage): array
|
||||
{
|
||||
if (! $this->ready()) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
}
|
||||
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$query = $this->filteredExpressQuery($filters);
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query->orderBy('Id')
|
||||
->forPage($page, $perPage)
|
||||
->get()
|
||||
->all();
|
||||
|
||||
return ['data' => $rows, 'total' => $total];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string> $filters
|
||||
* @return \Generator<int,object>
|
||||
*/
|
||||
public function exportRows(array $filters, int $batchSize = 500): \Generator
|
||||
{
|
||||
if (! $this->ready()) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
}
|
||||
|
||||
$batchSize = min(max($batchSize, 1), 5000);
|
||||
$lastId = null;
|
||||
|
||||
while (true) {
|
||||
$query = $this->filteredExpressQuery($filters);
|
||||
if ($lastId !== null) {
|
||||
$query->where('Id', '>', $lastId);
|
||||
}
|
||||
|
||||
$rows = $query->orderBy('Id')->limit($batchSize)->get()->all();
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
yield $row;
|
||||
}
|
||||
|
||||
$lastId = (int) $rows[array_key_last($rows)]->Id;
|
||||
if (count($rows) < $batchSize) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网点标签数据。快递编码为空时返回全部网点;传入编码时只返回对应网点。
|
||||
* @return array{data:list<object>,total:int}
|
||||
*/
|
||||
public function paginateNetworks(?string $expcode, int $page, int $perPage): array
|
||||
{
|
||||
$dataset = $this->datasets->get(self::NETWORK_DATASET_KEY);
|
||||
if (! $this->mirror->ready($dataset)) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
}
|
||||
|
||||
$query = $this->connection->table($dataset->targetTable())->select([
|
||||
'id', 'expid', 'expcode', 'netcode', 'netno', 'netname', 'isbill',
|
||||
'province', 'city', 'area', 'town', 'address', 'linker', 'linkermobile',
|
||||
'status', 'apitype', 'apipltmname', 'created', 'expnum',
|
||||
]);
|
||||
if ($expcode !== null && $expcode !== '') {
|
||||
$query->where('expcode', $expcode);
|
||||
}
|
||||
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$total = (clone $query)->count();
|
||||
|
||||
return [
|
||||
'data' => $query->orderByDesc('id')->forPage($page, $perPage)->get()->all(),
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 打款账户标签数据。
|
||||
* @return array{data:list<object>,total:int}
|
||||
*/
|
||||
public function paginateAccounts(?string $expcode, int $page, int $perPage): array
|
||||
{
|
||||
$dataset = $this->datasets->get(self::ACCOUNT_DATASET_KEY);
|
||||
if (! $this->mirror->ready($dataset)) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
}
|
||||
|
||||
$query = $this->connection->table($dataset->targetTable())->select([
|
||||
'Id', 'expcode', 'expname', 'accountType', 'account', 'accountName',
|
||||
'accountkhh', 'createTime', 'invoice', 'erpId', 'erpAccountId',
|
||||
]);
|
||||
if ($expcode !== null && $expcode !== '') {
|
||||
$query->where('expcode', $expcode);
|
||||
}
|
||||
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$total = (clone $query)->count();
|
||||
|
||||
return [
|
||||
'data' => $query->orderByDesc('Id')->forPage($page, $perPage)->get()->all(),
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 快递资料操作日志。
|
||||
* @return array{data:list<object>,total:int}
|
||||
*/
|
||||
public function paginateLogs(?string $expcode, int $page, int $perPage): array
|
||||
{
|
||||
$dataset = $this->datasets->get(self::LOG_DATASET_KEY);
|
||||
if (! $this->mirror->ready($dataset)) {
|
||||
throw new RuntimeException('business_mirror_dataset_not_ready');
|
||||
}
|
||||
|
||||
$query = $this->connection->table($dataset->targetTable())->select([
|
||||
'Id', 'expcode', 'doTime', 'createTime', 'modifyTime',
|
||||
]);
|
||||
if ($expcode !== null && $expcode !== '') {
|
||||
$query->where('expcode', $expcode);
|
||||
}
|
||||
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$total = (clone $query)->count();
|
||||
|
||||
return [
|
||||
'data' => $query->orderByDesc('Id')->forPage($page, $perPage)->get()->all(),
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{main:bool,networks:bool,accounts:bool,logs:bool,network_ready:bool,account_ready:bool,log_ready:bool} */
|
||||
public function relatedAvailability(): array
|
||||
{
|
||||
try {
|
||||
$schema = Schema::connection($this->connection->getName());
|
||||
$networkDataset = $this->datasets->get(self::NETWORK_DATASET_KEY);
|
||||
$accountDataset = $this->datasets->get(self::ACCOUNT_DATASET_KEY);
|
||||
$logDataset = $this->datasets->get(self::LOG_DATASET_KEY);
|
||||
|
||||
return [
|
||||
'main' => $schema->hasTable($this->dataset()->targetTable()),
|
||||
'networks' => $schema->hasTable($networkDataset->targetTable()),
|
||||
'accounts' => $schema->hasTable($accountDataset->targetTable()),
|
||||
'logs' => $schema->hasTable($logDataset->targetTable()),
|
||||
'network_ready' => $this->mirror->ready($networkDataset),
|
||||
'account_ready' => $this->mirror->ready($accountDataset),
|
||||
'log_ready' => $this->mirror->ready($logDataset),
|
||||
];
|
||||
} catch (Throwable) {
|
||||
return ['main' => false, 'networks' => false, 'accounts' => false, 'logs' => false, 'network_ready' => false, 'account_ready' => false, 'log_ready' => false];
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,string> $filters */
|
||||
private function filteredExpressQuery(array $filters): Builder
|
||||
{
|
||||
$query = $this->connection->table($this->dataset()->targetTable())->select([
|
||||
'Id', 'expcode', 'expname', 'outcode', 'scexpcode', 'status', 'paymethod',
|
||||
'addvalidsgn', 'kuaiyunsgn', 'autonosgn', 'packnosgn', 'skucountsplitsgn',
|
||||
'weight0sgn', 'stopdate', 'tradebtime', 'tradeetime', 'signcom', 'signmode',
|
||||
'linker', 'linkmobile', 'paytype', 'undefined1', 'undefined2', 'undefined3',
|
||||
'volume0sgn', 'handnosgn', 'decryptsgn', 'pj1sgn', 'undefinedtype',
|
||||
'undefined4', 'undefined5', 'dzqzjbr', 'guid', 'is_delete', 'exp_properties',
|
||||
'modified',
|
||||
]);
|
||||
|
||||
if (($filters['expcode'] ?? '') !== '') {
|
||||
$query->where('expcode', $filters['expcode']);
|
||||
}
|
||||
if (($filters['expname'] ?? '') !== '') {
|
||||
$query->whereRaw("expname LIKE ? ESCAPE '!'", [$this->contains($filters['expname'])]);
|
||||
}
|
||||
foreach (['undefined1', 'undefined2', 'undefined3'] as $column) {
|
||||
if (($filters[$column] ?? '') !== '') {
|
||||
$query->whereRaw($column . " LIKE ? ESCAPE '!'", [$this->contains($filters[$column])]);
|
||||
}
|
||||
}
|
||||
if (($filters['tradebtime_from'] ?? '') !== '') {
|
||||
$query->whereDate('tradebtime', '>=', $filters['tradebtime_from']);
|
||||
}
|
||||
if (($filters['tradebtime_to'] ?? '') !== '') {
|
||||
$query->whereDate('tradebtime', '<=', $filters['tradebtime_to']);
|
||||
}
|
||||
if (($filters['status'] ?? '') !== '') {
|
||||
$query->where('status', (int) $filters['status']);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function dataset(): \Modules\EcommerceFinance\Contracts\BusinessMirrorDataset
|
||||
{
|
||||
return $this->datasets->get(self::DATASET_KEY);
|
||||
}
|
||||
|
||||
private function contains(string $value): string
|
||||
{
|
||||
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], trim($value)) . '%';
|
||||
}
|
||||
|
||||
private function timestamp(mixed $value): ?int
|
||||
{
|
||||
if (! is_string($value) || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($value)->getTimestamp();
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,495 @@
|
||||
@php
|
||||
$tab = $courierTab ?? 'express';
|
||||
$filters = $courierFilters ?? [];
|
||||
$rows = $courierRows ?? null;
|
||||
$sync = $courierSyncStatus ?? ['code' => 'unavailable', 'lag_minutes' => null];
|
||||
$state = $courierState ?? null;
|
||||
$statusLabels = [
|
||||
'healthy' => '同步正常',
|
||||
'delayed' => '同步延迟',
|
||||
'syncing' => '增量同步中',
|
||||
'reconciling' => '全量校准中',
|
||||
'initializing' => '首次同步中',
|
||||
'initial_failed' => '首次同步失败',
|
||||
'failed' => '同步失败',
|
||||
'pending' => '等待首次同步',
|
||||
'unavailable' => '镜像未建立',
|
||||
];
|
||||
$statusClass = in_array($sync['code'] ?? '', ['healthy'], true)
|
||||
? 'is-success'
|
||||
: (in_array($sync['code'] ?? '', ['failed', 'initial_failed', 'unavailable'], true) ? 'is-danger' : 'is-warning');
|
||||
$date = static function ($value): string {
|
||||
if ($value === null || $value === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
try {
|
||||
return \Illuminate\Support\Carbon::parse((string) $value)->format('Y-m-d');
|
||||
} catch (\Throwable) {
|
||||
return (string) $value;
|
||||
}
|
||||
};
|
||||
$datetime = static function ($value): string {
|
||||
if ($value === null || $value === '') {
|
||||
return '-';
|
||||
}
|
||||
|
||||
try {
|
||||
return \Illuminate\Support\Carbon::parse((string) $value)->format('Y-m-d H:i');
|
||||
} catch (\Throwable) {
|
||||
return (string) $value;
|
||||
}
|
||||
};
|
||||
$bool = static function ($value): string {
|
||||
return in_array(strtolower((string) $value), ['1', 'true', 'yes', 'y'], true) || $value === true ? '是' : '否';
|
||||
};
|
||||
$tabUrl = static function (string $targetTab) use ($filters): string {
|
||||
return route('business.pages.show', array_merge(
|
||||
['section' => 'courier-payables', 'page' => 'courier-information', 'courier_tab' => $targetTab],
|
||||
array_filter($filters, static fn ($value) => $value !== '' && $value !== null),
|
||||
));
|
||||
};
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
.courier-information-page {
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font-family: JetBrains Mono, Microsoft YaHei, PingFang SC, monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.courier-information-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.courier-information-status__main,
|
||||
.courier-information-status__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.courier-information-status__meta { color: var(--vip-content-muted, #595959); font-size: 0.75rem; }
|
||||
|
||||
.courier-information-status__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--vip-warning, #935f07);
|
||||
}
|
||||
|
||||
.courier-information-status__dot.is-success { background: var(--vip-success, #53793e); }
|
||||
.courier-information-status__dot.is-danger { background: var(--vip-danger, #b80000); }
|
||||
|
||||
.courier-information-tabs {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
min-height: 48px;
|
||||
margin-bottom: 16px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.courier-information-tab {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 48px;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.8125rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.courier-information-tab:hover,
|
||||
.courier-information-tab.is-active { color: var(--vip-content-accent-dark, #856632); }
|
||||
|
||||
.courier-information-tab.is-active::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
background: var(--vip-content-accent, #b9954f);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.courier-information-filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.courier-information-field { min-width: 0; }
|
||||
.courier-information-field label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.courier-information-field input,
|
||||
.courier-information-field select {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.courier-information-field input:focus,
|
||||
.courier-information-field select:focus {
|
||||
border-color: var(--vip-content-accent-dark, #856632);
|
||||
outline: 3px solid rgba(133, 102, 50, 0.2);
|
||||
}
|
||||
|
||||
.courier-information-actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.courier-information-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.courier-information-button:hover { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-soft, #f7f0de); }
|
||||
.courier-information-button.is-primary { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-dark, #856632); color: #fff; }
|
||||
|
||||
.courier-information-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid rgba(147, 95, 7, 0.35);
|
||||
border-radius: 8px;
|
||||
background: rgba(147, 95, 7, 0.06);
|
||||
color: var(--vip-warning, #935f07);
|
||||
}
|
||||
|
||||
.courier-information-notice.is-error {
|
||||
border-color: rgba(184, 0, 0, 0.3);
|
||||
background: rgba(184, 0, 0, 0.05);
|
||||
color: var(--vip-danger, #b80000);
|
||||
}
|
||||
|
||||
.courier-information-table-wrap {
|
||||
overflow-x: auto;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.courier-information-table {
|
||||
width: 100%;
|
||||
min-width: 2260px;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.78125rem;
|
||||
}
|
||||
|
||||
.courier-information-table th {
|
||||
padding: 9px 10px;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
color: #4f4a43;
|
||||
font-weight: 600;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.courier-information-table td {
|
||||
max-width: 240px;
|
||||
padding: 9px 10px;
|
||||
border-bottom: 1px solid #eeeae3;
|
||||
color: var(--vip-content-ink, #171717);
|
||||
line-height: 1.45;
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.courier-information-table tbody tr:hover { background: #fcfaf5; }
|
||||
.courier-information-table .is-code { font-variant-numeric: tabular-nums; }
|
||||
.courier-information-table .is-clamped { overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.courier-information-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 22px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
}
|
||||
|
||||
.courier-information-badge.is-enabled { border-color: rgba(83, 121, 62, .3); background: rgba(83, 121, 62, .08); color: var(--vip-success, #53793e); }
|
||||
.courier-information-badge.is-disabled { border-color: rgba(184, 0, 0, .25); background: rgba(184, 0, 0, .05); color: var(--vip-danger, #b80000); }
|
||||
|
||||
.courier-information-empty { min-height: 160px; padding: 42px 16px !important; color: var(--vip-content-muted, #595959) !important; text-align: center; }
|
||||
|
||||
.courier-information-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.courier-information-pagination { display: flex; align-items: center; gap: 6px; }
|
||||
.courier-information-page-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 32px;
|
||||
height: 30px;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 6px;
|
||||
color: var(--vip-content-ink, #171717);
|
||||
}
|
||||
.courier-information-page-link:hover { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-soft, #f7f0de); }
|
||||
.courier-information-page-link.is-disabled { opacity: .4; pointer-events: none; }
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
.courier-information-filters { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.courier-information-filters { grid-template-columns: 1fr; }
|
||||
.courier-information-status { align-items: flex-start; flex-direction: column; }
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="courier-information-page">
|
||||
<div class="courier-information-status" role="status">
|
||||
<div class="courier-information-status__main">
|
||||
<span class="courier-information-status__dot {{ $statusClass }}" aria-hidden="true"></span>
|
||||
<span>{{ $statusLabels[$sync['code'] ?? 'unavailable'] ?? '镜像状态未知' }}</span>
|
||||
</div>
|
||||
<div class="courier-information-status__meta">
|
||||
@if (!empty($state['last_succeeded_at']))
|
||||
<span>最近同步 {{ $datetime($state['last_succeeded_at']) }}</span>
|
||||
@endif
|
||||
@if (($sync['lag_minutes'] ?? null) !== null)
|
||||
<span>数据延迟 {{ number_format((int) $sync['lag_minutes']) }} 分钟</span>
|
||||
@endif
|
||||
@if (isset($state['mirror_row_count']))
|
||||
<span>主表 {{ number_format((int) $state['mirror_row_count']) }} 条</span>
|
||||
@endif
|
||||
@if ($rows && !$courierError)
|
||||
<span>当前结果 {{ number_format($rows->total()) }} 条</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="courier-information-tabs" aria-label="快递资料分组">
|
||||
@foreach ([
|
||||
'express' => ['icon' => 'local_shipping', 'label' => '快递资料'],
|
||||
'networks' => ['icon' => 'storefront', 'label' => '网点'],
|
||||
'accounts' => ['icon' => 'account_balance', 'label' => '打款信息'],
|
||||
'logs' => ['icon' => 'history', 'label' => '操作日志'],
|
||||
] as $key => $item)
|
||||
<a class="courier-information-tab {{ $tab === $key ? 'is-active' : '' }}" href="{{ $tabUrl($key) }}" @if ($tab === $key) aria-current="page" @endif>
|
||||
<span class="material-icons-outlined" aria-hidden="true">{{ $item['icon'] }}</span>
|
||||
<span>{{ $item['label'] }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</nav>
|
||||
|
||||
@if ($tab === 'express')
|
||||
<form method="GET" class="courier-information-filters" aria-label="快递资料筛选">
|
||||
<input type="hidden" name="courier_tab" value="express">
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-expcode">快递代码</label>
|
||||
<input id="courier-expcode" name="expcode" type="text" value="{{ $filters['expcode'] ?? '' }}" placeholder="精确匹配">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-expname">快递名称</label>
|
||||
<input id="courier-expname" name="expname" type="text" value="{{ $filters['expname'] ?? '' }}" placeholder="名称关键词">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-start">合同开始时间(起)</label>
|
||||
<input id="courier-start" name="tradebtime_from" type="date" value="{{ $filters['tradebtime_from'] ?? '' }}">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-end">合同开始时间(止)</label>
|
||||
<input id="courier-end" name="tradebtime_to" type="date" value="{{ $filters['tradebtime_to'] ?? '' }}">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-custom-1">自定义1</label>
|
||||
<input id="courier-custom-1" name="undefined1" type="text" value="{{ $filters['undefined1'] ?? '' }}">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-custom-2">自定义2</label>
|
||||
<input id="courier-custom-2" name="undefined2" type="text" value="{{ $filters['undefined2'] ?? '' }}">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-custom-3">自定义3</label>
|
||||
<input id="courier-custom-3" name="undefined3" type="text" value="{{ $filters['undefined3'] ?? '' }}">
|
||||
</div>
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-status">是否启用</label>
|
||||
<select id="courier-status" name="status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="1" @selected(($filters['status'] ?? '') === '1')>已启用</option>
|
||||
<option value="0" @selected(($filters['status'] ?? '') === '0')>已停用</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="courier-information-actions">
|
||||
<button type="submit" class="courier-information-button is-primary"><span class="material-icons-outlined" aria-hidden="true">search</span>查询</button>
|
||||
<a class="courier-information-button" href="{{ route('business.courier-payables.courier-information.export', array_filter($filters, static fn ($value) => $value !== '' && $value !== null)) }}"><span class="material-icons-outlined" aria-hidden="true">download</span>导出</a>
|
||||
<a class="courier-information-button" href="{{ route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-information']) }}"><span class="material-icons-outlined" aria-hidden="true">refresh</span>重置</a>
|
||||
</div>
|
||||
</form>
|
||||
@elseif (in_array($tab, ['networks', 'accounts'], true))
|
||||
<form method="GET" class="courier-information-filters" aria-label="{{ $tab === 'networks' ? '网点' : '打款信息' }}筛选">
|
||||
<input type="hidden" name="courier_tab" value="{{ $tab }}">
|
||||
<div class="courier-information-field">
|
||||
<label for="courier-related-expcode">快递代码</label>
|
||||
<input id="courier-related-expcode" name="expcode" type="text" value="{{ $filters['expcode'] ?? '' }}" placeholder="留空显示全部">
|
||||
</div>
|
||||
<div class="courier-information-actions">
|
||||
<button type="submit" class="courier-information-button is-primary"><span class="material-icons-outlined" aria-hidden="true">search</span>查询</button>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@if ($courierError)
|
||||
<div class="courier-information-notice is-error" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $courierError }}</span>
|
||||
</div>
|
||||
@elseif ($courierPending)
|
||||
<div class="courier-information-notice" role="status">
|
||||
<span class="material-icons-outlined" aria-hidden="true">schedule</span>
|
||||
<span>首次全量同步完成后刷新本页,即可查询工作平台中的快递资料。</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($tab === 'logs')
|
||||
<div class="courier-information-table-wrap">
|
||||
<table class="courier-information-table" style="min-width: 760px" aria-label="快递资料操作日志">
|
||||
<thead><tr><th>ID</th><th>快递代码</th><th>操作时间</th><th>创建时间</th><th>修改时间</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse ($rows ?? [] as $row)
|
||||
<tr><td class="is-code">{{ $row->Id ?? '-' }}</td><td class="is-code">{{ $row->expcode ?? '-' }}</td><td>{{ $row->doTime ?? '-' }}</td><td>{{ $datetime($row->createTime ?? null) }}</td><td>{{ $datetime($row->modifyTime ?? null) }}</td></tr>
|
||||
@empty
|
||||
<tr><td colspan="5" class="courier-information-empty">{{ $courierError ? '数据暂时不可用。' : '当前筛选条件下没有操作日志。' }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@if ($rows && !$courierError)
|
||||
<div class="courier-information-footer"><span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页,本页 {{ $rows->count() }} 条,共 {{ number_format($rows->total()) }} 条</span><nav class="courier-information-pagination" aria-label="操作日志分页"><a class="courier-information-page-link {{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->previousPageUrl() ?: '#' }}" aria-label="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a><a class="courier-information-page-link {{ $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>
|
||||
@elseif ($tab === 'express')
|
||||
<div class="courier-information-table-wrap">
|
||||
<table class="courier-information-table" aria-label="快递资料">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th><th>快递公司代码</th><th>快递公司名称</th><th>外部编码</th><th>杉橙快递代码</th><th>运费打款方式</th>
|
||||
<th>是否启用</th><th>可达服务</th><th>是否快运</th><th>自动寻号</th><th>配货单号寻号</th><th>数量拆单</th><th>重量0寻号</th>
|
||||
<th>停运时间</th><th>合同开始时间</th><th>合同结束时间</th><th>联系人</th><th>联系电话</th><th>自定义1</th><th>自定义2</th><th>自定义3</th>
|
||||
<th>体积0寻号</th><th>手工单号</th><th>解密</th><th>最后修改</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rows ?? [] as $row)
|
||||
@php $enabled = $bool($row->status ?? null) === '是'; @endphp
|
||||
<tr>
|
||||
<td class="is-code">{{ $row->Id ?? '-' }}</td>
|
||||
<td class="is-code">{{ $row->expcode ?? '-' }}</td>
|
||||
<td class="is-clamped" title="{{ $row->expname ?? '' }}">{{ $row->expname ?? '-' }}</td>
|
||||
<td class="is-code">{{ $row->outcode ?? '-' }}</td>
|
||||
<td class="is-code">{{ $row->scexpcode ?? '-' }}</td>
|
||||
<td>{{ $row->paymethod ?? '-' }}</td>
|
||||
<td><span class="courier-information-badge {{ $enabled ? 'is-enabled' : 'is-disabled' }}">{{ $enabled ? '是' : '否' }}</span></td>
|
||||
<td>{{ $bool($row->addvalidsgn ?? null) }}</td><td>{{ $bool($row->kuaiyunsgn ?? null) }}</td><td>{{ $bool($row->autonosgn ?? null) }}</td>
|
||||
<td>{{ $bool($row->packnosgn ?? null) }}</td><td>{{ $bool($row->skucountsplitsgn ?? null) }}</td><td>{{ $bool($row->weight0sgn ?? null) }}</td>
|
||||
<td>{{ $date($row->stopdate ?? null) }}</td><td>{{ $date($row->tradebtime ?? null) }}</td><td>{{ $date($row->tradeetime ?? null) }}</td>
|
||||
<td>{{ $row->linker ?? '-' }}</td><td>{{ $row->linkmobile ?? '-' }}</td>
|
||||
<td>{{ $row->undefined1 ?? '-' }}</td><td>{{ $row->undefined2 ?? '-' }}</td><td>{{ $row->undefined3 ?? '-' }}</td>
|
||||
<td>{{ $bool($row->volume0sgn ?? null) }}</td><td>{{ $bool($row->handnosgn ?? null) }}</td><td>{{ $bool($row->decryptsgn ?? null) }}</td>
|
||||
<td class="is-code">{{ $datetime($row->modified ?? null) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="25" class="courier-information-empty">{{ $courierError ? '数据暂时不可用。' : '当前筛选条件下没有快递资料。' }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@if ($rows && !$courierError)
|
||||
<div class="courier-information-footer">
|
||||
<span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页,本页 {{ $rows->count() }} 条,共 {{ number_format($rows->total()) }} 条</span>
|
||||
<nav class="courier-information-pagination" aria-label="快递资料分页">
|
||||
<a class="courier-information-page-link {{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->previousPageUrl() ?: '#' }}" aria-label="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
<a class="courier-information-page-link {{ $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>
|
||||
@elseif ($tab === 'networks')
|
||||
<div class="courier-information-table-wrap">
|
||||
<table class="courier-information-table" style="min-width: 1560px" aria-label="快递网点">
|
||||
<thead><tr><th>ID</th><th>快递代码</th><th>网点代码</th><th>网点名称</th><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">{{ $row->id ?? '-' }}</td><td class="is-code">{{ $row->expcode ?? '-' }}</td><td class="is-code">{{ $row->netcode ?? '-' }}</td><td>{{ $row->netname ?? '-' }}</td><td>{{ $row->netno ?? '-' }}</td><td>{{ $row->province ?? '-' }}</td><td>{{ $row->city ?? '-' }}</td><td>{{ $row->area ?? '-' }}</td><td>{{ $row->town ?? '-' }}</td><td class="is-clamped" title="{{ $row->address ?? '' }}">{{ $row->address ?? '-' }}</td><td>{{ $row->linker ?? '-' }}</td><td>{{ $row->linkermobile ?? '-' }}</td><td>{{ $bool($row->status ?? null) }}</td><td>{{ $row->apipltmname ?? ($row->apitype ?? '-') }}</td><td>{{ $datetime($row->created ?? null) }}</td><td>{{ $row->expnum ?? '-' }}</td></tr>
|
||||
@empty
|
||||
<tr><td colspan="16" class="courier-information-empty">{{ $courierError ? '数据暂时不可用。' : '当前筛选条件下没有网点资料。' }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@if ($rows && !$courierError)
|
||||
<div class="courier-information-footer"><span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页,本页 {{ $rows->count() }} 条,共 {{ number_format($rows->total()) }} 条</span><nav class="courier-information-pagination" aria-label="网点分页"><a class="courier-information-page-link {{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->previousPageUrl() ?: '#' }}" aria-label="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a><a class="courier-information-page-link {{ $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>
|
||||
@else
|
||||
<div class="courier-information-table-wrap">
|
||||
<table class="courier-information-table" style="min-width: 1120px" aria-label="打款信息">
|
||||
<thead><tr><th>ID</th><th>快递代码</th><th>快递名称</th><th>账户类型</th><th>账户</th><th>账户名称</th><th>开户行</th><th>创建时间</th><th>发票信息</th><th>ERP账户ID</th></tr></thead>
|
||||
<tbody>
|
||||
@forelse ($rows ?? [] as $row)
|
||||
<tr><td class="is-code">{{ $row->Id ?? '-' }}</td><td class="is-code">{{ $row->expcode ?? '-' }}</td><td>{{ $row->expname ?? '-' }}</td><td>{{ $row->accountType ?? '-' }}</td><td class="is-code">{{ $row->account ?? '-' }}</td><td>{{ $row->accountName ?? '-' }}</td><td>{{ $row->accountkhh ?? '-' }}</td><td>{{ $datetime($row->createTime ?? null) }}</td><td>{{ $row->invoice ?? '-' }}</td><td class="is-code">{{ $row->erpAccountId ?? '-' }}</td></tr>
|
||||
@empty
|
||||
<tr><td colspan="10" class="courier-information-empty">{{ $courierError ? '数据暂时不可用。' : '当前筛选条件下没有打款账户。' }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@if ($rows && !$courierError)
|
||||
<div class="courier-information-footer"><span>第 {{ $rows->currentPage() }} / {{ max($rows->lastPage(), 1) }} 页,本页 {{ $rows->count() }} 条,共 {{ number_format($rows->total()) }} 条</span><nav class="courier-information-pagination" aria-label="打款信息分页"><a class="courier-information-page-link {{ $rows->onFirstPage() ? 'is-disabled' : '' }}" href="{{ $rows->previousPageUrl() ?: '#' }}" aria-label="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a><a class="courier-information-page-link {{ $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
|
||||
</div>
|
||||
@ -110,6 +110,10 @@ Route::get('workspace/refunds/non-jingxi-refund-data/export', 'Business\Pages@ex
|
||||
->middleware('business.page:refunds,non-jingxi-refund-data')
|
||||
->name('business.refunds.non-jingxi-refund-data.export');
|
||||
|
||||
Route::get('workspace/courier-payables/courier-information/export', 'Business\Pages@exportCourierInformation')
|
||||
->middleware('business.page:courier-payables,courier-information')
|
||||
->name('business.courier-payables.courier-information.export');
|
||||
|
||||
Route::get('workspace/{section}/{page}/{tab}/export', 'Business\Pages@export')
|
||||
->name('business.pages.export');
|
||||
|
||||
|
||||
91
tests/Feature/Business/CourierInformationPageTest.php
Normal file
91
tests/Feature/Business/CourierInformationPageTest.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class CourierInformationPageTest extends FeatureTestCase
|
||||
{
|
||||
public function test_page_explains_when_courier_mirror_is_not_available(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
|
||||
$repository = Mockery::mock(CourierInformationMirrorRepository::class);
|
||||
$repository->shouldReceive('relatedAvailability')->once()->andReturn([
|
||||
'main' => false, 'networks' => false, 'accounts' => false,
|
||||
'network_ready' => false, 'account_ready' => false,
|
||||
]);
|
||||
$repository->shouldReceive('available')->once()->andReturnFalse();
|
||||
$repository->shouldNotReceive('state');
|
||||
$repository->shouldNotReceive('syncStatus');
|
||||
$repository->shouldNotReceive('ready');
|
||||
$repository->shouldNotReceive('paginate');
|
||||
$this->app->instance(CourierInformationMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'courier-payables',
|
||||
'page' => 'courier-information',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('快递资料')
|
||||
->assertSeeText('共享业务镜像尚未建立,快递资料暂时无法读取。')
|
||||
->assertSeeText('网点')
|
||||
->assertSeeText('打款信息')
|
||||
->assertSeeText('操作日志');
|
||||
}
|
||||
|
||||
public function test_page_renders_mirrored_courier_rows_and_filters(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
|
||||
$repository = Mockery::mock(CourierInformationMirrorRepository::class);
|
||||
$repository->shouldReceive('relatedAvailability')->once()->andReturn([
|
||||
'main' => true, 'networks' => false, 'accounts' => false,
|
||||
'network_ready' => false, 'account_ready' => false,
|
||||
]);
|
||||
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('state')->once()->andReturn([
|
||||
'status' => 'succeeded',
|
||||
'initial_completed_at' => '2026-08-28 10:00:00',
|
||||
'last_succeeded_at' => '2026-08-28 10:00:00',
|
||||
'mirror_row_count' => 1,
|
||||
]);
|
||||
$repository->shouldReceive('syncStatus')->once()->andReturn(['code' => 'healthy', 'lag_minutes' => 3]);
|
||||
$repository->shouldReceive('ready')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('paginate')->once()->withArgs(function (array $filters, int $page, int $limit): bool {
|
||||
return $filters['expcode'] === 'SF' && $filters['status'] === '1' && $page === 1 && $limit > 0;
|
||||
})->andReturn([
|
||||
'data' => [(object) [
|
||||
'Id' => 1,
|
||||
'expcode' => 'SF',
|
||||
'expname' => '顺丰速运',
|
||||
'status' => 1,
|
||||
'tradebtime' => '2026-01-01 00:00:00',
|
||||
]],
|
||||
'total' => 1,
|
||||
]);
|
||||
$this->app->instance(CourierInformationMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'courier-payables',
|
||||
'page' => 'courier-information',
|
||||
'expcode' => 'SF',
|
||||
'status' => '1',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('顺丰速运')
|
||||
->assertSeeText('SF')
|
||||
->assertSeeText('同步正常')
|
||||
->assertSeeText('可达服务')
|
||||
->assertSee('name="expcode"', false);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CourierInformationMirrorRepositoryTest extends TestCase
|
||||
{
|
||||
private CourierInformationMirrorRepository $repository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'database.connections.business_mirror' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
],
|
||||
'upstream_mirrors.connection' => 'business_mirror',
|
||||
'upstream_mirrors.datasets' => [
|
||||
CourierInformationMirrorRepository::DATASET_KEY => $this->definition('wp_express', 'js_wp_express', ['Id'], 'modified'),
|
||||
CourierInformationMirrorRepository::NETWORK_DATASET_KEY => $this->definition('wp_express_expNet', 'js_wp_express_exp_net', ['id']),
|
||||
CourierInformationMirrorRepository::ACCOUNT_DATASET_KEY => $this->definition('wp_express_account', 'js_wp_express_account', ['Id']),
|
||||
CourierInformationMirrorRepository::LOG_DATASET_KEY => $this->definition('wp_express_extension', 'js_wp_express_extension', ['Id'], 'modifyTime'),
|
||||
],
|
||||
]);
|
||||
DB::purge('business_mirror');
|
||||
|
||||
Schema::connection('business_mirror')->create('business_mirror_table_states', function (Blueprint $table): void {
|
||||
$table->string('dataset_key')->primary();
|
||||
$table->string('status')->nullable();
|
||||
$table->string('run_mode')->nullable();
|
||||
$table->json('cursor')->nullable();
|
||||
$table->json('high_watermark')->nullable();
|
||||
$table->dateTime('initial_completed_at')->nullable();
|
||||
$table->dateTime('last_succeeded_at')->nullable();
|
||||
$table->unsignedBigInteger('mirror_row_count')->default(0);
|
||||
});
|
||||
$this->createExpressTable();
|
||||
$this->createRelatedTables();
|
||||
|
||||
$this->repository = new CourierInformationMirrorRepository(
|
||||
new BusinessMirrorDatasetRegistry(),
|
||||
new GenericBusinessMirrorRepository(app('db')),
|
||||
);
|
||||
}
|
||||
|
||||
public function test_it_filters_and_pages_mirrored_courier_records(): void
|
||||
{
|
||||
$this->markReady(2);
|
||||
$this->insertExpress(1, ['expcode' => 'SF', 'expname' => '顺丰', 'status' => 1]);
|
||||
$this->insertExpress(2, ['expcode' => 'YD', 'expname' => '韵达', 'status' => 0]);
|
||||
|
||||
$result = $this->repository->paginate([
|
||||
'expcode' => 'SF',
|
||||
'expname' => '',
|
||||
'undefined1' => '',
|
||||
'undefined2' => '',
|
||||
'undefined3' => '',
|
||||
'tradebtime_from' => '',
|
||||
'tradebtime_to' => '',
|
||||
'status' => '1',
|
||||
], 1, 1);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('SF', $result['data'][0]->expcode);
|
||||
$this->assertTrue($this->repository->available());
|
||||
$this->assertTrue($this->repository->ready());
|
||||
}
|
||||
|
||||
public function test_it_reads_networks_and_accounts_by_courier_code(): void
|
||||
{
|
||||
$this->markReady(1);
|
||||
$this->markDatasetReady(CourierInformationMirrorRepository::NETWORK_DATASET_KEY);
|
||||
$this->markDatasetReady(CourierInformationMirrorRepository::ACCOUNT_DATASET_KEY);
|
||||
$this->markDatasetReady(CourierInformationMirrorRepository::LOG_DATASET_KEY);
|
||||
DB::connection('business_mirror')->table('js_wp_express_exp_net')->insert([
|
||||
'_mirror_key' => 'network-1', 'id' => 1, 'expid' => 1, 'expcode' => 'SF', 'netname' => '浦东网点',
|
||||
]);
|
||||
DB::connection('business_mirror')->table('js_wp_express_account')->insert([
|
||||
'_mirror_key' => 'account-1', 'Id' => 1, 'expcode' => 'SF', 'accountName' => '顺丰账户',
|
||||
]);
|
||||
DB::connection('business_mirror')->table('js_wp_express_extension')->insert([
|
||||
'_mirror_key' => 'log-1', 'Id' => 1, 'expcode' => 'SF', 'doTime' => 2,
|
||||
'createTime' => '2026-08-28 10:00:00', 'modifyTime' => '2026-08-28 10:00:00',
|
||||
]);
|
||||
|
||||
$networks = $this->repository->paginateNetworks('SF', 1, 20);
|
||||
$accounts = $this->repository->paginateAccounts('SF', 1, 20);
|
||||
$logs = $this->repository->paginateLogs('SF', 1, 20);
|
||||
|
||||
$this->assertSame(1, $networks['total']);
|
||||
$this->assertSame('浦东网点', $networks['data'][0]->netname);
|
||||
$this->assertSame(1, $accounts['total']);
|
||||
$this->assertSame('顺丰账户', $accounts['data'][0]->accountName);
|
||||
$this->assertSame(1, $logs['total']);
|
||||
$this->assertSame(2, $logs['data'][0]->doTime);
|
||||
}
|
||||
|
||||
private function createExpressTable(): void
|
||||
{
|
||||
Schema::connection('business_mirror')->create('js_wp_express', function (Blueprint $table): void {
|
||||
$table->string('_mirror_key')->primary();
|
||||
$table->integer('Id');
|
||||
foreach (['expcode', 'expname', 'outcode', 'scexpcode', 'paymethod', 'signcom', 'signmode', 'linker', 'linkmobile', 'paytype', 'undefined1', 'undefined2', 'undefined3', 'undefinedtype', 'undefined4', 'undefined5', 'dzqzjbr', 'guid', 'exp_properties'] as $column) {
|
||||
$table->string($column)->nullable();
|
||||
}
|
||||
foreach (['status', 'addvalidsgn', 'kuaiyunsgn', 'autonosgn', 'packnosgn', 'skucountsplitsgn', 'weight0sgn', 'volume0sgn', 'handnosgn', 'decryptsgn', 'pj1sgn', 'is_delete'] as $column) {
|
||||
$table->integer($column)->nullable();
|
||||
}
|
||||
foreach (['stopdate', 'tradebtime', 'tradeetime', 'modified'] as $column) {
|
||||
$table->dateTime($column)->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function createRelatedTables(): void
|
||||
{
|
||||
Schema::connection('business_mirror')->create('js_wp_express_exp_net', function (Blueprint $table): void {
|
||||
$table->string('_mirror_key')->primary();
|
||||
$table->integer('id');
|
||||
$table->integer('expid')->nullable();
|
||||
foreach (['expcode', 'netcode', 'netno', 'netname', 'isbill', 'province', 'city', 'area', 'town', 'address', 'linker', 'linkermobile', 'status', 'apitype', 'apipltmname', 'created', 'expnum'] as $column) {
|
||||
$table->string($column)->nullable();
|
||||
}
|
||||
});
|
||||
Schema::connection('business_mirror')->create('js_wp_express_account', function (Blueprint $table): void {
|
||||
$table->string('_mirror_key')->primary();
|
||||
$table->integer('Id');
|
||||
foreach (['expcode', 'expname', 'accountType', 'account', 'accountName', 'accountkhh', 'createTime', 'invoice', 'erpId', 'erpAccountId'] as $column) {
|
||||
$table->string($column)->nullable();
|
||||
}
|
||||
});
|
||||
Schema::connection('business_mirror')->create('js_wp_express_extension', function (Blueprint $table): void {
|
||||
$table->string('_mirror_key')->primary();
|
||||
$table->integer('Id');
|
||||
$table->string('expcode')->nullable();
|
||||
$table->integer('doTime')->nullable();
|
||||
$table->dateTime('createTime')->nullable();
|
||||
$table->dateTime('modifyTime')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
private function insertExpress(int $id, array $overrides): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('js_wp_express')->insert(array_merge([
|
||||
'_mirror_key' => 'express-' . $id,
|
||||
'Id' => $id,
|
||||
'expcode' => 'EXP-' . $id,
|
||||
'expname' => '测试快递',
|
||||
'status' => 1,
|
||||
'modified' => '2026-08-28 10:00:00',
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
private function markReady(int $rows): void
|
||||
{
|
||||
$this->markDatasetReady(CourierInformationMirrorRepository::DATASET_KEY, $rows);
|
||||
}
|
||||
|
||||
private function markDatasetReady(string $key, int $rows = 1): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
|
||||
'dataset_key' => $key,
|
||||
'status' => 'succeeded',
|
||||
'initial_completed_at' => '2026-08-28 10:00:00',
|
||||
'last_succeeded_at' => '2026-08-28 10:00:00',
|
||||
'mirror_row_count' => $rows,
|
||||
]);
|
||||
}
|
||||
|
||||
private function definition(string $object, string $target, array $keys, ?string $watermark = null): array
|
||||
{
|
||||
return [
|
||||
'source_key' => 'jiansun',
|
||||
'schema' => 'dbo',
|
||||
'object' => $object,
|
||||
'target' => $target,
|
||||
'keys' => $keys,
|
||||
'watermark' => $watermark,
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user