对齐聚水潭小额打款完成时间和状态口径 #157
@ -107,6 +107,8 @@ class Pages extends Controller
|
||||
|
||||
protected const ACCOUNT_BALANCE_SECTION = 'account-balance-data';
|
||||
|
||||
protected const SMALL_PAYMENT_SECTION = 'financial-operations';
|
||||
|
||||
protected const ACCOUNT_BALANCE_PAGES = [
|
||||
'bank-balance',
|
||||
'store-receiving-balance',
|
||||
@ -138,6 +140,16 @@ class Pages extends Controller
|
||||
|
||||
public function show(string $section, ?string $page = null, ?string $tab = null)
|
||||
{
|
||||
if ($section === 'refunds' && $page === 'small-payment-work-orders') {
|
||||
$url = route('business.pages.show', [
|
||||
'section' => self::SMALL_PAYMENT_SECTION,
|
||||
'page' => $page,
|
||||
]);
|
||||
$query = request()->getQueryString();
|
||||
|
||||
return redirect()->to($url . ($query === null ? '' : '?' . $query));
|
||||
}
|
||||
|
||||
if ($section === 'billing-transactions' && in_array($page, self::ACCOUNT_BALANCE_PAGES, true)) {
|
||||
$url = route('business.pages.show', array_filter([
|
||||
'section' => self::ACCOUNT_BALANCE_SECTION,
|
||||
@ -1159,7 +1171,7 @@ class Pages extends Controller
|
||||
{
|
||||
$filters = $this->smallPaymentWorkOrderFilters($request);
|
||||
$redirectParameters = array_filter([
|
||||
'section' => 'refunds',
|
||||
'section' => self::SMALL_PAYMENT_SECTION,
|
||||
'page' => 'small-payment-work-orders',
|
||||
...$filters,
|
||||
], static fn (mixed $value): bool => $value !== '' && $value !== null);
|
||||
|
||||
@ -36,12 +36,14 @@ class Sidebar extends Controller
|
||||
|
||||
$settings = SidebarPermissions::settings();
|
||||
$configured = in_array($subject, $settings['configured'] ?? [], true);
|
||||
$selected = $configured ? (array) ($settings['subjects'][$subject] ?? []) : SidebarPermissions::allKeys();
|
||||
$selected = $configured
|
||||
? SidebarPermissions::normalizeAllowedKeys((array) ($settings['subjects'][$subject] ?? []))
|
||||
: SidebarPermissions::allKeys();
|
||||
$source = $configured ? '当前使用单独配置。' : '未单独配置,使用系统默认完整页面。';
|
||||
$canInherit = str_starts_with($subject, 'user_');
|
||||
if (str_starts_with($subject, 'group_')) {
|
||||
$group = UserAccessGroup::forCompany()->findOrFail(substr($subject, 6));
|
||||
$selected = $group->page_permissions ?? [];
|
||||
$selected = SidebarPermissions::normalizeAllowedKeys($group->page_permissions ?? []);
|
||||
$configured = true;
|
||||
$source = '分组权限与用户的其他分组权限合并;用户例外配置优先。新分组默认没有页面权限。';
|
||||
} elseif ($canInherit && ! $configured) {
|
||||
|
||||
@ -7,6 +7,10 @@ use App\Models\Auth\UserAccessGroup;
|
||||
|
||||
class SidebarPermissions
|
||||
{
|
||||
private const LEGACY_PAGE_KEYS = [
|
||||
'refunds/small-payment-work-orders' => 'financial-operations/small-payment-work-orders',
|
||||
];
|
||||
|
||||
public static function settings(): array
|
||||
{
|
||||
$value = setting('system.sidebar_permissions', []);
|
||||
@ -39,12 +43,14 @@ class SidebarPermissions
|
||||
$settings = self::settings();
|
||||
$userSubject = 'user_' . $user->id;
|
||||
if (in_array($userSubject, $settings['configured'] ?? [], true)) {
|
||||
return array_values(array_unique((array) ($settings['subjects'][$userSubject] ?? [])));
|
||||
return self::normalizeAllowedKeys((array) ($settings['subjects'][$userSubject] ?? []));
|
||||
}
|
||||
|
||||
$groups = UserAccessGroup::forUser($user);
|
||||
if ($groups->isNotEmpty()) {
|
||||
return $groups->flatMap(fn ($group) => $group->page_permissions ?? [])->unique()->values()->all();
|
||||
return self::normalizeAllowedKeys(
|
||||
$groups->flatMap(fn ($group) => $group->page_permissions ?? [])->all()
|
||||
);
|
||||
}
|
||||
|
||||
$subject = 'role_' . SystemAccess::roleName($user);
|
||||
@ -53,7 +59,7 @@ class SidebarPermissions
|
||||
return null;
|
||||
}
|
||||
|
||||
return array_values(array_unique((array) ($settings['subjects'][$subject] ?? [])));
|
||||
return self::normalizeAllowedKeys((array) ($settings['subjects'][$subject] ?? []));
|
||||
}
|
||||
|
||||
public static function navigationFor(User $user): array
|
||||
@ -127,4 +133,20 @@ class SidebarPermissions
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
public static function normalizeAllowedKeys(array $keys): array
|
||||
{
|
||||
$normalized = [];
|
||||
|
||||
foreach ($keys as $key) {
|
||||
$key = (string) $key;
|
||||
$normalized[] = self::LEGACY_PAGE_KEYS[$key] ?? $key;
|
||||
|
||||
if ($key === 'refunds') {
|
||||
$normalized[] = self::LEGACY_PAGE_KEYS['refunds/small-payment-work-orders'];
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($normalized));
|
||||
}
|
||||
}
|
||||
|
||||
@ -430,7 +430,6 @@ $navigation = [
|
||||
'pages' => [
|
||||
['slug' => 'jingxi-refund-data', 'title' => '京喜退款数据'],
|
||||
['slug' => 'non-jingxi-refund-data', 'title' => '售后(退货退款)'],
|
||||
['slug' => 'small-payment-work-orders', 'title' => '小额打款'],
|
||||
],
|
||||
],
|
||||
[
|
||||
@ -563,6 +562,7 @@ $navigation = [
|
||||
['slug' => 'advertising-expenses', 'title' => '广告费用'],
|
||||
['slug' => 'marketing-expenses', 'title' => '营销费用'],
|
||||
['slug' => 'after-sales-expenses', 'title' => '售后费用'],
|
||||
['slug' => 'small-payment-work-orders', 'title' => '小额打款'],
|
||||
['slug' => 'management-expenses', 'title' => '管理费用'],
|
||||
['slug' => 'labor-expenses', 'title' => '人工费用'],
|
||||
['slug' => 'bookkeeping-details', 'title' => '记账明细'],
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private const INDEXES = [
|
||||
'js_jst_work_order_info' => [
|
||||
'idx_small_payment_completion',
|
||||
['woTypeId', 'completeTime', 'woNo', 'Id'],
|
||||
],
|
||||
'js_jst_small_payment_order_map' => [
|
||||
'idx_small_payment_order_map',
|
||||
['woTypeId', 'woNo'],
|
||||
],
|
||||
];
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
if (config('system_database.mode', 'bootstrap') === 'bootstrap') {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = Schema::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||||
foreach (self::INDEXES as $table => [$name, $columns]) {
|
||||
if (! $schema->hasTable($table) || $schema->hasIndex($table, $name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$schema->table($table, static fn (Blueprint $blueprint) => $blueprint->index($columns, $name));
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (config('system_database.mode', 'bootstrap') === 'bootstrap') {
|
||||
return;
|
||||
}
|
||||
|
||||
$schema = Schema::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||||
foreach (self::INDEXES as $table => [$name]) {
|
||||
if (! $schema->hasTable($table) || ! $schema->hasIndex($table, $name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$schema->table($table, static fn (Blueprint $blueprint) => $blueprint->dropIndex($name));
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -16,7 +16,7 @@
|
||||
| 本次接入 | `jiansun/dbo.jst_purchase + dbo.jst_purchaseItem` 通过固定联表查询进入 `product_purchase_price_mirror_rows`;普通商品资料只读取 `Finished` 且带“1688一键代发”标签的最近采购单价,并与原商品采购价分别标注来源。 |
|
||||
| 本次接入 | 工作平台快递资料页面读取 `wp_express` 主表,并预留网点 `wp_express_expNet`、打款账户 `wp_express_account` 标签;三张表均登记为独立镜像数据集,首次同步完成前页面明确显示同步状态。 |
|
||||
| 本次接入 | 聚水潭聚工单协同工单登记 `work_order_main`、`work_order_sub`、`work_order_form_config`、`work_order_form_value` 四张表;工单 API 固定筛选类型“仓内未发”,首次同步完成前返回同步未完成状态。 |
|
||||
| 待首次同步 | 聚水潭聚工单“小额打款”读取 `jiansun.dbo.jst_workOrderInfo` 与已解析映射 `dbo.jst_small_payment_order_map`;退款模块的小额打款子页面固定筛选工单类型 `82D94F5C54544EE3833CD8A3AB8B9F73`,页面只读共享镜像,首次同步完成前明确显示待同步状态。 |
|
||||
| 待首次同步 | 聚水潭聚工单“小额打款”读取 `jiansun.dbo.jst_workOrderInfo` 与已解析映射 `dbo.jst_small_payment_order_map`;财务经营报表的小额打款页面固定筛选工单类型 `82D94F5C54544EE3833CD8A3AB8B9F73`,页面只读共享镜像,首次同步完成前明确显示待同步状态。 |
|
||||
| 本次建立 | `config/upstream_mirrors.php` 中登记全部当前代码引用的原表;通用表级同步框架负责建表、稳定游标、断点续传、增量重叠和全量校准。 |
|
||||
| 已开始逐域切换 | 公司、店铺、商标页面在对应镜像完成首次全量后自动改读完整镜像表;首次全量前保留旧快照作为兼容回滚。其他业务域仍需逐项切换,不得宣称全项目已经完成。 |
|
||||
| 中央同步接入 | 店铺维度报表从 `jiansun.dbo.jst_originalBillInfo` 和 `dbo.fys_shopInfo_flow` 自动、可断点同步;不再依赖开发者电脑、SQLite 或人工 XLSX。 |
|
||||
@ -118,4 +118,4 @@ php artisan business-mirror:sync-tables jiansun.dbo.jst_workOrderInfo --full
|
||||
php artisan business-mirror:sync-tables jiansun.dbo.jst_small_payment_order_map --full
|
||||
```
|
||||
|
||||
两张表完成后,页面按映射表的工单创建时间稳定分页,并以 `woNo` 关联最新的工单主记录。筛选支持日期、工单号、店铺、支付宝批次号、线上订单号、状态和打款原因;表单详情从镜像中的 `businessFieldList` 解析,导出与页面使用同一筛选口径。首次全量未完成、查询索引未就绪或刷新失败时均保留明确状态,不能把空结果解释为没有小额打款工单。
|
||||
两张表完成后,页面按工单主记录的完成时间筛选并稳定分页,以 `woNo` 关联最新的工单主记录;查询依赖工单表 `(woTypeId, completeTime, woNo, Id)` 和映射表 `(woTypeId, woNo)` 两个索引。映射表的 `transaction_no` 是线上订单号后备来源,不得展示为支付宝批次号;支付宝批次号只读取表单中的明确字段。工单状态按聚水潭状态码翻译,已完成工单再结合打款方式显示打款状态。筛选支持完成日期、工单号、店铺、支付宝批次号、线上订单号、状态和打款原因;表单详情从镜像中的 `businessFieldList` 解析,导出与页面使用同一筛选口径。首次全量未完成、查询索引未就绪或刷新失败时均保留明确状态,不能把空结果解释为没有小额打款工单。
|
||||
|
||||
@ -22,10 +22,24 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
public const SMALL_PAYMENT_WORK_TYPE_ID = '82D94F5C54544EE3833CD8A3AB8B9F73';
|
||||
|
||||
private const REQUIRED_QUERY_INDEXES = [
|
||||
self::WORK_ORDER_DATASET => [['woTypeId', 'woNo', 'Id']],
|
||||
self::PAYMENT_MAP_DATASET => [['woTypeId', 'work_order_create_time', 'woNo']],
|
||||
self::WORK_ORDER_DATASET => [
|
||||
['woTypeId', 'woNo', 'Id'],
|
||||
['woTypeId', 'completeTime', 'woNo', 'Id'],
|
||||
],
|
||||
self::PAYMENT_MAP_DATASET => [['woTypeId', 'woNo']],
|
||||
];
|
||||
|
||||
private const WORK_ORDER_STATUS_LABELS = [
|
||||
'WAIT_AUDIT' => '待审核',
|
||||
'REJECT_AUDIT' => '审核驳回',
|
||||
'WAIT_CONFIRM' => '待确认',
|
||||
'CONFIRMING' => '确认中',
|
||||
'CONFIRMED' => '已完成',
|
||||
'DISABLED' => '已作废',
|
||||
];
|
||||
|
||||
private const COMPLETED_WORK_ORDER_STATUSES = ['CONFIRMED', '已完成', '已确认'];
|
||||
|
||||
private readonly ConnectionInterface $connection;
|
||||
|
||||
public function __construct(
|
||||
@ -147,7 +161,7 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query
|
||||
->select($this->selectColumns())
|
||||
->orderByDesc('p.work_order_create_time')
|
||||
->orderByDesc('w.completeTime')
|
||||
->orderByDesc('p.woNo')
|
||||
->forPage($page, $perPage)
|
||||
->get()
|
||||
@ -177,7 +191,7 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
do {
|
||||
$rows = (clone $query)
|
||||
->select($this->selectColumns())
|
||||
->orderByDesc('p.work_order_create_time')
|
||||
->orderByDesc('w.completeTime')
|
||||
->orderByDesc('p.woNo')
|
||||
->forPage($page, $chunkSize)
|
||||
->get();
|
||||
@ -201,21 +215,18 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
{
|
||||
$workOrders = $this->dataset(self::WORK_ORDER_DATASET)->targetTable();
|
||||
$paymentMap = $this->dataset(self::PAYMENT_MAP_DATASET)->targetTable();
|
||||
$latestWorkOrders = $this->connection->table($workOrders . ' AS work_order_versions')
|
||||
->selectRaw('woNo, MAX(Id) AS Id')
|
||||
->where('woTypeId', self::SMALL_PAYMENT_WORK_TYPE_ID)
|
||||
->groupBy('woNo');
|
||||
|
||||
return $this->connection->table($paymentMap . ' AS p')
|
||||
->joinSub($latestWorkOrders, 'work_order_versions', static function ($join): void {
|
||||
$join->on('work_order_versions.woNo', '=', 'p.woNo');
|
||||
return $this->connection->table($workOrders . ' AS w')
|
||||
->join($paymentMap . ' AS p', static function ($join): void {
|
||||
$join->on('p.woTypeId', '=', 'w.woTypeId')
|
||||
->on('p.woNo', '=', 'w.woNo');
|
||||
})
|
||||
->join($workOrders . ' AS w', 'w.Id', '=', 'work_order_versions.Id')
|
||||
// Match the leading columns of (woTypeId, woNo, Id), rather than
|
||||
// rescanning all orders of this type for every payment-map row.
|
||||
->whereColumn('w.woNo', 'p.woNo')
|
||||
->where('p.woTypeId', self::SMALL_PAYMENT_WORK_TYPE_ID)
|
||||
->where('w.woTypeId', self::SMALL_PAYMENT_WORK_TYPE_ID);
|
||||
->leftJoin($workOrders . ' AS newer_work_order', static function ($join): void {
|
||||
$join->on('newer_work_order.woTypeId', '=', 'w.woTypeId')
|
||||
->on('newer_work_order.woNo', '=', 'w.woNo')
|
||||
->on('newer_work_order.Id', '>', 'w.Id');
|
||||
})
|
||||
->where('w.woTypeId', self::SMALL_PAYMENT_WORK_TYPE_ID)
|
||||
->whereNull('newer_work_order.Id');
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
@ -223,7 +234,7 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
{
|
||||
return [
|
||||
'w.Id AS source_id', 'p.woNo AS work_order_no',
|
||||
'p.transaction_no AS alipay_batch_no', 'w.status AS work_order_status',
|
||||
'p.transaction_no AS mapped_online_order_no', 'w.status AS work_order_status',
|
||||
'p.status AS mapped_status', 'w.title AS work_order_title',
|
||||
'p.title AS mapped_title', 'w.content AS work_order_content',
|
||||
'w.grade AS priority', 'w.urging AS urging_count',
|
||||
@ -239,8 +250,8 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
/** @param array<string,string> $filters */
|
||||
private function applyFilters(Builder $query, array $filters): void
|
||||
{
|
||||
$query->where('p.work_order_create_time', '>=', $filters['from'] . ' 00:00:00')
|
||||
->where('p.work_order_create_time', '<', Carbon::parse($filters['to'])->addDay()->format('Y-m-d 00:00:00'));
|
||||
$query->where('w.completeTime', '>=', $filters['from'] . ' 00:00:00')
|
||||
->where('w.completeTime', '<', Carbon::parse($filters['to'])->addDay()->format('Y-m-d 00:00:00'));
|
||||
|
||||
if ($filters['work_order_no'] !== '') {
|
||||
$query->whereRaw("p.woNo LIKE ? ESCAPE '!'", [$this->contains($filters['work_order_no'])]);
|
||||
@ -249,16 +260,31 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
$query->whereRaw("p.shop_name LIKE ? ESCAPE '!'", [$this->contains($filters['shop_name'])]);
|
||||
}
|
||||
if ($filters['transaction_no'] !== '') {
|
||||
$query->whereRaw("p.transaction_no LIKE ? ESCAPE '!'", [$this->contains($filters['transaction_no'])]);
|
||||
$query->whereRaw("w.businessFieldList LIKE ? ESCAPE '!'", [$this->contains($filters['transaction_no'])]);
|
||||
}
|
||||
if ($filters['online_order_no'] !== '') {
|
||||
$query->whereRaw("w.businessFieldList LIKE ? ESCAPE '!'", [$this->contains($filters['online_order_no'])]);
|
||||
$query->where(function (Builder $onlineOrder) use ($filters): void {
|
||||
$needle = $this->contains($filters['online_order_no']);
|
||||
$onlineOrder->whereRaw("p.transaction_no LIKE ? ESCAPE '!'", [$needle])
|
||||
->orWhereRaw("w.businessFieldList LIKE ? ESCAPE '!'", [$needle]);
|
||||
});
|
||||
}
|
||||
if ($filters['status'] !== '') {
|
||||
$query->where(function (Builder $status) use ($filters): void {
|
||||
$status->where('w.status', $filters['status'])
|
||||
->orWhere('p.status', $filters['status'])
|
||||
$statuses = $this->workOrderStatusesForFilter($filters['status']);
|
||||
$status->whereIn('w.status', $statuses)
|
||||
->orWhereIn('p.status', $statuses)
|
||||
->orWhereRaw("w.businessFieldList LIKE ? ESCAPE '!'", [$this->contains($filters['status'])]);
|
||||
|
||||
$paymentMethod = $this->paymentMethodForStatus($filters['status']);
|
||||
if ($paymentMethod !== null || trim($filters['status']) === '已打款') {
|
||||
$status->orWhere(function (Builder $payment) use ($paymentMethod): void {
|
||||
$payment->whereIn('w.status', self::COMPLETED_WORK_ORDER_STATUSES);
|
||||
if ($paymentMethod !== null) {
|
||||
$this->applyPaymentMethodFilter($payment, $paymentMethod);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($filters['payment_reason'] !== '') {
|
||||
@ -270,20 +296,32 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
private function decorate(array $row): array
|
||||
{
|
||||
$fields = $this->businessFields($row['business_field_list'] ?? null);
|
||||
|
||||
$row['work_order_status'] = $this->firstFilled($row['work_order_status'] ?? null, $row['mapped_status'] ?? null);
|
||||
$row['work_order_title'] = $this->firstFilled($row['work_order_title'] ?? null, $row['mapped_title'] ?? null);
|
||||
$row['work_order_type'] = $this->field($fields, ['工单类型']) ?: '小额打款';
|
||||
$row['payment_status'] = $this->firstFilled(
|
||||
$this->field($fields, ['打款状态', '支付打款状态', '付款状态']),
|
||||
$sourceWorkOrderStatus = $this->firstFilled(
|
||||
$row['work_order_status'] ?? null,
|
||||
$row['mapped_status'] ?? null,
|
||||
);
|
||||
|
||||
$row['work_order_status'] = $this->workOrderStatusLabel($sourceWorkOrderStatus);
|
||||
$row['work_order_title'] = $this->firstFilled($row['work_order_title'] ?? null, $row['mapped_title'] ?? null);
|
||||
$row['work_order_type'] = $this->field($fields, ['工单类型']) ?: '小额打款';
|
||||
$paymentMethod = $this->field($fields, [
|
||||
'打款方式', '收款方式', '支付方式', 'SELECTField1413856620250724113227',
|
||||
]);
|
||||
$row['payment_status'] = $this->firstFilled(
|
||||
$this->field($fields, ['打款状态', '支付打款状态', '付款状态']),
|
||||
$this->completedPaymentStatus($sourceWorkOrderStatus, $paymentMethod),
|
||||
'未提供',
|
||||
);
|
||||
$row['alipay_batch_no'] = $this->field($fields, [
|
||||
'支付宝批次号', '支付宝打款批次号', '打款批次号', '批次号', 'alipayBatchNo', 'batchNo',
|
||||
]);
|
||||
$row['payment_amount'] = $this->money($this->field($fields, [
|
||||
'打款金额', '付款金额', '小额打款金额', 'amountField',
|
||||
]));
|
||||
$row['online_order_no'] = $this->field($fields, [
|
||||
'线上订单号', '线上单号', '订单号', 'orderField',
|
||||
]);
|
||||
$row['online_order_no'] = $this->firstFilled(
|
||||
$this->field($fields, ['线上订单号', '线上单号', '订单号', 'orderField']),
|
||||
$row['mapped_online_order_no'] ?? null,
|
||||
);
|
||||
$row['buyer_account'] = $this->field($fields, [
|
||||
'买家账号', '买家帐号', '买家', 'buyerIdField',
|
||||
]);
|
||||
@ -306,7 +344,12 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
$row['payment_time'] = $this->field($fields, [
|
||||
'支付时间', '打款时间', '付款时间', 'payDateField',
|
||||
]);
|
||||
unset($row['business_field_list'], $row['mapped_status'], $row['mapped_title']);
|
||||
unset(
|
||||
$row['business_field_list'],
|
||||
$row['mapped_online_order_no'],
|
||||
$row['mapped_status'],
|
||||
$row['mapped_title'],
|
||||
);
|
||||
|
||||
return $row;
|
||||
}
|
||||
@ -443,6 +486,63 @@ class SmallPaymentWorkOrderMirrorRepository
|
||||
return '';
|
||||
}
|
||||
|
||||
private function workOrderStatusLabel(string $status): string
|
||||
{
|
||||
$code = strtoupper(trim($status));
|
||||
|
||||
return self::WORK_ORDER_STATUS_LABELS[$code] ?? $status;
|
||||
}
|
||||
|
||||
private function completedPaymentStatus(string $workOrderStatus, string $paymentMethod): string
|
||||
{
|
||||
$normalizedStatus = strtoupper(trim($workOrderStatus));
|
||||
if (! in_array($normalizedStatus, self::COMPLETED_WORK_ORDER_STATUSES, true)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (str_contains($paymentMethod, '支付宝')) {
|
||||
return '支付宝已打款';
|
||||
}
|
||||
if (str_contains($paymentMethod, '银行卡')) {
|
||||
return '银行卡已打款';
|
||||
}
|
||||
|
||||
return '已打款';
|
||||
}
|
||||
|
||||
private function paymentMethodForStatus(string $status): ?string
|
||||
{
|
||||
return match (trim($status)) {
|
||||
'支付宝已打款' => '支付宝账号',
|
||||
'银行卡已打款' => '银行卡',
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function applyPaymentMethodFilter(Builder $query, string $paymentMethod): void
|
||||
{
|
||||
$query->where(function (Builder $fields) use ($paymentMethod): void {
|
||||
foreach (['value', 'fieldValue', 'valueLabel'] as $key) {
|
||||
$fields->orWhereRaw(
|
||||
"w.businessFieldList LIKE ? ESCAPE '!'",
|
||||
[$this->contains(sprintf('"%s":"%s"', $key, $paymentMethod))],
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function workOrderStatusesForFilter(string $status): array
|
||||
{
|
||||
$status = trim($status);
|
||||
$code = array_search($status, self::WORK_ORDER_STATUS_LABELS, true);
|
||||
|
||||
return array_values(array_unique(array_filter([
|
||||
$status,
|
||||
is_string($code) ? $code : null,
|
||||
], static fn (?string $value): bool => $value !== null && $value !== '')));
|
||||
}
|
||||
|
||||
private function contains(string $value): string
|
||||
{
|
||||
return '%' . str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $value) . '%';
|
||||
|
||||
@ -116,15 +116,15 @@
|
||||
<form
|
||||
class="upstream-data-toolbar"
|
||||
method="GET"
|
||||
action="{{ route('business.pages.show', ['section' => 'refunds', 'page' => 'small-payment-work-orders']) }}"
|
||||
action="{{ route('business.pages.show', ['section' => 'financial-operations', 'page' => 'small-payment-work-orders']) }}"
|
||||
data-filter-contract="small-payment-work-orders.filters"
|
||||
>
|
||||
<div class="upstream-data-field">
|
||||
<label for="small-payment-from">发起开始日期</label>
|
||||
<label for="small-payment-from">完成开始日期</label>
|
||||
<input id="small-payment-from" type="date" name="from" value="{{ $filters['from'] }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="small-payment-to">发起结束日期</label>
|
||||
<label for="small-payment-to">完成结束日期</label>
|
||||
<input id="small-payment-to" type="date" name="to" value="{{ $filters['to'] }}">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
@ -145,7 +145,7 @@
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="small-payment-status">工单/打款状态</label>
|
||||
<input id="small-payment-status" type="search" name="status" value="{{ $filters['status'] }}" placeholder="例如 已完成">
|
||||
<input id="small-payment-status" type="search" name="status" value="{{ $filters['status'] }}" placeholder="例如 已完成/支付宝已打款">
|
||||
</div>
|
||||
<div class="upstream-data-field">
|
||||
<label for="small-payment-reason">打款原因</label>
|
||||
@ -156,14 +156,14 @@
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a class="upstream-data-button" href="{{ route('business.pages.show', ['section' => 'refunds', 'page' => 'small-payment-work-orders']) }}">
|
||||
<a class="upstream-data-button" href="{{ route('business.pages.show', ['section' => 'financial-operations', 'page' => 'small-payment-work-orders']) }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
<a
|
||||
class="upstream-data-button finance-table-fallback-export {{ $rows->total() === 0 || $smallPaymentError || $smallPaymentPending || $smallPaymentIndexPending ? 'is-disabled' : '' }}"
|
||||
data-finance-fallback-export
|
||||
href="{{ $rows->total() === 0 || $smallPaymentError || $smallPaymentPending || $smallPaymentIndexPending ? '#' : route('business.refunds.small-payment-work-orders.export', $filters) }}"
|
||||
href="{{ $rows->total() === 0 || $smallPaymentError || $smallPaymentPending || $smallPaymentIndexPending ? '#' : route('business.financial-operations.small-payment-work-orders.export', $filters) }}"
|
||||
@if ($rows->total() === 0 || $smallPaymentError || $smallPaymentPending || $smallPaymentIndexPending) aria-disabled="true" @endif
|
||||
>
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
@ -188,7 +188,7 @@
|
||||
<table
|
||||
class="upstream-data-table small-payment-table small-payment-table--wide"
|
||||
aria-label="聚水潭小额打款工单"
|
||||
data-finance-export-base="{{ route('business.refunds.small-payment-work-orders.export') }}"
|
||||
data-finance-export-base="{{ route('business.financial-operations.small-payment-work-orders.export') }}"
|
||||
data-finance-export-query="{{ e(json_encode($filters, JSON_UNESCAPED_UNICODE)) }}"
|
||||
data-finance-export-disabled="{{ $rows->total() === 0 || $smallPaymentError || $smallPaymentPending || $smallPaymentIndexPending ? 'true' : 'false' }}"
|
||||
>
|
||||
|
||||
@ -130,8 +130,12 @@ 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/financial-operations/small-payment-work-orders/export', 'Business\Pages@exportSmallPaymentWorkOrders')
|
||||
->middleware('business.page:financial-operations,small-payment-work-orders')
|
||||
->name('business.financial-operations.small-payment-work-orders.export');
|
||||
|
||||
Route::get('workspace/refunds/small-payment-work-orders/export', 'Business\Pages@exportSmallPaymentWorkOrders')
|
||||
->middleware('business.page:refunds,small-payment-work-orders')
|
||||
->middleware('business.page:financial-operations,small-payment-work-orders')
|
||||
->name('business.refunds.small-payment-work-orders.export');
|
||||
|
||||
Route::get('workspace/courier-payables/courier-information/export', 'Business\Pages@exportCourierInformation')
|
||||
|
||||
@ -65,7 +65,7 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
|
||||
$url = route('business.pages.show', [
|
||||
'company_id' => $this->company->id,
|
||||
'section' => 'refunds', 'page' => 'small-payment-work-orders',
|
||||
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
|
||||
]) . '?' . http_build_query([
|
||||
'from' => '2026-09-01', 'to' => '2026-09-03', 'work_order_no' => 'GD-001',
|
||||
'shop_name' => '京东旗舰店', 'transaction_no' => 'BATCH-001',
|
||||
@ -79,13 +79,15 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
->assertSee('data-filter-contract="small-payment-work-orders.filters"', false)
|
||||
->assertSee('data-sync-state="syncing"', false)
|
||||
->assertSeeText('业务镜像同步中,当前显示最近成功数据')
|
||||
->assertSeeText('完成开始日期')
|
||||
->assertSeeText('完成结束日期')
|
||||
->assertSee('aria-label="聚水潭小额打款工单"', false)
|
||||
->assertSeeText('GD-001')
|
||||
->assertSeeText('支付宝已打款')
|
||||
->assertSeeText('¥ 50.00')
|
||||
->assertSeeText('共 26 条记录,本页 1 条')
|
||||
->assertSeeText('第 2 / 2 页')
|
||||
->assertSee('workspace/refunds/small-payment-work-orders/export', false)
|
||||
->assertSee('workspace/financial-operations/small-payment-work-orders/export', false)
|
||||
->assertSee('data-finance-export-base=', false)
|
||||
->assertSee('data-export-field="work_order_no"', false)
|
||||
->assertSee('value="ORDER-001"', false);
|
||||
@ -115,7 +117,7 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
]]);
|
||||
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.refunds.small-payment-work-orders.export', [
|
||||
$response = $this->loginAs()->get(route('business.financial-operations.small-payment-work-orders.export', [
|
||||
'from' => '2026-09-01', 'to' => '2026-09-03',
|
||||
]));
|
||||
|
||||
@ -146,7 +148,7 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'refunds', 'page' => 'small-payment-work-orders',
|
||||
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('业务镜像正在执行首次同步')
|
||||
@ -168,7 +170,7 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
]]);
|
||||
$this->app->instance(SmallPaymentWorkOrderMirrorRepository::class, $repository);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.refunds.small-payment-work-orders.export', [
|
||||
$response = $this->loginAs()->get(route('business.financial-operations.small-payment-work-orders.export', [
|
||||
'columns' => 'shop_name,work_order_no,not_a_real_field,shop_name',
|
||||
]));
|
||||
|
||||
@ -190,7 +192,24 @@ class SmallPaymentWorkOrderPageTest extends FeatureTestCase
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$this->withExceptionHandling()->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'refunds', 'page' => 'small-payment-work-orders',
|
||||
'section' => 'financial-operations', 'page' => 'small-payment-work-orders',
|
||||
]))->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_legacy_refund_url_redirects_to_financial_operations_and_preserves_filters(): void
|
||||
{
|
||||
$this->grantAdminPermission();
|
||||
|
||||
$this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'refunds',
|
||||
'page' => 'small-payment-work-orders',
|
||||
'from' => '2026-09-01',
|
||||
'shop_name' => '测试店铺',
|
||||
]))->assertRedirect(route('business.pages.show', [
|
||||
'section' => 'financial-operations',
|
||||
'page' => 'small-payment-work-orders',
|
||||
'from' => '2026-09-01',
|
||||
'shop_name' => '测试店铺',
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ class NavigationTest extends FeatureTestCase
|
||||
$receivablesIndex = array_search('store-receivables', $slugs, true);
|
||||
$orderData = $navigation[$orderIndex] ?? null;
|
||||
$refund = $navigation[$refundIndex] ?? null;
|
||||
$financialOperations = collect($navigation)->firstWhere('slug', 'financial-operations');
|
||||
|
||||
$this->assertNotFalse($invoiceIndex);
|
||||
$this->assertNotFalse($fundIndex);
|
||||
@ -125,13 +126,20 @@ class NavigationTest extends FeatureTestCase
|
||||
$this->assertSame('退款数据', $refund['title']);
|
||||
$this->assertSame('assignment_return', $refund['icon']);
|
||||
$this->assertSame(
|
||||
['jingxi-refund-data', 'non-jingxi-refund-data', 'small-payment-work-orders'],
|
||||
['jingxi-refund-data', 'non-jingxi-refund-data'],
|
||||
array_column($refund['pages'], 'slug')
|
||||
);
|
||||
$this->assertSame(
|
||||
['京喜退款数据', '售后(退货退款)', '小额打款'],
|
||||
['京喜退款数据', '售后(退货退款)'],
|
||||
array_column($refund['pages'], 'title')
|
||||
);
|
||||
$this->assertNotNull($financialOperations);
|
||||
$smallPayment = collect($financialOperations['pages'])->firstWhere('slug', 'small-payment-work-orders');
|
||||
$this->assertSame('小额打款', $smallPayment['title'] ?? null);
|
||||
$this->assertGreaterThan(
|
||||
array_search('after-sales-expenses', array_column($financialOperations['pages'], 'slug'), true),
|
||||
array_search('small-payment-work-orders', array_column($financialOperations['pages'], 'slug'), true)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -128,6 +128,53 @@ class UserPermissionsTest extends FeatureTestCase
|
||||
$this->assertSame(['subject-trademark-rules'], $pages->pluck('slug')->all());
|
||||
}
|
||||
|
||||
public function test_legacy_small_payment_permission_follows_the_page_to_financial_operations(): void
|
||||
{
|
||||
$regular = $this->account('user');
|
||||
$subject = 'user_'.$regular->id;
|
||||
setting()->set('system.sidebar_permissions', [
|
||||
'configured' => [$subject],
|
||||
'subjects' => [$subject => ['refunds/small-payment-work-orders']],
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
['financial-operations/small-payment-work-orders'],
|
||||
SidebarPermissions::allowedKeys($regular)
|
||||
);
|
||||
|
||||
$navigation = SidebarPermissions::navigationFor($regular);
|
||||
$this->assertSame(['financial-operations'], array_column($navigation, 'slug'));
|
||||
$this->assertSame(
|
||||
['small-payment-work-orders'],
|
||||
array_column($navigation[0]['pages'], 'slug')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_legacy_refunds_section_permission_keeps_small_payment_access(): void
|
||||
{
|
||||
$regular = $this->account('user');
|
||||
$subject = 'user_'.$regular->id;
|
||||
setting()->set('system.sidebar_permissions', [
|
||||
'configured' => [$subject],
|
||||
'subjects' => [$subject => ['refunds']],
|
||||
]);
|
||||
|
||||
$this->assertSame(
|
||||
['refunds', 'financial-operations/small-payment-work-orders'],
|
||||
SidebarPermissions::allowedKeys($regular)
|
||||
);
|
||||
|
||||
$navigation = SidebarPermissions::navigationFor($regular);
|
||||
$this->assertSame(
|
||||
['refunds', 'financial-operations'],
|
||||
array_column($navigation, 'slug')
|
||||
);
|
||||
$this->assertSame(
|
||||
['small-payment-work-orders'],
|
||||
array_column($navigation[1]['pages'], 'slug')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_dingtalk_bound_user_cannot_be_deleted_into_an_orphaned_identity(): void
|
||||
{
|
||||
$target = $this->account('user');
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SmallPaymentCompletionIndexMigrationTest extends TestCase
|
||||
{
|
||||
public function test_indexes_are_idempotent_and_reversible_without_changing_rows(): void
|
||||
{
|
||||
config([
|
||||
'database.connections.small_payment_index_fixture' => [
|
||||
'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '',
|
||||
],
|
||||
'upstream_mirrors.connection' => 'small_payment_index_fixture',
|
||||
'system_database.mode' => 'shared',
|
||||
]);
|
||||
$schema = Schema::connection('small_payment_index_fixture');
|
||||
$db = DB::connection('small_payment_index_fixture');
|
||||
$schema->create('js_jst_work_order_info', static function (Blueprint $table): void {
|
||||
$table->integer('Id');
|
||||
$table->string('woTypeId');
|
||||
$table->string('woNo');
|
||||
$table->dateTime('completeTime')->nullable();
|
||||
});
|
||||
$schema->create('js_jst_small_payment_order_map', static function (Blueprint $table): void {
|
||||
$table->string('woTypeId');
|
||||
$table->string('woNo');
|
||||
});
|
||||
$db->table('js_jst_work_order_info')->insert([
|
||||
'Id' => 1, 'woTypeId' => 'TYPE', 'woNo' => 'GD-001', 'completeTime' => '2026-09-11 10:00:00',
|
||||
]);
|
||||
$db->table('js_jst_small_payment_order_map')->insert(['woTypeId' => 'TYPE', 'woNo' => 'GD-001']);
|
||||
|
||||
$migration = require base_path('database/migrations/2026_09_11_110000_index_small_payment_completion_queries.php');
|
||||
$migration->up();
|
||||
$migration->up();
|
||||
|
||||
$this->assertTrue($schema->hasIndex('js_jst_work_order_info', 'idx_small_payment_completion'));
|
||||
$this->assertTrue($schema->hasIndex('js_jst_small_payment_order_map', 'idx_small_payment_order_map'));
|
||||
$this->assertSame(1, $db->table('js_jst_work_order_info')->count());
|
||||
$this->assertSame(1, $db->table('js_jst_small_payment_order_map')->count());
|
||||
|
||||
$migration->down();
|
||||
$this->assertFalse($schema->hasIndex('js_jst_work_order_info', 'idx_small_payment_completion'));
|
||||
$this->assertFalse($schema->hasIndex('js_jst_small_payment_order_map', 'idx_small_payment_order_map'));
|
||||
DB::purge('small_payment_index_fixture');
|
||||
}
|
||||
}
|
||||
@ -29,12 +29,15 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
SmallPaymentWorkOrderMirrorRepository::WORK_ORDER_DATASET => [
|
||||
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_workOrderInfo',
|
||||
'target' => 'fixture_work_orders', 'keys' => ['Id'], 'watermark' => 'updateTime',
|
||||
'indexes' => [['woTypeId', 'woNo', 'Id']],
|
||||
'indexes' => [
|
||||
['woTypeId', 'woNo', 'Id'],
|
||||
['woTypeId', 'completeTime', 'woNo', 'Id'],
|
||||
],
|
||||
],
|
||||
SmallPaymentWorkOrderMirrorRepository::PAYMENT_MAP_DATASET => [
|
||||
'source_key' => 'jiansun', 'schema' => 'dbo', 'object' => 'jst_small_payment_order_map',
|
||||
'target' => 'fixture_payment_map', 'keys' => ['woNo'], 'watermark' => 'updated_at',
|
||||
'indexes' => [['woTypeId', 'work_order_create_time', 'woNo']],
|
||||
'indexes' => [['woTypeId', 'woNo']],
|
||||
],
|
||||
],
|
||||
]);
|
||||
@ -54,12 +57,13 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
}
|
||||
$table->integer('urging')->nullable();
|
||||
$table->index(['woTypeId', 'woNo', 'Id']);
|
||||
$table->index(['woTypeId', 'completeTime', 'woNo', 'Id']);
|
||||
});
|
||||
$schema->create('fixture_payment_map', function (Blueprint $table): void {
|
||||
foreach (['woNo', 'woTypeId', 'transaction_no', 'shop_name', 'payment_reason', 'title', 'status', 'work_order_create_time', 'work_order_update_time', 'parse_status', 'parse_error', 'updated_at'] as $column) {
|
||||
$table->text($column)->nullable();
|
||||
}
|
||||
$table->index(['woTypeId', 'work_order_create_time', 'woNo']);
|
||||
$table->index(['woTypeId', 'woNo']);
|
||||
});
|
||||
|
||||
foreach (array_keys(config('upstream_mirrors.datasets')) as $key) {
|
||||
@ -80,15 +84,15 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
$workOrderRows = [
|
||||
[
|
||||
'Id' => 1, 'woNo' => 'GD-001', 'woTypeId' => $type,
|
||||
'title' => '小额打款一', 'content' => '售后补偿', 'status' => '已完成',
|
||||
'title' => '小额打款一', 'content' => '售后补偿', 'status' => 'CONFIRMED',
|
||||
'grade' => '普通', 'urging' => 1, 'createby' => '客服甲', 'executors' => '财务甲',
|
||||
'completer' => '财务甲', 'completeTime' => '2026-09-02 11:00:00',
|
||||
'created' => '2026-09-02 09:00:00', 'updated' => '2026-09-02 11:00:00',
|
||||
'updateTime' => '2026-09-02 11:01:00',
|
||||
'businessFieldList' => json_encode([
|
||||
['fieldName' => '打款状态', 'fieldValue' => '支付宝已打款'],
|
||||
['fieldId' => 'SELECTField1413856620250724113227', 'value' => '支付宝账号'],
|
||||
['fieldName' => '支付宝批次号', 'fieldValue' => 'BATCH-001'],
|
||||
['fieldName' => '打款金额', 'fieldValue' => '50.005'],
|
||||
['fieldName' => '线上订单号', 'fieldValue' => 'ORDER-001'],
|
||||
['fieldName' => '买家账号', 'fieldValue' => 'buyer-one'],
|
||||
['fieldName' => '支付宝账号', 'fieldValue' => 'account-one'],
|
||||
['fieldName' => '支付宝姓名', 'fieldValue' => '测试买家'],
|
||||
@ -101,11 +105,13 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
],
|
||||
[
|
||||
'Id' => 2, 'woNo' => 'GD-002', 'woTypeId' => $type,
|
||||
'title' => '小额打款二', 'content' => '少收多收', 'status' => '处理中',
|
||||
'title' => '小额打款二', 'content' => '少收多收', 'status' => 'CONFIRMED',
|
||||
'grade' => '紧急', 'urging' => 0, 'createby' => '客服乙',
|
||||
'created' => '2026-09-03 09:00:00', 'updated' => '2026-09-03 09:30:00',
|
||||
'completer' => '财务乙', 'completeTime' => '2026-09-03 10:00:00',
|
||||
'created' => '2026-08-31 09:00:00', 'updated' => '2026-09-03 10:00:00',
|
||||
'updateTime' => '2026-09-03 09:31:00',
|
||||
'businessFieldList' => json_encode([
|
||||
['fieldId' => 'SELECTField1413856620250724113227', 'value' => '银行卡'],
|
||||
['fieldId' => 'shopField', 'type' => 'SELECT', 'value' => '18526685', 'valueLabel' => '天猫-亿家达旗舰店'],
|
||||
['fieldId' => 'alipayAccountField', 'type' => 'TEXT', 'value' => 'account-two'],
|
||||
['fieldId' => 'alipayNameField', 'type' => 'TEXT', 'value' => '测试买家二'],
|
||||
@ -134,16 +140,16 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
));
|
||||
DB::connection('business_mirror')->table('fixture_payment_map')->insert([
|
||||
[
|
||||
'woNo' => 'GD-001', 'woTypeId' => $type, 'transaction_no' => 'BATCH-001',
|
||||
'woNo' => 'GD-001', 'woTypeId' => $type, 'transaction_no' => 'ORDER-001',
|
||||
'shop_name' => '京东旗舰店', 'payment_reason' => '售后补偿', 'title' => '小额打款一',
|
||||
'status' => '已完成', 'work_order_create_time' => '2026-09-02 09:00:00',
|
||||
'work_order_update_time' => '2026-09-02 11:00:00', 'parse_status' => 'success',
|
||||
'updated_at' => '2026-09-02 11:01:00',
|
||||
],
|
||||
[
|
||||
'woNo' => 'GD-002', 'woTypeId' => $type, 'transaction_no' => 'BATCH-002',
|
||||
'woNo' => 'GD-002', 'woTypeId' => $type, 'transaction_no' => 'ORDER-002',
|
||||
'shop_name' => '天猫旗舰店', 'payment_reason' => '少收多收', 'title' => '小额打款二',
|
||||
'status' => '处理中', 'work_order_create_time' => '2026-09-03 09:00:00',
|
||||
'status' => 'CONFIRMED', 'work_order_create_time' => '2026-08-31 09:00:00',
|
||||
'work_order_update_time' => '2026-09-03 09:30:00', 'parse_status' => 'success',
|
||||
'updated_at' => '2026-09-03 09:31:00',
|
||||
],
|
||||
@ -173,6 +179,8 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('50.01', $result['page_payment_amount']);
|
||||
$this->assertSame('GD-001', $result['data'][0]['work_order_no']);
|
||||
$this->assertSame('BATCH-001', $result['data'][0]['alipay_batch_no']);
|
||||
$this->assertSame('已完成', $result['data'][0]['work_order_status']);
|
||||
$this->assertSame('小额打款', $result['data'][0]['work_order_type']);
|
||||
$this->assertSame('支付宝已打款', $result['data'][0]['payment_status']);
|
||||
$this->assertSame('ORDER-001', $result['data'][0]['online_order_no']);
|
||||
@ -193,23 +201,44 @@ class SmallPaymentWorkOrderMirrorRepositoryTest extends TestCase
|
||||
public function test_export_uses_same_filters_and_parsed_shape(): void
|
||||
{
|
||||
$rows = iterator_to_array($this->repository->exportRows($this->filters([
|
||||
'transaction_no' => 'BATCH-002',
|
||||
'payment_reason' => '少收',
|
||||
'transaction_no' => 'BATCH-001',
|
||||
'payment_reason' => '售后',
|
||||
]), 1));
|
||||
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame('GD-002', $rows[0]['work_order_no']);
|
||||
$this->assertSame('20.00', $rows[0]['payment_amount']);
|
||||
$this->assertSame('ORDER-002', $rows[0]['online_order_no']);
|
||||
$this->assertSame('buyer-two', $rows[0]['buyer_account']);
|
||||
$this->assertSame('account-two', $rows[0]['alipay_account']);
|
||||
$this->assertSame('测试买家二', $rows[0]['alipay_name']);
|
||||
$this->assertSame('ITEM-002', $rows[0]['product_code']);
|
||||
$this->assertSame('SKU-002', $rows[0]['sku_code']);
|
||||
$this->assertSame('淘宝天猫', $rows[0]['platform']);
|
||||
$this->assertSame('188.88', $rows[0]['actual_paid_amount']);
|
||||
$this->assertSame('2026-09-03 08:30:00', $rows[0]['payment_time']);
|
||||
$this->assertSame('处理中', $rows[0]['payment_status']);
|
||||
$this->assertSame('GD-001', $rows[0]['work_order_no']);
|
||||
$this->assertSame('BATCH-001', $rows[0]['alipay_batch_no']);
|
||||
$this->assertSame('ORDER-001', $rows[0]['online_order_no']);
|
||||
$this->assertSame('已完成', $rows[0]['work_order_status']);
|
||||
$this->assertSame('支付宝已打款', $rows[0]['payment_status']);
|
||||
}
|
||||
|
||||
public function test_completion_date_and_chinese_status_follow_upstream_semantics(): void
|
||||
{
|
||||
$result = $this->repository->paginate($this->filters([
|
||||
'from' => '2026-09-03',
|
||||
'to' => '2026-09-03',
|
||||
'status' => '已完成',
|
||||
]), 1, 25);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('GD-002', $result['data'][0]['work_order_no']);
|
||||
$this->assertSame('2026-08-31 09:00:00', $result['data'][0]['work_order_created_at']);
|
||||
$this->assertSame('2026-09-03 10:00:00', $result['data'][0]['completed_at']);
|
||||
$this->assertSame('已完成', $result['data'][0]['work_order_status']);
|
||||
$this->assertSame('银行卡已打款', $result['data'][0]['payment_status']);
|
||||
$this->assertSame('', $result['data'][0]['alipay_batch_no']);
|
||||
}
|
||||
|
||||
public function test_derived_payment_status_filter_matches_payment_method_field(): void
|
||||
{
|
||||
$result = $this->repository->paginate($this->filters([
|
||||
'status' => '支付宝已打款',
|
||||
]), 1, 25);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('GD-001', $result['data'][0]['work_order_no']);
|
||||
$this->assertSame('支付宝已打款', $result['data'][0]['payment_status']);
|
||||
}
|
||||
|
||||
public function test_unready_state_is_explicit(): void
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user