聚水潭聚工单接入售后费用 #96
@ -149,6 +149,77 @@ class WorkOrderMirrorRepository
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return after-sales alert work orders for the after-sales expense page.
|
||||
* The work-order values describe an operational alert; they are not added
|
||||
* to the order/refund monetary columns.
|
||||
*
|
||||
* @param array<string,string> $filters
|
||||
* @return array{data:list<array<string,mixed>>,total:int,page:int,per_page:int,last_page:int,ready:bool,sync_status:array<string,mixed>}
|
||||
*/
|
||||
public function paginateAfterSales(int $page, int $perPage, array $filters = []): array
|
||||
{
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 50);
|
||||
$syncStatus = $this->syncStatus();
|
||||
|
||||
if (! $syncStatus['ready']) {
|
||||
return [
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'last_page' => 1,
|
||||
'ready' => false,
|
||||
'sync_status' => $syncStatus,
|
||||
];
|
||||
}
|
||||
|
||||
$main = $this->datasets->get(self::MAIN_DATASET)->targetTable();
|
||||
$config = $this->datasets->get(self::CONFIG_DATASET)->targetTable();
|
||||
$latestConfigs = $this->connection->table($config . ' AS config_versions')
|
||||
->selectRaw('work_type, MAX(id) AS id')
|
||||
->groupBy('work_type');
|
||||
|
||||
$query = $this->connection->table($main . ' AS m')
|
||||
->joinSub($latestConfigs, 'config_versions', static function ($join): void {
|
||||
$join->on('config_versions.work_type', '=', 'm.work_type');
|
||||
})
|
||||
->join($config . ' AS c', 'c.id', '=', 'config_versions.id')
|
||||
->where('c.work_type_name', 'like', '%售后%')
|
||||
->where(static function (Builder $active): void {
|
||||
$active->whereNull('m.is_deleted')->orWhere('m.is_deleted', 0);
|
||||
});
|
||||
|
||||
$this->applyAfterSalesFilters($query, $filters);
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query
|
||||
->select([
|
||||
'm.id', 'm.work_type', 'c.work_type_name', 'c.group_name',
|
||||
'm.order_no', 'm.title', 'm.status', 'm.create_user',
|
||||
'm.submit_time', 'm.expect_end_time', 'm.end_time',
|
||||
'm.description', 'm.create_time', 'm.update_time',
|
||||
])
|
||||
->orderByDesc('m.create_time')
|
||||
->orderByDesc('m.id')
|
||||
->forPage($page, $perPage)
|
||||
->get()
|
||||
->map(static fn (object $row): array => (array) $row)
|
||||
->all();
|
||||
|
||||
$this->hydrateRelatedRows($rows);
|
||||
|
||||
return [
|
||||
'data' => $rows,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'per_page' => $perPage,
|
||||
'last_page' => max((int) ceil($total / $perPage), 1),
|
||||
'ready' => true,
|
||||
'sync_status' => $syncStatus,
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,string> $filters */
|
||||
private function applyFilters(Builder $query, array $filters): void
|
||||
{
|
||||
@ -185,6 +256,44 @@ class WorkOrderMirrorRepository
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,string> $filters */
|
||||
private function applyAfterSalesFilters(Builder $query, array $filters): void
|
||||
{
|
||||
if (($filters['from'] ?? '') !== '') {
|
||||
$query->where('m.create_time', '>=', $filters['from'] . ' 00:00:00');
|
||||
}
|
||||
if (($filters['to'] ?? '') !== '') {
|
||||
$query->where('m.create_time', '<', Carbon::parse($filters['to'])->addDay()->toDateString());
|
||||
}
|
||||
if (($filters['order_no'] ?? '') !== '') {
|
||||
$query->whereRaw("m.order_no LIKE ? ESCAPE '!'", [$this->contains($filters['order_no'])]);
|
||||
}
|
||||
if (($filters['shop_name'] ?? '') !== '') {
|
||||
// Older Tmall/JD alert forms store the shop text in a generic
|
||||
// question/field_1 value instead of the explicit shop_name key.
|
||||
$this->whereFormValueLike($query, ['shop_name', 'question', 'field_1'], $filters['shop_name']);
|
||||
}
|
||||
if (($filters['sku_id'] ?? '') !== '') {
|
||||
$this->whereFormValueLike($query, ['goods_id', 'id', 'question', 'field_1'], $filters['sku_id']);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param list<string> $fieldCodes */
|
||||
private function whereFormValueLike(Builder $query, array $fieldCodes, string $value): void
|
||||
{
|
||||
$valueTable = $this->datasets->get(self::VALUE_DATASET)->targetTable();
|
||||
$query->whereExists(function (Builder $valueQuery) use ($valueTable, $fieldCodes, $value): void {
|
||||
$valueQuery->selectRaw('1')
|
||||
->from($valueTable . ' AS filter_values')
|
||||
->whereColumn('filter_values.main_order_no', 'm.order_no')
|
||||
->whereIn('filter_values.field_code', $fieldCodes)
|
||||
->whereRaw("filter_values.field_value LIKE ? ESCAPE '!'", [$this->contains($value)])
|
||||
->where(static function (Builder $active): void {
|
||||
$active->whereNull('filter_values.is_deleted')->orWhere('filter_values.is_deleted', 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** @param list<array<string,mixed>> $rows */
|
||||
private function hydrateRelatedRows(array &$rows): void
|
||||
{
|
||||
|
||||
@ -6,6 +6,7 @@ use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataRepository;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\WorkOrderMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Throwable;
|
||||
@ -18,6 +19,7 @@ class OrderDataPageData
|
||||
private readonly OrderDataRepository $orders,
|
||||
private readonly UpstreamQueryCacheFactory $caches,
|
||||
private readonly ?OrderDataMirrorRepository $mirrorOrders = null,
|
||||
private readonly ?WorkOrderMirrorRepository $workOrderMirror = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -42,6 +44,7 @@ class OrderDataPageData
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => ['X-Caiwu-Data-Status' => 'mirror'],
|
||||
'orderDataError' => null,
|
||||
'afterSalesWorkOrders' => $this->afterSalesWorkOrders($filters),
|
||||
];
|
||||
}
|
||||
|
||||
@ -57,6 +60,7 @@ class OrderDataPageData
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => $cache->responseHeaders(),
|
||||
'orderDataError' => null,
|
||||
'afterSalesWorkOrders' => $this->afterSalesWorkOrders($filters),
|
||||
];
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
return $this->failed($filters, $page, $limit, $exception->getMessage());
|
||||
@ -118,6 +122,44 @@ class OrderDataPageData
|
||||
'orderDataFilters' => $filters,
|
||||
'orderDataStatus' => [],
|
||||
'orderDataError' => $message,
|
||||
'afterSalesWorkOrders' => $this->emptyAfterSalesWorkOrders(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array<string,string> $filters */
|
||||
private function afterSalesWorkOrders(array $filters): array
|
||||
{
|
||||
if ($this->workOrderMirror === null) {
|
||||
return $this->emptyAfterSalesWorkOrders();
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->workOrderMirror->paginateAfterSales(
|
||||
max((int) request('work_order_page', 1), 1),
|
||||
10,
|
||||
$filters,
|
||||
);
|
||||
} catch (Throwable) {
|
||||
return $this->emptyAfterSalesWorkOrders();
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{data:list<array<string,mixed>>,total:int,page:int,per_page:int,last_page:int,ready:bool,sync_status:array<string,mixed>} */
|
||||
private function emptyAfterSalesWorkOrders(): array
|
||||
{
|
||||
return [
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'page' => max((int) request('work_order_page', 1), 1),
|
||||
'per_page' => 10,
|
||||
'last_page' => 1,
|
||||
'ready' => false,
|
||||
'sync_status' => [
|
||||
'ready' => false,
|
||||
'code' => 'unavailable',
|
||||
'last_succeeded_at' => null,
|
||||
'datasets' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,268 @@
|
||||
@php
|
||||
$filters = [
|
||||
'from' => preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) request('from', '')) === 1 ? (string) request('from') : '',
|
||||
'to' => preg_match('/^\d{4}-\d{2}-\d{2}$/', (string) request('to', '')) === 1 ? (string) request('to') : '',
|
||||
'order_no' => mb_substr(trim((string) request('order_no', '')), 0, 100),
|
||||
'shop_name' => mb_substr(trim((string) request('shop_name', '')), 0, 100),
|
||||
'sku_id' => mb_substr(trim((string) request('sku_id', '')), 0, 100),
|
||||
];
|
||||
try {
|
||||
$workOrders = app(\Modules\EcommerceFinance\Repositories\WorkOrderMirrorRepository::class)->paginateAfterSales(
|
||||
max((int) request('work_order_page', 1), 1),
|
||||
10,
|
||||
$filters,
|
||||
);
|
||||
} catch (\Throwable) {
|
||||
$workOrders = [
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'page' => max((int) request('work_order_page', 1), 1),
|
||||
'per_page' => 10,
|
||||
'last_page' => 1,
|
||||
'ready' => false,
|
||||
'sync_status' => ['ready' => false, 'code' => 'unavailable', 'last_succeeded_at' => null, 'datasets' => []],
|
||||
];
|
||||
}
|
||||
@endphp
|
||||
|
||||
<div class="upstream-data-page upstream-data-page--spaced financial-after-sales-page">
|
||||
@include('business.pages.partials.upstream-data-styles')
|
||||
|
||||
<style>
|
||||
.financial-after-sales-page .after-sales-work-order-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-head h2 {
|
||||
margin: 0;
|
||||
font-size: .875rem;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-head p {
|
||||
margin: .35rem 0 0;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: .78125rem;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-filters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(9rem, 1fr)) auto;
|
||||
gap: .75rem;
|
||||
align-items: end;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-filters label {
|
||||
display: grid;
|
||||
gap: .35rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-filters input {
|
||||
min-width: 0;
|
||||
height: 2.125rem;
|
||||
padding: 0 .625rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: .5rem;
|
||||
background: var(--vip-surface, #fff);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-filters input:focus {
|
||||
border-color: var(--vip-content-accent-dark, #856632);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(133, 102, 50, .2);
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-actions {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-table {
|
||||
width: 100%;
|
||||
min-width: 54rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-table th,
|
||||
.financial-after-sales-page .after-sales-work-order-table td {
|
||||
padding: .75rem 1rem;
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
font-size: .78125rem;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-table th {
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-table td {
|
||||
color: var(--vip-content-ink, #171717);
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-table td:last-child {
|
||||
max-width: 20rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: .75rem 1rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: .78125rem;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-pagination__controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.financial-after-sales-page .after-sales-work-order-filters {
|
||||
grid-template-columns: repeat(2, minmax(9rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.financial-after-sales-page .after-sales-work-order-head,
|
||||
.financial-after-sales-page .after-sales-work-order-pagination {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.financial-after-sales-page .after-sales-work-order-filters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<section class="upstream-data-table-panel" aria-labelledby="financial-after-sales-work-orders-title">
|
||||
<div class="after-sales-work-order-head">
|
||||
<div>
|
||||
<h2 id="financial-after-sales-work-orders-title">聚水潭售后工单</h2>
|
||||
<p>工单作为售后处理线索展示,费用金额仍以订单售后费用明细的金额字段为准。</p>
|
||||
</div>
|
||||
@if (($workOrders['ready'] ?? false) && ($workOrders['total'] ?? 0) > 0)
|
||||
<span>{{ $workOrders['total'] }} 条工单</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<form method="GET" class="after-sales-work-order-filters" aria-label="聚水潭售后工单筛选">
|
||||
<label>
|
||||
<span>创建开始日期</span>
|
||||
<input type="date" name="from" value="{{ $filters['from'] }}">
|
||||
</label>
|
||||
<label>
|
||||
<span>创建结束日期</span>
|
||||
<input type="date" name="to" value="{{ $filters['to'] }}">
|
||||
</label>
|
||||
<label>
|
||||
<span>订单号</span>
|
||||
<input type="search" name="order_no" value="{{ $filters['order_no'] }}" placeholder="按订单号筛选">
|
||||
</label>
|
||||
<label>
|
||||
<span>店铺</span>
|
||||
<input type="search" name="shop_name" value="{{ $filters['shop_name'] }}" placeholder="按表单店铺筛选">
|
||||
</label>
|
||||
<label>
|
||||
<span>商品 ID</span>
|
||||
<input type="search" name="sku_id" value="{{ $filters['sku_id'] }}" placeholder="按表单商品 ID 筛选">
|
||||
</label>
|
||||
<div class="after-sales-work-order-actions">
|
||||
<button type="submit" class="upstream-data-button upstream-data-button--primary">
|
||||
<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">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@if (!($workOrders['ready'] ?? false))
|
||||
<div class="upstream-data-alert" role="status">
|
||||
<span class="material-icons-outlined" aria-hidden="true">sync</span>
|
||||
<span>聚水潭售后工单镜像尚未完成首次同步,当前不把空镜像解释为没有工单。</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="after-sales-work-order-table-wrap">
|
||||
<table class="after-sales-work-order-table" aria-label="聚水潭售后工单列表">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>工单号</th>
|
||||
<th>工单类型</th>
|
||||
<th>标题</th>
|
||||
<th>状态</th>
|
||||
<th>创建人</th>
|
||||
<th>创建时间</th>
|
||||
<th>表单信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse (($workOrders['data'] ?? []) as $workOrder)
|
||||
<tr>
|
||||
<td>{{ $workOrder['order_no'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['work_type_name'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['title'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['status'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['create_user'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['create_time'] ?? '-' }}</td>
|
||||
<td>
|
||||
@forelse (($workOrder['form_values'] ?? []) as $formValue)
|
||||
<div>{{ $formValue['field_name'] ?? $formValue['field_code'] ?? '字段' }}:{{ $formValue['field_value'] ?? '-' }}</div>
|
||||
@empty
|
||||
-
|
||||
@endforelse
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7">{{ ($workOrders['ready'] ?? false) ? '当前筛选暂无售后工单。' : '等待售后工单镜像首次同步。' }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (($workOrders['ready'] ?? false) && ($workOrders['last_page'] ?? 1) > 1)
|
||||
<div class="after-sales-work-order-pagination">
|
||||
<span>第 {{ $workOrders['page'] }} / {{ $workOrders['last_page'] }} 页,共 {{ $workOrders['total'] }} 条</span>
|
||||
<div class="after-sales-work-order-pagination__controls">
|
||||
<a
|
||||
href="{{ $workOrders['page'] > 1 ? request()->fullUrlWithQuery(['work_order_page' => $workOrders['page'] - 1]) : '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => $workOrders['page'] <= 1])
|
||||
aria-label="上一页售后工单"
|
||||
title="上一页售后工单"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
<a
|
||||
href="{{ $workOrders['page'] < $workOrders['last_page'] ? request()->fullUrlWithQuery(['work_order_page' => $workOrders['page'] + 1]) : '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => $workOrders['page'] >= $workOrders['last_page']])
|
||||
aria-label="下一页售后工单"
|
||||
title="下一页售后工单"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</section>
|
||||
</div>
|
||||
@ -126,6 +126,10 @@
|
||||
$filters = $orderData['orderDataFilters'];
|
||||
$status = data_get($orderData, 'orderDataStatus.X-Caiwu-Data-Status');
|
||||
$orderDataError = $orderData['orderDataError'];
|
||||
$afterSalesWorkOrders = $orderData['afterSalesWorkOrders'] ?? [
|
||||
'data' => [], 'total' => 0, 'page' => 1, 'per_page' => 10, 'last_page' => 1,
|
||||
'ready' => false, 'sync_status' => [],
|
||||
];
|
||||
@endphp
|
||||
|
||||
<div class="upstream-data-page upstream-data-page--spaced">
|
||||
@ -315,6 +319,87 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="upstream-data-table-panel" data-after-sales-work-orders="true" style="margin-top: 1rem;">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;gap:1rem;padding:1rem;border-bottom:1px solid var(--vip-border, #d8d2c7);">
|
||||
<div>
|
||||
<h2>聚水潭售后工单</h2>
|
||||
<p>售后预警工单仅作为经营处理线索展示,不计入上方订单退款金额。</p>
|
||||
</div>
|
||||
@if (($afterSalesWorkOrders['ready'] ?? false) && ($afterSalesWorkOrders['total'] ?? 0) > 0)
|
||||
<span style="color:var(--vip-content-muted, #595959);font-size:.78125rem;">
|
||||
共 {{ $afterSalesWorkOrders['total'] }} 条
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (!($afterSalesWorkOrders['ready'] ?? false))
|
||||
<div class="upstream-data-alert" role="status">
|
||||
<span class="material-icons-outlined" aria-hidden="true">sync</span>
|
||||
<span>聚水潭售后工单镜像尚未完成首次同步,当前不把空镜像解释为没有工单。</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="upstream-data-table-scroll">
|
||||
<table class="upstream-data-table" aria-label="聚水潭售后工单">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>工单号</th>
|
||||
<th>工单类型</th>
|
||||
<th>标题</th>
|
||||
<th>状态</th>
|
||||
<th>创建人</th>
|
||||
<th>创建时间</th>
|
||||
<th>表单信息</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse (($afterSalesWorkOrders['data'] ?? []) as $workOrder)
|
||||
<tr>
|
||||
<td data-export-value="{{ $workOrder['order_no'] ?? '' }}">{{ $workOrder['order_no'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['work_type_name'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['title'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['status'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['create_user'] ?? '-' }}</td>
|
||||
<td>{{ $workOrder['create_time'] ?? '-' }}</td>
|
||||
<td>
|
||||
@forelse (($workOrder['form_values'] ?? []) as $formValue)
|
||||
<div>{{ $formValue['field_name'] ?? $formValue['field_code'] ?? '字段' }}:{{ $formValue['field_value'] ?? '-' }}</div>
|
||||
@empty
|
||||
-
|
||||
@endforelse
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="7">暂无售后工单</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (($afterSalesWorkOrders['ready'] ?? false) && ($afterSalesWorkOrders['last_page'] ?? 1) > 1)
|
||||
<div class="upstream-data-pagination">
|
||||
<span>当前页 {{ count($afterSalesWorkOrders['data'] ?? []) }} 条</span>
|
||||
<div class="upstream-data-pagination__controls">
|
||||
<a
|
||||
href="{{ $afterSalesWorkOrders['page'] > 1 ? request()->fullUrlWithQuery(['work_order_page' => $afterSalesWorkOrders['page'] - 1]) : '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => $afterSalesWorkOrders['page'] <= 1])
|
||||
aria-label="上一页售后工单"
|
||||
title="上一页售后工单"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
<span>第 {{ $afterSalesWorkOrders['page'] }} / {{ $afterSalesWorkOrders['last_page'] }} 页</span>
|
||||
<a
|
||||
href="{{ $afterSalesWorkOrders['page'] < $afterSalesWorkOrders['last_page'] ? request()->fullUrlWithQuery(['work_order_page' => $afterSalesWorkOrders['page'] + 1]) : '#' }}"
|
||||
@class(['upstream-data-button', 'is-disabled' => $afterSalesWorkOrders['page'] >= $afterSalesWorkOrders['last_page']])
|
||||
aria-label="下一页售后工单"
|
||||
title="下一页售后工单"
|
||||
><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Tests\Feature\Business;
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\OrderDataMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\WorkOrderMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
@ -19,6 +20,18 @@ class OrderDataPagesUpstreamTest extends FeatureTestCase
|
||||
$mirror->shouldReceive('ready')->zeroOrMoreTimes()->andReturnFalse();
|
||||
$mirror->shouldReceive('promotionReady')->zeroOrMoreTimes()->andReturnFalse();
|
||||
$this->app->instance(OrderDataMirrorRepository::class, $mirror);
|
||||
|
||||
$workOrders = Mockery::mock(WorkOrderMirrorRepository::class);
|
||||
$workOrders->shouldReceive('paginateAfterSales')->zeroOrMoreTimes()->andReturn([
|
||||
'data' => [],
|
||||
'total' => 0,
|
||||
'page' => 1,
|
||||
'per_page' => 10,
|
||||
'last_page' => 1,
|
||||
'ready' => false,
|
||||
'sync_status' => ['ready' => false, 'code' => 'initial_sync_pending', 'last_succeeded_at' => null, 'datasets' => []],
|
||||
]);
|
||||
$this->app->instance(WorkOrderMirrorRepository::class, $workOrders);
|
||||
}
|
||||
|
||||
public function test_after_sales_prefers_complete_business_mirror_without_registering_a_snapshot(): void
|
||||
@ -187,6 +200,55 @@ class OrderDataPagesUpstreamTest extends FeatureTestCase
|
||||
->assertSee('disabled="disabled"', false);
|
||||
}
|
||||
|
||||
public function test_after_sales_page_renders_mirrored_work_orders_as_a_separate_read_only_section(): void
|
||||
{
|
||||
$this->allowOrderDataPages();
|
||||
$mirror = Mockery::mock(OrderDataMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->once()->andReturnTrue();
|
||||
$mirror->shouldReceive('afterSales')->once()->andReturn(['rows' => []]);
|
||||
$this->app->instance(OrderDataMirrorRepository::class, $mirror);
|
||||
|
||||
$workOrders = Mockery::mock(WorkOrderMirrorRepository::class);
|
||||
$workOrders->shouldReceive('paginateAfterSales')->once()->withArgs(function (int $page, int $limit, array $filters): bool {
|
||||
return $page === 1 && $limit === 10
|
||||
&& $filters['from'] === '2026-08-24'
|
||||
&& $filters['to'] === '2026-08-24'
|
||||
&& $filters['sku_id'] === 'SKU-AF';
|
||||
})->andReturn([
|
||||
'data' => [[
|
||||
'order_no' => 'WO-005-AF',
|
||||
'work_type_name' => '天猫_售后超标',
|
||||
'title' => '售后超标提醒',
|
||||
'status' => '待处理',
|
||||
'create_user' => '运营一组',
|
||||
'create_time' => '2026-08-24 10:00:00',
|
||||
'form_values' => [['field_name' => '店铺名称', 'field_value' => '天猫旗舰店']],
|
||||
]],
|
||||
'total' => 1,
|
||||
'page' => 1,
|
||||
'per_page' => 10,
|
||||
'last_page' => 1,
|
||||
'ready' => true,
|
||||
'sync_status' => ['ready' => true, 'code' => 'ready', 'last_succeeded_at' => '2026-08-24 12:00:00', 'datasets' => []],
|
||||
]);
|
||||
$this->app->instance(WorkOrderMirrorRepository::class, $workOrders);
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'order-data',
|
||||
'page' => 'raw-data',
|
||||
'tab' => 'after-sales-expenses',
|
||||
'from' => '2026-08-24',
|
||||
'to' => '2026-08-24',
|
||||
'sku_id' => 'SKU-AF',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('聚水潭售后工单')
|
||||
->assertSeeText('WO-005-AF')
|
||||
->assertSeeText('天猫_售后超标')
|
||||
->assertSeeText('天猫旗舰店')
|
||||
->assertSeeText('不计入上方订单退款金额');
|
||||
}
|
||||
|
||||
private function allowOrderDataPages(): void
|
||||
{
|
||||
$this->user->update(['locale' => 'zh-CN']);
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Tests\Feature\Business;
|
||||
use App\Models\Auth\Permission;
|
||||
use Modules\EcommerceFinance\Models\AlipayBill;
|
||||
use Modules\EcommerceFinance\Repositories\NonJingxiRefundMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\WorkOrderMirrorRepository;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class PagesTest extends FeatureTestCase
|
||||
@ -190,6 +191,53 @@ class PagesTest extends FeatureTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function testFinancialAfterSalesPageRendersMirroredJushuitanWorkOrders(): void
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||||
);
|
||||
|
||||
$workOrders = \Mockery::mock(WorkOrderMirrorRepository::class);
|
||||
$workOrders->shouldReceive('paginateAfterSales')->once()->withArgs(function (int $page, int $limit, array $filters): bool {
|
||||
return $page === 1 && $limit === 10
|
||||
&& $filters['from'] === '2026-08-24'
|
||||
&& $filters['to'] === '2026-08-24'
|
||||
&& $filters['shop_name'] === '天猫旗舰店';
|
||||
})->andReturn([
|
||||
'data' => [[
|
||||
'order_no' => 'WO-FIN-005',
|
||||
'work_type_name' => '天猫_售后超标',
|
||||
'title' => '售后超标提醒',
|
||||
'status' => '待处理',
|
||||
'create_user' => '运营一组',
|
||||
'create_time' => '2026-08-24 10:00:00',
|
||||
'form_values' => [['field_name' => '店铺名称', 'field_value' => '天猫旗舰店']],
|
||||
]],
|
||||
'total' => 1,
|
||||
'page' => 1,
|
||||
'per_page' => 10,
|
||||
'last_page' => 1,
|
||||
'ready' => true,
|
||||
'sync_status' => ['ready' => true, 'code' => 'ready', 'last_succeeded_at' => '2026-08-24 12:00:00', 'datasets' => []],
|
||||
]);
|
||||
$this->app->instance(WorkOrderMirrorRepository::class, $workOrders);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'financial-operations',
|
||||
'page' => 'after-sales-expenses',
|
||||
'from' => '2026-08-24',
|
||||
'to' => '2026-08-24',
|
||||
'shop_name' => '天猫旗舰店',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('聚水潭售后工单')
|
||||
->assertSeeText('WO-FIN-005')
|
||||
->assertSeeText('天猫_售后超标')
|
||||
->assertSeeText('天猫旗舰店')
|
||||
->assertSeeText('费用金额仍以订单售后费用明细的金额字段为准');
|
||||
}
|
||||
|
||||
public function testRefundPagesUseTheGenericBusinessPageRoute()
|
||||
{
|
||||
$this->user->attachPermission(
|
||||
|
||||
@ -97,6 +97,45 @@ class WorkOrderMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame('WO-001', $secondPage['data'][0]['order_no']);
|
||||
}
|
||||
|
||||
public function test_after_sales_alerts_are_filtered_by_type_and_form_values(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('fixture_work_order_config')->insert([
|
||||
'id' => 3,
|
||||
'work_type' => 'ORDER_AFSALES',
|
||||
'work_type_name' => '天猫_售后超标',
|
||||
'group_name' => '运营',
|
||||
'is_enabled' => 1,
|
||||
]);
|
||||
DB::connection('business_mirror')->table('fixture_work_order_main')->insert([
|
||||
'id' => 5,
|
||||
'work_type' => 'ORDER_AFSALES',
|
||||
'order_no' => 'WO-005-AF',
|
||||
'title' => '售后超标提醒',
|
||||
'status' => '待处理',
|
||||
'create_user' => '运营一组',
|
||||
'create_time' => '2026-08-24 10:00:00',
|
||||
'update_time' => '2026-08-24 10:00:00',
|
||||
'is_deleted' => 0,
|
||||
]);
|
||||
DB::connection('business_mirror')->table('fixture_work_order_values')->insert([
|
||||
['id' => 3, 'main_order_no' => 'WO-005-AF', 'field_code' => 'shop_name', 'field_name' => '店铺名称', 'field_value' => '天猫旗舰店', 'field_type' => 'text', 'sort' => 1, 'is_deleted' => 0],
|
||||
['id' => 4, 'main_order_no' => 'WO-005-AF', 'field_code' => 'id', 'field_name' => '商品ID', 'field_value' => 'SKU-AF-1', 'field_type' => 'text', 'sort' => 2, 'is_deleted' => 0],
|
||||
]);
|
||||
|
||||
$result = $this->repository->paginateAfterSales(1, 10, [
|
||||
'from' => '2026-08-24',
|
||||
'to' => '2026-08-24',
|
||||
'shop_name' => '旗舰店',
|
||||
'sku_id' => 'SKU-AF',
|
||||
]);
|
||||
|
||||
$this->assertTrue($result['ready']);
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('WO-005-AF', $result['data'][0]['order_no']);
|
||||
$this->assertSame('天猫_售后超标', $result['data'][0]['work_type_name']);
|
||||
$this->assertSame('天猫旗舰店', $result['data'][0]['form_values'][0]['field_value']);
|
||||
}
|
||||
|
||||
public function test_unready_state_is_explicit_and_never_looks_like_empty_data(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('business_mirror_table_states')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user