Merge remote-tracking branch 'origin/main'

This commit is contained in:
chenxi 2026-09-30 16:39:12 +08:00
commit fdb7f77173
9 changed files with 707 additions and 0 deletions

View 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;
}
}

View 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;
}
}

View File

@ -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',
]);
}
/**
* 账单流水数据 › 种菜明细表。
*/

View 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',
];
}

View File

@ -427,6 +427,7 @@ $navigation = [
'pages' => [
['slug' => 'bank-transactions', 'title' => '银行账户流水'],
['slug' => 'planting-details', 'title' => '种菜明细表'],
['slug' => 'jst-order-labels', 'title' => '聚水潭订单标签明细'],
[
'slug' => 'store-receiving-bills',
'title' => '店铺收款账户账单',

View File

@ -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');
}
};

View File

@ -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>

View File

@ -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.')

View 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()
);
}
}