Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
fdb7f77173
154
app/Business/JstOrderLabelImporter.php
Normal file
154
app/Business/JstOrderLabelImporter.php
Normal file
@ -0,0 +1,154 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 聚水潭订单标签明细导入(从共享镜像 ops_jst_orders 拆标签写入 jst_order_labels)。
|
||||
*
|
||||
* 一个标签一行:一单有 N 个标签就写 N 行;订单不去重、标签不汇总。
|
||||
* 无标签的订单保留一行,标签记为「(无标签)」,方便与订单总数核对。
|
||||
*/
|
||||
class JstOrderLabelImporter
|
||||
{
|
||||
public const NO_LABEL = '(无标签)';
|
||||
|
||||
private const READ_CHUNK = 2000;
|
||||
|
||||
private const INSERT_CHUNK = 2000;
|
||||
|
||||
public function __construct(private readonly DatabaseManager $databases) {}
|
||||
|
||||
/**
|
||||
* @return array{from:string,to:string,orders:int,rows:int,no_label_orders:int,labels:int,seconds:float}
|
||||
*/
|
||||
public function import(string $from, string $to, int $companyId = 1, bool $replace = false, string $source = 'jst-order-label-import'): array
|
||||
{
|
||||
$from = $this->normalizeDate($from, '起始日期');
|
||||
$to = $this->normalizeDate($to, '结束日期');
|
||||
if ($from > $to) {
|
||||
throw new InvalidArgumentException('起始日期不能晚于结束日期。');
|
||||
}
|
||||
|
||||
$start = $from . ' 00:00:00';
|
||||
$end = date('Y-m-d', strtotime($to . ' +1 day')) . ' 00:00:00';
|
||||
|
||||
$mirror = $this->databases->connection(config('upstream_mirrors.connection', 'business_mirror'));
|
||||
if (! $mirror->getSchemaBuilder()->hasTable('ops_jst_orders')) {
|
||||
throw new RuntimeException('聚水潭订单镜像尚未建立(ops_jst_orders)。');
|
||||
}
|
||||
|
||||
$target = $this->databases->connection();
|
||||
$startedAt = microtime(true);
|
||||
|
||||
if ($replace) {
|
||||
$target->table('jst_order_labels')
|
||||
->where('company_id', $companyId)
|
||||
->where('ordered_at', '>=', $start)
|
||||
->where('ordered_at', '<', $end)
|
||||
->delete();
|
||||
}
|
||||
|
||||
$now = now();
|
||||
$orders = 0;
|
||||
$rows = 0;
|
||||
$noLabelOrders = 0;
|
||||
$buffer = [];
|
||||
|
||||
$flush = function () use (&$buffer, &$rows, $target): void {
|
||||
foreach (array_chunk($buffer, self::INSERT_CHUNK) as $chunk) {
|
||||
$target->table('jst_order_labels')->insert($chunk);
|
||||
$rows += count($chunk);
|
||||
}
|
||||
$buffer = [];
|
||||
};
|
||||
|
||||
$mirror->table('ops_jst_orders')
|
||||
->where('order_date', '>=', $start)
|
||||
->where('order_date', '<', $end)
|
||||
->orderBy('Id')
|
||||
->chunkById(self::READ_CHUNK, function ($records) use (&$buffer, &$orders, &$noLabelOrders, $flush, $companyId, $source, $now): void {
|
||||
foreach ($records as $record) {
|
||||
$orders++;
|
||||
$orderedAt = $record->order_date === null ? null : substr((string) $record->order_date, 0, 19);
|
||||
if ($orderedAt === null || $orderedAt === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$labels = $this->splitLabels((string) ($record->labels ?? ''));
|
||||
if ($labels === []) {
|
||||
$labels = [self::NO_LABEL];
|
||||
$noLabelOrders++;
|
||||
}
|
||||
|
||||
foreach ($labels as $label) {
|
||||
$buffer[] = [
|
||||
'company_id' => $companyId,
|
||||
'order_no' => mb_substr((string) $record->so_id, 0, 100),
|
||||
'shop_name' => mb_substr((string) ($record->shop_name ?? ''), 0, 191),
|
||||
'ordered_at' => $orderedAt,
|
||||
'label' => $label,
|
||||
'created_from' => $source,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
if (count($buffer) >= self::INSERT_CHUNK * 2) {
|
||||
$flush();
|
||||
}
|
||||
}
|
||||
}, 'Id');
|
||||
|
||||
$flush();
|
||||
|
||||
$labels = $target->table('jst_order_labels')
|
||||
->where('company_id', $companyId)
|
||||
->where('ordered_at', '>=', $start)
|
||||
->where('ordered_at', '<', $end)
|
||||
->distinct()
|
||||
->count('label');
|
||||
|
||||
return [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'orders' => $orders,
|
||||
'rows' => $rows,
|
||||
'no_label_orders' => $noLabelOrders,
|
||||
'labels' => (int) $labels,
|
||||
'seconds' => round(microtime(true) - $startedAt, 1),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function splitLabels(string $labels): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (explode(',', $labels) as $label) {
|
||||
$label = trim($label);
|
||||
if ($label !== '') {
|
||||
$out[] = mb_substr($label, 0, 191);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
private function normalizeDate(string $value, string $label): string
|
||||
{
|
||||
$value = trim($value);
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) !== 1) {
|
||||
throw new InvalidArgumentException($label . '必须是 YYYY-MM-DD。');
|
||||
}
|
||||
|
||||
[$year, $month, $day] = array_map('intval', explode('-', $value));
|
||||
if (! checkdate($month, $day, $year)) {
|
||||
throw new InvalidArgumentException($label . '不是有效日期。');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
42
app/Console/Commands/ImportJstOrderLabels.php
Normal file
42
app/Console/Commands/ImportJstOrderLabels.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Business\JstOrderLabelImporter;
|
||||
use Illuminate\Console\Command;
|
||||
use Throwable;
|
||||
|
||||
class ImportJstOrderLabels extends Command
|
||||
{
|
||||
protected $signature = 'jst-order-labels:import
|
||||
{--from= : 起始下单日期 YYYY-MM-DD}
|
||||
{--to= : 结束下单日期 YYYY-MM-DD}
|
||||
{--company=1 : 公司 ID}
|
||||
{--replace : 先清空该区间已有行}';
|
||||
|
||||
protected $description = '把聚水潭订单按标签拆行导入「聚水潭订单标签明细」(账单流水数据页)';
|
||||
|
||||
public function handle(JstOrderLabelImporter $importer): int
|
||||
{
|
||||
$from = (string) $this->option('from');
|
||||
$to = (string) $this->option('to');
|
||||
|
||||
try {
|
||||
$result = $importer->import(
|
||||
$from,
|
||||
$to,
|
||||
(int) $this->option('company'),
|
||||
(bool) $this->option('replace'),
|
||||
'jst-order-label-import',
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
$this->error($exception->getMessage());
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->info(json_encode($result, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@ -1254,10 +1254,121 @@ class Pages extends Controller
|
||||
'bill-offset' => app(\Modules\EcommerceFinance\View\BillOffsetPageData::class)->page(),
|
||||
'account-mapping' => $this->contentForAccountMapping(),
|
||||
'planting-details' => $this->contentForPlantingDetails(),
|
||||
'jst-order-labels' => $this->contentForJstOrderLabels(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单流水数据 › 聚水潭订单标签明细。
|
||||
*
|
||||
* 按标签拆行展示(一个标签一行),不做标签汇总、不对订单去重。
|
||||
*/
|
||||
protected function contentForJstOrderLabels(): array
|
||||
{
|
||||
$request = request();
|
||||
$filters = $this->jstOrderLabelFilters($request);
|
||||
$query = $this->jstOrderLabelQuery($filters);
|
||||
|
||||
$limit = min(max((int) setting('default.list_limit', 25), 10), 100);
|
||||
$records = $query
|
||||
->orderByDesc('ordered_at')
|
||||
->orderByDesc('id')
|
||||
->paginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
return [
|
||||
'view' => 'business.pages.billing-transactions.jst-order-labels',
|
||||
'records' => $records,
|
||||
'filters' => $filters,
|
||||
'totalRows' => $records->total(),
|
||||
'filterOptions' => [
|
||||
'shops' => $this->jstOrderLabelOptions('shop_name', 500),
|
||||
'labels' => $this->jstOrderLabelOptions('label', 500),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function jstOrderLabelFilters(Request $request): array
|
||||
{
|
||||
return [
|
||||
'date_from' => $this->dateFilter($request->query('date_from')),
|
||||
'date_to' => $this->dateFilter($request->query('date_to')),
|
||||
'shop_name' => mb_substr(trim((string) $request->query('shop_name')), 0, 191),
|
||||
'label' => mb_substr(trim((string) $request->query('label')), 0, 191),
|
||||
'keyword' => mb_substr(trim((string) $request->query('keyword')), 0, 100),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,string> $filters */
|
||||
private function jstOrderLabelQuery(array $filters): \Illuminate\Database\Eloquent\Builder
|
||||
{
|
||||
$query = \App\Models\Business\JstOrderLabel::query()->where('company_id', company_id());
|
||||
|
||||
if ($filters['date_from'] !== '') {
|
||||
$query->where('ordered_at', '>=', $filters['date_from'] . ' 00:00:00');
|
||||
}
|
||||
if ($filters['date_to'] !== '') {
|
||||
$query->where('ordered_at', '<', date('Y-m-d', strtotime($filters['date_to'] . ' +1 day')) . ' 00:00:00');
|
||||
}
|
||||
if ($filters['shop_name'] !== '') {
|
||||
$query->where('shop_name', $filters['shop_name']);
|
||||
}
|
||||
if ($filters['label'] !== '') {
|
||||
$query->where('label', 'like', '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $filters['label']) . '%');
|
||||
}
|
||||
if ($filters['keyword'] !== '') {
|
||||
$query->where('order_no', 'like', '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $filters['keyword']) . '%');
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function jstOrderLabelOptions(string $column, int $limit): array
|
||||
{
|
||||
return \App\Models\Business\JstOrderLabel::query()
|
||||
->where('company_id', company_id())
|
||||
->whereNotNull($column)
|
||||
->where($column, '<>', '')
|
||||
->distinct()
|
||||
->orderBy($column)
|
||||
->limit($limit)
|
||||
->pluck($column)
|
||||
->all();
|
||||
}
|
||||
|
||||
/** 账单流水数据 › 聚水潭订单标签明细导出(按当前筛选,全量) */
|
||||
public function exportJstOrderLabels(Request $request): StreamedResponse
|
||||
{
|
||||
$query = $this->jstOrderLabelQuery($this->jstOrderLabelFilters($request));
|
||||
|
||||
return response()->streamDownload(function () use ($query): void {
|
||||
$csv = fopen('php://output', 'w');
|
||||
if ($csv === false) {
|
||||
throw new \RuntimeException('无法创建聚水潭订单标签明细导出流。');
|
||||
}
|
||||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
fputcsv($csv, ['线上订单号', '店铺名称', '下单时间', '标签']);
|
||||
|
||||
$query->orderBy('id')->chunkById(2000, function ($rows) use ($csv): void {
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($csv, [
|
||||
$row->order_no,
|
||||
$row->shop_name,
|
||||
$row->ordered_at?->format('Y-m-d H:i:s'),
|
||||
$row->label,
|
||||
]);
|
||||
}
|
||||
});
|
||||
fclose($csv);
|
||||
}, '聚水潭订单标签明细_' . now()->format('Ymd_His') . '.csv', [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 账单流水数据 › 种菜明细表。
|
||||
*/
|
||||
|
||||
26
app/Models/Business/JstOrderLabel.php
Normal file
26
app/Models/Business/JstOrderLabel.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Business;
|
||||
|
||||
use App\Abstracts\Model;
|
||||
|
||||
/**
|
||||
* 账单流水数据 › 聚水潭订单标签明细(按标签拆行,一标签一行)。
|
||||
*/
|
||||
class JstOrderLabel extends Model
|
||||
{
|
||||
protected $table = 'jst_order_labels';
|
||||
|
||||
protected $fillable = [
|
||||
'company_id',
|
||||
'order_no',
|
||||
'shop_name',
|
||||
'ordered_at',
|
||||
'label',
|
||||
'created_from',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ordered_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
@ -427,6 +427,7 @@ $navigation = [
|
||||
'pages' => [
|
||||
['slug' => 'bank-transactions', 'title' => '银行账户流水'],
|
||||
['slug' => 'planting-details', 'title' => '种菜明细表'],
|
||||
['slug' => 'jst-order-labels', 'title' => '聚水潭订单标签明细'],
|
||||
[
|
||||
'slug' => 'store-receiving-bills',
|
||||
'title' => '店铺收款账户账单',
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* 账单流水数据 › 聚水潭订单标签明细。
|
||||
*
|
||||
* 来源:聚水潭订单镜像 ops_jst_orders(线上订单号 so_id / 店铺 shop_name / 下单时间 order_date / 标签 labels)。
|
||||
* 按标签拆行存储:一个标签一行,一单有 N 个标签就有 N 行,订单不去重、标签不汇总。
|
||||
* 无标签的订单保留一行,标签记为「(无标签)」。
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('jst_order_labels', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedInteger('company_id');
|
||||
$table->string('order_no', 100)->comment('线上订单号');
|
||||
$table->string('shop_name', 191)->nullable()->comment('店铺名称');
|
||||
$table->dateTime('ordered_at')->comment('下单时间');
|
||||
$table->string('label', 191)->comment('标签(单个)');
|
||||
$table->string('created_from', 100)->nullable();
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
$table->index(['company_id', 'ordered_at'], 'jst_order_labels_company_time');
|
||||
$table->index(['company_id', 'label'], 'jst_order_labels_company_label');
|
||||
$table->index(['company_id', 'shop_name'], 'jst_order_labels_company_shop');
|
||||
$table->index('order_no', 'jst_order_labels_order_no');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('jst_order_labels');
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,213 @@
|
||||
@php
|
||||
$records = $records ?? null;
|
||||
$filters = $filters ?? [];
|
||||
$totalRows = (int) ($totalRows ?? ($records?->total() ?? 0));
|
||||
$filterOptions = $filterOptions ?? ['shops' => [], 'labels' => []];
|
||||
@endphp
|
||||
|
||||
<div class="upstream-data-page upstream-data-page--spaced" data-jst-order-labels="true">
|
||||
@include('business.pages.partials.upstream-data-styles')
|
||||
|
||||
<style>
|
||||
.jst-order-labels-table {
|
||||
min-width: 68rem;
|
||||
}
|
||||
|
||||
.jst-order-labels-date {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.jst-order-labels-date input {
|
||||
border-radius: 0.375rem 0 0 0.375rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jst-order-labels-clear {
|
||||
display: inline-flex;
|
||||
width: 2.125rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-left: 0;
|
||||
border-radius: 0 0.375rem 0.375rem 0;
|
||||
background: #fff;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.jst-order-labels-clear:hover {
|
||||
border-color: var(--vip-content-accent-dark, #856632);
|
||||
color: var(--vip-content-accent-dark, #856632);
|
||||
}
|
||||
|
||||
.jst-order-labels-sep {
|
||||
align-self: flex-end;
|
||||
padding-bottom: 0.6rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
}
|
||||
|
||||
.jst-order-labels-hint {
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<p class="jst-order-labels-hint">
|
||||
数据来源:聚水潭订单(共享镜像 ops_jst_orders),标签按行拆开、订单不去重、标签不汇总;无标签订单记为「(无标签)」。
|
||||
当前筛选共 {{ number_format($totalRows) }} 行。
|
||||
</p>
|
||||
|
||||
<form class="upstream-data-toolbar" method="get" action="{{ request()->url() }}" aria-label="聚水潭订单标签明细筛选">
|
||||
<div class="upstream-data-field">
|
||||
<label for="jst-label-date-from">下单时间从</label>
|
||||
<span class="jst-order-labels-date">
|
||||
<input id="jst-label-date-from" name="date_from" type="date" value="{{ $filters['date_from'] ?? '' }}" title="点击选择开始日期">
|
||||
<button class="jst-order-labels-clear" type="button" data-jst-labels-clear="jst-label-date-from" title="清除开始日期" aria-label="清除开始日期">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<span class="jst-order-labels-sep" aria-hidden="true">至</span>
|
||||
<div class="upstream-data-field">
|
||||
<label for="jst-label-date-to">下单时间至</label>
|
||||
<span class="jst-order-labels-date">
|
||||
<input id="jst-label-date-to" name="date_to" type="date" value="{{ $filters['date_to'] ?? '' }}" title="点击选择结束日期">
|
||||
<button class="jst-order-labels-clear" type="button" data-jst-labels-clear="jst-label-date-to" title="清除结束日期" aria-label="清除结束日期">×</button>
|
||||
</span>
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="jst-label-shop">店铺名称</label>
|
||||
<select id="jst-label-shop" name="shop_name">
|
||||
<option value="">全部店铺</option>
|
||||
@foreach ($filterOptions['shops'] as $shop)
|
||||
<option value="{{ $shop }}" @selected(($filters['shop_name'] ?? '') === $shop)>{{ $shop }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="jst-label-label">标签</label>
|
||||
<select id="jst-label-label" name="label">
|
||||
<option value="">全部标签</option>
|
||||
@foreach ($filterOptions['labels'] as $label)
|
||||
<option value="{{ $label }}" @selected(($filters['label'] ?? '') === $label)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="jst-label-keyword">线上订单号</label>
|
||||
<input id="jst-label-keyword" name="keyword" type="text" value="{{ $filters['keyword'] ?? '' }}" placeholder="输入完整或部分订单号">
|
||||
</div>
|
||||
<div class="upstream-data-actions">
|
||||
<button class="upstream-data-button upstream-data-button--primary" type="submit">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a class="upstream-data-button" href="{{ request()->url() }}" title="清除筛选条件">
|
||||
<span class="material-icons-outlined" aria-hidden="true">restart_alt</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
<a class="upstream-data-button" href="{{ route('business.billing-transactions.jst-order-labels.export', array_merge(['company_id' => company_id()], request()->query())) }}" title="按当前筛选导出全部明细">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>导出</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="upstream-data-table-panel">
|
||||
<div class="upstream-data-table-scroll">
|
||||
<table class="upstream-data-table jst-order-labels-table" aria-label="聚水潭订单标签明细">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">序号</th>
|
||||
<th scope="col">线上订单号</th>
|
||||
<th scope="col">店铺名称</th>
|
||||
<th scope="col">下单时间</th>
|
||||
<th scope="col">标签</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($records as $index => $record)
|
||||
<tr>
|
||||
<td style="color: var(--vip-content-muted, #595959)">{{ $records->firstItem() + $index }}</td>
|
||||
<td>{{ $record->order_no ?: '-' }}</td>
|
||||
<td>{{ $record->shop_name ?: '-' }}</td>
|
||||
<td>{{ $record->ordered_at?->format('Y-m-d H:i:s') ?? '-' }}</td>
|
||||
<td>{{ $record->label ?: '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="5">
|
||||
{{ $totalRows === 0 ? '暂无数据,请先用 jst-order-labels:import 导入订单标签明细。' : '当前筛选条件下没有记录,请调整筛选条件后重试。' }}
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="upstream-data-pagination">
|
||||
<span>
|
||||
共 {{ number_format($records->total()) }} 行
|
||||
· 每页 {{ $records->perPage() }} 行
|
||||
· 第 {{ $records->currentPage() }} / {{ max($records->lastPage(), 1) }} 页
|
||||
</span>
|
||||
<div class="upstream-data-pagination__controls" aria-label="聚水潭订单标签明细分页">
|
||||
@if ($records->onFirstPage())
|
||||
<span class="upstream-data-button is-disabled" aria-disabled="true" title="上一页">
|
||||
<span class="material-icons-outlined" aria-hidden="true">chevron_left</span>
|
||||
</span>
|
||||
@else
|
||||
<a class="upstream-data-button" href="{{ $records->previousPageUrl() }}" aria-label="上一页" title="上一页">
|
||||
<span class="material-icons-outlined" aria-hidden="true">chevron_left</span>
|
||||
</a>
|
||||
@endif
|
||||
<span>第 {{ $records->currentPage() }} / {{ max($records->lastPage(), 1) }} 页</span>
|
||||
@if ($records->hasMorePages())
|
||||
<a class="upstream-data-button" href="{{ $records->nextPageUrl() }}" aria-label="下一页" title="下一页">
|
||||
<span class="material-icons-outlined" aria-hidden="true">chevron_right</span>
|
||||
</a>
|
||||
@else
|
||||
<span class="upstream-data-button is-disabled" aria-disabled="true" title="下一页">
|
||||
<span class="material-icons-outlined" aria-hidden="true">chevron_right</span>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.querySelector('[data-jst-order-labels="true"] form.upstream-data-toolbar');
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
var from = form.querySelector('#jst-label-date-from');
|
||||
var to = form.querySelector('#jst-label-date-to');
|
||||
|
||||
[from, to].forEach(function (input) {
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
input.addEventListener('click', function () {
|
||||
if (typeof input.showPicker === 'function') {
|
||||
try { input.showPicker(); } catch (error) { /* 已经打开时忽略 */ }
|
||||
}
|
||||
});
|
||||
input.addEventListener('change', function () {
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
|
||||
form.querySelectorAll('[data-jst-labels-clear]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var input = form.querySelector('#' + button.getAttribute('data-jst-labels-clear'));
|
||||
if (input) {
|
||||
input.value = '';
|
||||
}
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@ -172,6 +172,10 @@ Route::get('workspace/billing-transactions/planting-details/export', 'Business\P
|
||||
->middleware('business.page:billing-transactions,planting-details')
|
||||
->name('business.billing-transactions.planting-details.export');
|
||||
|
||||
Route::get('workspace/billing-transactions/jst-order-labels/export', 'Business\Pages@exportJstOrderLabels')
|
||||
->middleware('business.page:billing-transactions,jst-order-labels')
|
||||
->name('business.billing-transactions.jst-order-labels.export');
|
||||
|
||||
Route::prefix('workspace/product-inventory/inventory-report/warehouse-mappings')
|
||||
->middleware('business.page:product-inventory,inventory-report')
|
||||
->name('business.product-inventory.inventory-report.warehouse-mappings.')
|
||||
|
||||
116
tests/Feature/Business/JstOrderLabelsPageTest.php
Normal file
116
tests/Feature/Business/JstOrderLabelsPageTest.php
Normal file
@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Business\JstOrderLabelImporter;
|
||||
use App\Models\Auth\Permission;
|
||||
use App\Models\Business\JstOrderLabel;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
/**
|
||||
* 账单流水数据 › 聚水潭订单标签明细(按标签拆行导入 + 页面 + 筛选 + 导出)。
|
||||
*/
|
||||
class JstOrderLabelsPageTest extends FeatureTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config(['database.connections.business_mirror' => [
|
||||
'driver' => 'sqlite',
|
||||
'database' => ':memory:',
|
||||
'prefix' => '',
|
||||
]]);
|
||||
DB::purge('business_mirror');
|
||||
|
||||
Schema::connection('business_mirror')->create('ops_jst_orders', function ($table): void {
|
||||
$table->integer('Id')->primary();
|
||||
$table->string('so_id')->nullable();
|
||||
$table->string('shop_name')->nullable();
|
||||
$table->dateTime('order_date')->nullable();
|
||||
$table->text('labels')->nullable();
|
||||
});
|
||||
|
||||
DB::connection('business_mirror')->table('ops_jst_orders')->insert([
|
||||
['Id' => 1, 'so_id' => '260901-391328633273048', 'shop_name' => '拼多多-优乐猫家具旗舰店', 'order_date' => '2026-09-01 00:00:13', 'labels' => '发货前售后,店铺-拼多多'],
|
||||
['Id' => 2, 'so_id' => '3608400015164657', 'shop_name' => '京喜-久伴吾家具京喜自营工厂店', 'order_date' => '2026-09-01 00:00:44', 'labels' => '京东供销订单, 京喜'],
|
||||
['Id' => 3, 'so_id' => '110211297173802010', 'shop_name' => '得物-全米', 'order_date' => '2026-08-31 23:59:00', 'labels' => '品牌直发'],
|
||||
['Id' => 4, 'so_id' => '260915-000000000000001', 'shop_name' => '得物-全米', 'order_date' => '2026-09-15 10:00:00', 'labels' => ''],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_import_splits_labels_without_deduping_orders(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
|
||||
$result = app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
|
||||
|
||||
$this->assertSame(3, $result['orders']);
|
||||
$this->assertSame(5, $result['rows']);
|
||||
$this->assertSame(1, $result['no_label_orders']);
|
||||
|
||||
$rows = JstOrderLabel::query()->where('company_id', $this->company->id)->orderBy('id')->get();
|
||||
$this->assertSame(['发货前售后', '店铺-拼多多', '京东供销订单', '京喜', '(无标签)'], $rows->pluck('label')->all());
|
||||
// 同一订单拆成多行,订单不去重
|
||||
$this->assertSame(2, $rows->where('order_no', '260901-391328633273048')->count());
|
||||
$this->assertSame('2026-09-15 10:00:00', $rows->last()->ordered_at->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
public function test_import_window_excludes_out_of_range_orders(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
|
||||
// 8-31 的订单不在 9 月窗口内
|
||||
$result = app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
|
||||
$orders = JstOrderLabel::query()->where('company_id', $this->company->id)->pluck('order_no')->unique()->all();
|
||||
|
||||
$this->assertNotContains('110211297173802010', $orders);
|
||||
$this->assertSame(3, $result['orders']);
|
||||
}
|
||||
|
||||
public function test_page_filters_and_export(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
app(JstOrderLabelImporter::class)->import('2026-09-01', '2026-09-15', $this->company->id, true);
|
||||
|
||||
$page = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'billing-transactions',
|
||||
'page' => 'jst-order-labels',
|
||||
]));
|
||||
|
||||
$page->assertOk()
|
||||
->assertSee('聚水潭订单标签明细')
|
||||
->assertSee('260901-391328633273048')
|
||||
->assertSee('店铺-拼多多')
|
||||
->assertSee('(无标签)')
|
||||
->assertSee('id="jst-label-date-from"', false)
|
||||
->assertSee('data-jst-labels-clear="jst-label-date-to"', false);
|
||||
|
||||
$filtered = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'billing-transactions',
|
||||
'page' => 'jst-order-labels',
|
||||
'label' => '京喜',
|
||||
]));
|
||||
$filtered->assertOk()
|
||||
->assertSee('3608400015164657')
|
||||
->assertDontSee('260901-391328633273048');
|
||||
|
||||
$export = $this->loginAs()->get(route('business.billing-transactions.jst-order-labels.export', [
|
||||
'label' => '京喜',
|
||||
]));
|
||||
$export->assertOk()->assertDownload();
|
||||
$csv = $export->streamedContent();
|
||||
$this->assertStringContainsString('线上订单号,店铺名称,下单时间,标签', explode("\n", $csv)[0]);
|
||||
$this->assertStringContainsString('3608400015164657', $csv);
|
||||
$this->assertStringNotContainsString('260901-391328633273048', $csv);
|
||||
}
|
||||
|
||||
private function allowPage(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user