diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index f028630b..0b142a08 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -27,6 +27,7 @@ use Modules\EcommerceFinance\Exceptions\UpstreamSnapshotPendingException; use Modules\EcommerceFinance\Repositories\AlibabaBillRepository; use Modules\EcommerceFinance\Repositories\BillOffsetMirrorRepository; use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository; +use Modules\EcommerceFinance\Repositories\CourierFreightReconciliationMirrorRepository; use Modules\EcommerceFinance\Repositories\EdgeBandingRepository; use Modules\EcommerceFinance\Repositories\InventoryRepository; use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository; @@ -74,6 +75,7 @@ class Pages extends Controller private readonly ProductCatalogMirrorRepository $productCatalog, private readonly ProductPriceMirrorRepository $productPrices, private readonly CourierInformationMirrorRepository $courierInformation, + private readonly CourierFreightReconciliationMirrorRepository $courierFreightReconciliations, private readonly RawBoardPayablesRepository $rawBoardPayables, private readonly DingPayableWorkflowMirrorRepository $dingPayableWorkflows, private readonly SupplierPayableReportRepository $supplierPayableReports, @@ -573,6 +575,7 @@ class Pages extends Controller 'normal-products' => $this->contentForNormalProducts(), 'product-prices' => $this->contentForProductPrices(), 'courier-information' => $this->contentForCourierInformation(), + 'freight-reconciliation' => $this->contentForFreightReconciliation(), 'board-deductions' => $this->contentForBoardDeductions(), 'supplier-payables-report' => $this->contentForSupplierPayablesReport(), 'payable-cloud-warehouse-vehicle-fee' => $this->contentForDingPayableWorkflows(), @@ -613,6 +616,71 @@ class Pages extends Controller ]; } + protected function contentForFreightReconciliation(): array + { + $request = request(); + $date = static function (mixed $value): string { + $value = trim((string) $value); + return preg_match('/^\d{4}-\d{2}-\d{2}$/', $value) === 1 ? $value : ''; + }; + $filters = [ + 'express_no' => mb_substr(trim((string) $request->query('express_no', '')), 0, 80), + 'express_name' => mb_substr(trim((string) $request->query('express_name', '')), 0, 80), + 'month' => mb_substr(trim((string) $request->query('month', '')), 0, 20), + 'status' => mb_substr(trim((string) $request->query('status', '')), 0, 40), + 'modified_from' => $date($request->query('modified_from', '')), + 'modified_to' => $date($request->query('modified_to', '')), + ]; + $page = max((int) $request->query('page', 1), 1); + $limit = min(max((int) setting('default.list_limit', 25), 1), 100); + $rows = new LengthAwarePaginator([], 0, $limit, $page, [ + 'path' => $request->url(), + 'query' => $request->except('page'), + ]); + $state = null; + $sync = ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null]; + $summary = ['count' => 0, 'payable_amount' => 0.0, 'actual_payment' => 0.0, 'confirmed_amount' => 0.0, 'reconciliation_difference' => 0.0]; + $error = null; + $pending = false; + + try { + if (! $this->courierFreightReconciliations->available()) { + $error = '共享业务镜像尚未建立,运费对账暂时无法读取。'; + } elseif (! $this->courierFreightReconciliations->ready()) { + $state = $this->courierFreightReconciliations->state(); + $sync = $this->courierFreightReconciliations->syncStatus($state); + $pending = true; + $error = '运费对账镜像正在完成首次同步,完成前不会把未同步数据误判为无数据。'; + } else { + $result = $this->courierFreightReconciliations->paginate($filters, $page, $limit); + $rows = new LengthAwarePaginator( + Collection::make($result['data'] ?? []), + (int) ($result['total'] ?? 0), + $limit, + $page, + ['path' => $request->url(), 'query' => $request->except('page')], + ); + $state = $result['state'] ?? null; + $sync = $result['sync'] ?? $sync; + $summary = $result['summary'] ?? $summary; + } + } catch (Throwable $exception) { + report($exception); + $error = '运费对账数据暂时无法读取,请稍后重试。'; + } + + return [ + 'view' => 'business.pages.courier-payables.freight-reconciliation', + 'freightReconciliations' => $rows, + 'freightReconciliationFilters' => $filters, + 'freightReconciliationSummary' => $summary, + 'freightReconciliationState' => $state, + 'freightReconciliationSync' => $sync, + 'freightReconciliationError' => $error, + 'freightReconciliationPending' => $pending, + ]; + } + protected function contentForDingPayableWorkflows(): array { $normalizeDate = static function (mixed $value): string { diff --git a/app/Http/Controllers/Settings/Navigation.php b/app/Http/Controllers/Settings/Navigation.php index 28c1f269..2ded2fc8 100644 --- a/app/Http/Controllers/Settings/Navigation.php +++ b/app/Http/Controllers/Settings/Navigation.php @@ -13,7 +13,7 @@ class Navigation extends Controller parent::__construct(); $this->middleware(function ($request, $next) { - abort_unless(user()?->hasRole('admin'), 403); + abort_unless(\App\Utilities\SystemAccess::canOpenSettings(), 403); return $next($request); }); diff --git a/app/Http/Controllers/SystemSettings/Sidebar.php b/app/Http/Controllers/SystemSettings/Sidebar.php index 68328ebd..819180e6 100644 --- a/app/Http/Controllers/SystemSettings/Sidebar.php +++ b/app/Http/Controllers/SystemSettings/Sidebar.php @@ -21,6 +21,11 @@ class Sidebar extends Controller }); } + public function index() + { + return view('system_settings.sidebar.index'); + } + public function edit(Request $request) { $subjects = $this->subjects(); @@ -63,7 +68,7 @@ class Sidebar extends Controller flash('保存成功,侧边栏权限已更新。')->success(); - return redirect()->route('dashboard'); + return redirect()->route('system-settings.sidebar.edit', ['subject' => $subject]); } private function subjects(): array @@ -72,7 +77,6 @@ class Sidebar extends Controller if (SystemAccess::isSuperAdmin()) { $subjects = [ - 'role_super_admin' => '用户类型:' . $labels[SystemAccess::SUPER_ADMIN], 'role_admin' => '用户类型:' . $labels[SystemAccess::ADMIN], 'role_user' => '用户类型:' . $labels[SystemAccess::USER], ]; @@ -94,6 +98,9 @@ class Sidebar extends Controller foreach ($users as $managedUser) { $role = SystemAccess::roleName($managedUser); + if ($role === SystemAccess::SUPER_ADMIN) { + continue; + } $subjects['user_' . $managedUser->id] = '指定用户:' . $managedUser->name . ' - ' . ($labels[$role] ?? $role); } diff --git a/app/Http/Controllers/SystemSettings/Users.php b/app/Http/Controllers/SystemSettings/Users.php index e6a78333..d235de0c 100644 --- a/app/Http/Controllers/SystemSettings/Users.php +++ b/app/Http/Controllers/SystemSettings/Users.php @@ -87,12 +87,14 @@ class Users extends Controller $role = Role::where('name', $data['role'])->firstOrFail(); if ( - $managedUser->id === user()->id - && SystemAccess::isSuperAdmin($managedUser) - && $data['role'] !== SystemAccess::SUPER_ADMIN + SystemAccess::isSuperAdmin($managedUser) + && $managedUser->enabled + && ($data['role'] !== SystemAccess::SUPER_ADMIN || ! ($data['enabled'] ?? false)) && $this->superAdminCount() <= 1 ) { - return back()->withErrors(['role' => '系统至少需要保留一名超级管理员。'])->withInput(); + return back()->withErrors([ + $data['role'] !== SystemAccess::SUPER_ADMIN ? 'role' : 'enabled' => '系统至少需要保留一名启用的主管理员。', + ])->withInput(); } if ($managedUser->id === user()->id && ! ($data['enabled'] ?? false)) { @@ -111,9 +113,11 @@ class Users extends Controller } $managedUser->update($attributes); - $managedUser->roles()->sync([$role->id]); + $managedUser->syncRoles([$role->id]); }); + $managedUser->flushCache(); + flash('用户已修改。')->success(); return redirect()->route('system-settings.users.index'); @@ -126,8 +130,8 @@ class Users extends Controller abort_if($managedUser->id === user()->id, 422, '不能删除当前登录账号。'); abort_unless(SystemAccess::canManageUser(user(), $managedUser), 403); - if (SystemAccess::isSuperAdmin($managedUser) && $this->superAdminCount() <= 1) { - return back()->withErrors(['user' => '系统至少需要保留一名超级管理员。']); + if (SystemAccess::isSuperAdmin($managedUser) && $managedUser->enabled && $this->superAdminCount() <= 1) { + return back()->withErrors(['user' => '系统至少需要保留一名启用的主管理员。']); } $managedUser->delete(); @@ -143,10 +147,6 @@ class Users extends Controller $company->where('companies.id', company_id()); }); - if (! SystemAccess::isSuperAdmin()) { - $query->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::USER)); - } - return $query; } @@ -176,6 +176,7 @@ class Users extends Controller private function superAdminCount(): int { return User::query() + ->where('enabled', true) ->whereHas('roles', fn ($roles) => $roles->where('roles.name', SystemAccess::SUPER_ADMIN)) ->count(); } diff --git a/app/Http/Livewire/Menu/Settings.php b/app/Http/Livewire/Menu/Settings.php index 9d0cc611..6debfa40 100644 --- a/app/Http/Livewire/Menu/Settings.php +++ b/app/Http/Livewire/Menu/Settings.php @@ -26,7 +26,7 @@ class Settings extends Component $items = [ ['system-settings.users.index', '用户管理', 'people', '用户 类型 权限'], - ['system-settings.sidebar.edit', '侧边栏管理', 'menu', '侧边栏 用户类型 指定管理员'], + ['system-settings.permissions.index', '权限管理', 'admin_panel_settings', '侧边栏 用户页面 访问权限 管理员 身份'], ['system-settings.databases.index', '数据库管理', 'storage', '系统数据库 云数据库 上游 连接 测试'], ]; @@ -38,6 +38,8 @@ class Settings extends Component $item = $menu->route($route, $title, [], ($index + 1) * 10, [ 'icon' => $icon, 'search_keywords' => $keywords, + 'active' => fn () => request()->routeIs(Str::beforeLast($route, '.') . '.*') + || ($route === 'system-settings.permissions.index' && request()->routeIs('system-settings.sidebar.*')), ]); if ($item->isActive()) { diff --git a/app/Http/Requests/Auth/User.php b/app/Http/Requests/Auth/User.php index 88e7a5cb..c0f76b47 100644 --- a/app/Http/Requests/Auth/User.php +++ b/app/Http/Requests/Auth/User.php @@ -14,6 +14,30 @@ class User extends FormRequest */ public function authorize() { + $actor = $this->user(); + if ($actor && ! \App\Utilities\SystemAccess::isSuperAdmin($actor)) { + $target = $this->route('user'); + $target = $target instanceof \App\Models\Auth\User ? $target : \App\Models\Auth\User::find($target); + if ($target && $target->id !== $actor->id && \App\Utilities\SystemAccess::canOpenSettings($target)) { + return false; + } + if ($this->filled('permissions')) { + return false; + } + if ($this->has('roles')) { + $requested = array_map('strval', (array) $this->input('roles')); + $current = $target ? $target->roles()->pluck('id')->map(fn ($id) => (string) $id)->all() : []; + sort($requested); + sort($current); + if ($requested !== $current) { + $allowed = \App\Models\Auth\Role::where('name', 'user')->pluck('id')->map(fn ($id) => (string) $id)->all(); + if (array_diff($requested, $allowed)) { + return false; + } + } + } + } + return true; } diff --git a/app/Listeners/Menu/ShowInSettings.php b/app/Listeners/Menu/ShowInSettings.php index cef742aa..100e4660 100644 --- a/app/Listeners/Menu/ShowInSettings.php +++ b/app/Listeners/Menu/ShowInSettings.php @@ -19,7 +19,7 @@ class ShowInSettings { $menu = $event->menu; - if (user()?->hasRole('admin')) { + if (\App\Utilities\SystemAccess::canOpenSettings()) { $menu->route('settings.navigation.manage', '侧边栏功能名称', [], 75, [ 'icon' => 'menu', 'search_keywords' => '侧边栏 功能 名称', diff --git a/app/Utilities/SidebarPermissions.php b/app/Utilities/SidebarPermissions.php index 2738c115..04e4575d 100644 --- a/app/Utilities/SidebarPermissions.php +++ b/app/Utilities/SidebarPermissions.php @@ -31,13 +31,14 @@ class SidebarPermissions public static function allowedKeys(User $user): ?array { + if (SystemAccess::isSuperAdmin($user)) { + return null; + } + $settings = self::settings(); $subject = self::subjectKey($user); if (! in_array($subject, $settings['configured'] ?? [], true)) { - if ($user->created_from === 'dingtalk_roster' && SystemAccess::roleName($user) === SystemAccess::USER) { - return []; - } return null; } @@ -60,7 +61,7 @@ class SidebarPermissions return $section; } - $pages = array_values(array_filter($section['pages'] ?? [], function (array $page) use ($allowed, $sectionKey) { + $pages = array_values(array_filter(array_map(function (array $page) use ($allowed, $sectionKey) { $pageKey = $sectionKey . '/' . $page['slug']; if (in_array($pageKey, $allowed, true)) { @@ -70,7 +71,7 @@ class SidebarPermissions return in_array($sectionKey . '/' . $child['slug'], $allowed, true); })); } - return true; + return $page; } // 子页面可能在 allowed 列表中(独立 URL),父级分组也允许展开 @@ -80,12 +81,12 @@ class SidebarPermissions })); if (! empty($visibleChildren)) { $page['children'] = $visibleChildren; - return true; + return $page; } } - return false; - })); + return null; + }, $section['pages'] ?? []))); if (! empty($pages)) { $section['pages'] = $pages; diff --git a/app/Utilities/SystemAccess.php b/app/Utilities/SystemAccess.php index 4632792b..0b39f376 100644 --- a/app/Utilities/SystemAccess.php +++ b/app/Utilities/SystemAccess.php @@ -18,8 +18,11 @@ class SystemAccess return ''; } + // 身份变更必须立即生效,不能沿用权限库中已缓存的旧角色。 + $roles = $user->roles()->pluck('name')->all(); + foreach ([self::SUPER_ADMIN, self::ADMIN, self::USER] as $role) { - if ($user->hasRole($role)) { + if (in_array($role, $roles, true)) { return $role; } } @@ -53,8 +56,8 @@ class SystemAccess if (self::isSuperAdmin($actor)) { return [ - self::SUPER_ADMIN => '超级管理员', - self::ADMIN => '普通管理员', + self::SUPER_ADMIN => '主管理员(最高权限)', + self::ADMIN => '管理员', self::USER => '普通用户', ]; } @@ -65,8 +68,8 @@ class SystemAccess public static function roleLabels(): array { return [ - self::SUPER_ADMIN => '超级管理员', - self::ADMIN => '普通管理员', + self::SUPER_ADMIN => '主管理员', + self::ADMIN => '管理员', self::USER => '普通用户', ]; } diff --git a/config/business_navigation.php b/config/business_navigation.php index 5663fc9a..91836de0 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -254,7 +254,7 @@ return [ ['name' => 'account_type', 'label' => '账号类型'], ['name' => 'account', 'label' => '账号'], ['name' => 'password_configured', 'label' => '密码', 'kind' => 'secret_status'], - ['name' => 'bound_phone', 'label' => '绑定手机号', 'kind' => 'secret'], + ['name' => 'bound_phone', 'label' => '绑定手机号'], ['name' => 'verification_contact', 'label' => '手机号验证码对接人'], ['name' => 'account_group_name', 'label' => '账号分组名称'], ['name' => 'created_at', 'label' => '店铺创建时间', 'kind' => 'date'], diff --git a/docs/钉钉花名册登录.md b/docs/钉钉花名册登录.md index 18b201c3..6a94f5ff 100644 --- a/docs/钉钉花名册登录.md +++ b/docs/钉钉花名册登录.md @@ -15,7 +15,7 @@ 新账号使用钉钉接口返回的姓名、可用邮箱和手机号;邮箱缺失或已被其他账号使用时,生成稳定的 `@dingtalk.invalid` 内部占位地址。系统生成不可预知的随机本地密码,不展示或发送密码,不发送注册邀请。花名册显示名仅作为姓名缺失时的后备。 -新账号赋予现有 `user` 角色及进入应用外壳所需的 `read-admin-panel` 权限,不添加管理权限。业务访问继承管理员明确配置的普通用户权限;尚无配置时默认无业务权限,进入“你已登录,等待分配业务权限”的工作台。管理员可在“侧边栏管理”中为普通用户角色或指定账号配置业务范围。 +新账号赋予现有 `user` 角色及进入应用外壳所需的 `read-admin-panel` 权限,不添加管理权限。按负责人最新要求,尚无页面权限配置时默认显示完整业务导航;主管理员随后可在“侧边栏管理”中设置指定用户的业务范围。配置优先级为指定用户、角色、默认完整导航;显式配置为空时没有业务页面,不回退到完整导航。管理员专属操作(包括店铺密码读取)继续单独鉴权。 ## 只读数据契约 diff --git a/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php b/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php new file mode 100644 index 00000000..75e8018c --- /dev/null +++ b/modules/EcommerceFinance/Repositories/CourierFreightReconciliationMirrorRepository.php @@ -0,0 +1,198 @@ +connection = $mirror->connection(); + } + + public function available(): bool + { + try { + $schema = Schema::connection($this->connection->getName()); + $dataset = $this->datasets->get(self::DATASET); + + return $schema->hasTable('business_mirror_table_states') + && $schema->hasTable($dataset->targetTable()); + } catch (Throwable) { + return false; + } + } + + public function ready(): bool + { + try { + return $this->mirror->ready($this->datasets->get(self::DATASET)); + } catch (Throwable) { + return false; + } + } + + /** @return array|null */ + public function state(): ?array + { + try { + return $this->mirror->state($this->datasets->get(self::DATASET)); + } catch (Throwable) { + return null; + } + } + + /** @return array{code:string,last_succeeded_at:?string,lag_minutes:?int} */ + public function syncStatus(?array $state = null): array + { + $state ??= $this->state(); + if (! is_array($state)) { + return ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null]; + } + + $lastSucceeded = $state['last_succeeded_at'] ?? null; + $lagMinutes = null; + if (is_string($lastSucceeded) && $lastSucceeded !== '') { + try { + $lagMinutes = (int) ceil(max(0, now()->getTimestamp() - Carbon::parse($lastSucceeded)->getTimestamp()) / 60); + } catch (Throwable) { + $lagMinutes = null; + } + } + + $status = (string) ($state['status'] ?? 'pending'); + if (($state['initial_completed_at'] ?? null) === null) { + $code = $status === 'failed' ? 'initial_failed' : ($status === 'running' ? 'initializing' : 'pending'); + } elseif ($status === 'failed') { + $code = 'failed'; + } elseif ($status === 'running') { + $code = 'syncing'; + } elseif ($lagMinutes === null || $lagMinutes > max((int) config('upstream_mirrors.courier_information.stale_after_minutes', 60), 1)) { + $code = 'stale'; + } else { + $code = 'fresh'; + } + + return ['code' => $code, 'last_succeeded_at' => $lastSucceeded, 'lag_minutes' => $lagMinutes]; + } + + /** + * @param array $filters + * @return array{data:list>,total:int,summary:array,state:array|null,sync:array} + */ + public function paginate(array $filters, int $page, int $perPage): array + { + $dataset = $this->datasets->get(self::DATASET); + if (! $this->mirror->ready($dataset)) { + throw new RuntimeException('courier_freight_reconciliation_mirror_not_ready'); + } + + $page = max($page, 1); + $perPage = min(max($perPage, 1), 100); + $query = $this->filteredQuery($dataset->targetTable(), $filters); + $total = (clone $query)->count(); + $summary = (clone $query)->selectRaw( + 'COUNT(*) AS aggregate_count, ' + . 'COALESCE(SUM(payable_amount), 0) AS payable_amount, ' + . 'COALESCE(SUM(actual_payment), 0) AS actual_payment, ' + . 'COALESCE(SUM(confirmed_amount), 0) AS confirmed_amount, ' + . 'COALESCE(SUM(reconciliation_difference), 0) AS reconciliation_difference' + )->first(); + $rows = (clone $query)->select(self::COLUMNS) + ->orderByDesc('Id') + ->forPage($page, $perPage) + ->get() + ->map(fn (object $row): array => $this->mapRow($row)) + ->all(); + $state = $this->mirror->state($dataset); + + return [ + 'data' => $rows, + 'total' => $total, + 'summary' => [ + 'count' => (int) ($summary->aggregate_count ?? 0), + 'payable_amount' => (float) ($summary->payable_amount ?? 0), + 'actual_payment' => (float) ($summary->actual_payment ?? 0), + 'confirmed_amount' => (float) ($summary->confirmed_amount ?? 0), + 'reconciliation_difference' => (float) ($summary->reconciliation_difference ?? 0), + ], + 'state' => $state, + 'sync' => $this->syncStatus($state), + ]; + } + + private function filteredQuery(string $table, array $filters): Builder + { + $query = $this->connection->table($table) + ->where(static function (Builder $query): void { + $query->whereNull('is_delete')->orWhere('is_delete', '=', 0); + }); + $this->containsFilter($query, 'waybill', $filters['express_no'] ?? ''); + $this->containsFilter($query, 'expressname', $filters['express_name'] ?? ''); + + if (($filters['month'] ?? '') !== '') { + $query->where('month', '=', $filters['month']); + } + if (($filters['status'] ?? '') !== '') { + $query->where('status', '=', $filters['status']); + } + if (($filters['modified_from'] ?? '') !== '') { + $query->where('modify_time', '>=', $filters['modified_from'] . ' 00:00:00'); + } + if (($filters['modified_to'] ?? '') !== '') { + $query->where('modify_time', '<', date('Y-m-d 00:00:00', strtotime($filters['modified_to'] . ' +1 day'))); + } + + return $query; + } + + private function containsFilter(Builder $query, string $column, mixed $value): void + { + $value = trim((string) $value); + if ($value === '') { + return; + } + + $escaped = str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $value); + $query->whereRaw($column . " LIKE ? ESCAPE '!'", ['%' . $escaped . '%']); + } + + /** @return array */ + private function mapRow(object $row): array + { + $mapped = []; + foreach (self::COLUMNS as $column) { + $mapped[$column] = $row->{$column} ?? null; + } + foreach (['opening_balance', 'courier_provided_amount', 'reconciliation_difference', 'confirmed_amount', 'advance_payment', 'payable_amount', 'actual_payment', 'compensation_amount', 'other_replenishment', 'corporate_payment_amount', 'personal_payment_amount'] as $amount) { + $mapped[$amount] = $mapped[$amount] === null ? null : (float) $mapped[$amount]; + } + + return $mapped; + } +} diff --git a/resources/views/business/pages/courier-payables/freight-reconciliation.blade.php b/resources/views/business/pages/courier-payables/freight-reconciliation.blade.php index 9a5b066b..e7082813 100644 --- a/resources/views/business/pages/courier-payables/freight-reconciliation.blade.php +++ b/resources/views/business/pages/courier-payables/freight-reconciliation.blade.php @@ -1,311 +1,87 @@ - +@php + $filters = $freightReconciliationFilters ?? []; + $summary = $freightReconciliationSummary ?? []; + $sync = $freightReconciliationSync ?? ['code' => 'unavailable', 'last_succeeded_at' => null, 'lag_minutes' => null]; + $statusLabels = ['fresh' => '数据新鲜', 'stale' => '数据可能滞后', 'syncing' => '同步中', 'initializing' => '首次同步中', 'pending' => '等待同步', 'failed' => '同步失败', 'initial_failed' => '首次同步失败', 'unavailable' => '镜像不可用']; + $money = static fn (mixed $value): string => number_format((float) ($value ?? 0), 2); +@endphp + +
-
- -
-
-

{{ $page['title'] ?? '运费对账' }}

-
+ +
+
+

{{ $page['title'] ?? '运费对账' }}

+

数据来源:JianSun · dbo.fys_express_courier_settlement · 共享业务镜像

- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - - 重置 - -
-
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - {{-- 物流信息 --}} - - - - - - - - - - - {{-- 订单信息 --}} - - - - - - - - - {{-- 收件信息 --}} - - - - - - - - {{-- 商品规格 --}} - - - - - - {{-- 平台/店铺 --}} - - - - {{-- 供应商/仓库 --}} - - - - - - {{-- 时间 --}} - - - - {{-- 备注 --}} - - - - - - @forelse ($freightReconciliations ?? [] as $index => $item) - - - {{-- 物流信息 --}} - - - - - - - - - - - {{-- 订单信息 --}} - - - - - - - - - {{-- 收件信息 --}} - - - - - - - - {{-- 商品规格 --}} - - - - - - {{-- 平台/店铺 --}} - - - - {{-- 供应商/仓库 --}} - - - - - - {{-- 时间 --}} - - - - {{-- 备注 --}} - - - - @empty - - - - @endforelse - -
#物流信息订单信息收件信息商品规格平台/店铺供应商/仓库时间备注
#快递单号快递名称1快递名称2运费费率结算重量结算体积对账重量对账体积换单前单号交易单号商品ID条形码商品名称数量金额货号配货单号省市区收货详细地址收货姓名手机号码座机号码长(cm)宽(cm)高(cm)体积(cm³)重量(kg)平台店铺名称买家昵称供应商仓库成品条码成品名称成品供应商发货时间下载时间付款时间买家留言卖家备注
- {{ ($freightReconciliations->currentPage() - 1) * $freightReconciliations->perPage() + $index + 1 }} - {{ $item['express_no'] ?? '-' }}{{ $item['express_name1'] ?? '-' }}{{ $item['express_name2'] ?? '-' }} - {{ isset($item['freight']) ? number_format((float) $item['freight'], 2) : '-' }} - {{ $item['rate'] ?? '-' }}{{ isset($item['settle_weight']) ? number_format((float) $item['settle_weight'], 4) : '-' }}{{ isset($item['settle_volume']) ? number_format((float) $item['settle_volume'], 4) : '-' }}{{ isset($item['recon_weight']) ? number_format((float) $item['recon_weight'], 4) : '-' }}{{ isset($item['recon_volume']) ? number_format((float) $item['recon_volume'], 4) : '-' }}{{ $item['prev_express_no'] ?? '-' }}{{ $item['trade_no'] ?? '-' }}{{ $item['product_id'] ?? '-' }}{{ $item['barcode'] ?? '-' }} - {{ $item['product_name'] ?? '-' }} - {{ $item['qty'] ?? '-' }}{{ isset($item['amount']) ? number_format((float) $item['amount'], 2) : '-' }}{{ $item['item_no'] ?? '-' }}{{ $item['pick_no'] ?? '-' }}{{ $item['province'] ?? '-' }}{{ $item['city'] ?? '-' }}{{ $item['district'] ?? '-' }} - {{ $item['address'] ?? '-' }} - {{ $item['receiver_name'] ?? '-' }}{{ $item['receiver_phone'] ?? '-' }}{{ $item['receiver_tel'] ?? '-' }}{{ isset($item['length']) ? number_format((float) $item['length'], 2) : '-' }}{{ isset($item['width']) ? number_format((float) $item['width'], 2) : '-' }}{{ isset($item['height']) ? number_format((float) $item['height'], 2) : '-' }}{{ isset($item['volume']) ? number_format((float) $item['volume'], 4) : '-' }}{{ isset($item['weight']) ? number_format((float) $item['weight'], 4) : '-' }} - - {{ $item['platform'] ?? '-' }} - - - {{ $item['shop_name'] ?? '-' }} - {{ $item['buyer_nick'] ?? '-' }}{{ $item['supplier'] ?? '-' }}{{ $item['warehouse'] ?? '-' }}{{ $item['finished_barcode'] ?? '-' }}{{ $item['finished_name'] ?? '-' }}{{ $item['finished_supplier'] ?? '-' }}{{ $item['ship_time'] ?? '-' }}{{ $item['download_time'] ?? '-' }}{{ $item['pay_time'] ?? '-' }} - {{ $item['buyer_msg'] ?? '-' }} - - {{ $item['seller_memo'] ?? '-' }} -
-
- - - - - 暂无运费对账数据 -
-
-
- - - @if (isset($freightReconciliations) && $freightReconciliations->hasPages()) -
-
- 共 {{ $freightReconciliations->total() }} 条记录 -
-
- @if ($freightReconciliations->onFirstPage()) - 上一页 - @else - 上一页 - @endif - - @php - $current = $freightReconciliations->currentPage(); - $last = $freightReconciliations->lastPage(); - $start = max(1, $current - 2); - $end = min($last, $current + 2); - @endphp - - @if ($start > 1) - 1 - @if ($start > 2) - ... - @endif - @endif - - @for ($p = $start; $p <= $end; $p++) - @if ($p === $current) - {{ $p }} - @else - {{ $p }} - @endif - @endfor - - @if ($end < $last) - @if ($end < $last - 1) - ... - @endif - {{ $last }} - @endif - - @if ($freightReconciliations->hasMorePages()) - 下一页 - @else - 下一页 - @endif -
-
- @endif +
+ {{ $statusLabels[$sync['code'] ?? 'unavailable'] ?? '镜像状态未知' }} + @if (!empty($sync['last_succeeded_at'])) 最近同步 {{ $sync['last_succeeded_at'] }} @endif
- + @if (!empty($freightReconciliationError)) +
{{ $freightReconciliationError }}
+ @endif + +
+ @foreach ([ + ['label' => '记录数', 'value' => number_format((int) ($summary['count'] ?? 0))], + ['label' => '应付金额', 'value' => '¥ ' . $money($summary['payable_amount'] ?? 0)], + ['label' => '已确认金额', 'value' => '¥ ' . $money($summary['confirmed_amount'] ?? 0)], + ['label' => '实际付款', 'value' => '¥ ' . $money($summary['actual_payment'] ?? 0)], + ['label' => '对账差额', 'value' => '¥ ' . $money($summary['reconciliation_difference'] ?? 0)], + ] as $stat) +
{{ $stat['label'] }}
{{ $stat['value'] }}
+ @endforeach +
+ +
+
+ @foreach ([ + ['name' => 'express_no', 'label' => '快递单号', 'placeholder' => '包含匹配'], + ['name' => 'express_name', 'label' => '快递名称', 'placeholder' => '包含匹配'], + ['name' => 'month', 'label' => '月份', 'placeholder' => '如 2026-08'], + ['name' => 'status', 'label' => '状态', 'placeholder' => '精确匹配'], + ] as $field) + + @endforeach + + +
重置
+
+
+ +
+
+ + + + @forelse ($freightReconciliations ?? [] as $row) + + @foreach (['reconciliation_difference','confirmed_amount','payable_amount','actual_payment','advance_payment','compensation_amount','other_replenishment','corporate_payment_amount','personal_payment_amount'] as $amount) + + @endforeach + + + @empty + + @endforelse +
快递单号快递名称月份状态对账差额已确认金额应付金额实际付款预付款赔偿金额其他补款公司付款个人付款创建时间修改时间
{{ $row['waybill'] ?? '-' }}{{ $row['expressname'] ?: ($row['courier_company_name'] ?? '-') }}{{ $row['month'] ?? '-' }}{{ $row['status'] ?? '-' }}{{ $row[$amount] === null ? '-' : $money($row[$amount]) }}{{ $row['create_time'] ?? '-' }}{{ $row['modify_time'] ?? '-' }}
@if ($freightReconciliationPending ?? false) 正在等待镜像首次同步完成… @else 暂无符合条件的运费对账数据 @endif
+ @if (($freightReconciliations ?? null)?->hasPages()) +
共 {{ $freightReconciliations->total() }} 条记录
{{ $freightReconciliations->onEachSide(1)->links() }}
+ @endif +
+
diff --git a/resources/views/system_settings/partials/navigation-script.blade.php b/resources/views/system_settings/partials/navigation-script.blade.php new file mode 100644 index 00000000..351c9cbf --- /dev/null +++ b/resources/views/system_settings/partials/navigation-script.blade.php @@ -0,0 +1,74 @@ +@once +@push('scripts_start') + +@endpush +@endonce diff --git a/resources/views/system_settings/partials/navigation.blade.php b/resources/views/system_settings/partials/navigation.blade.php index b43c8795..65e1e1d4 100644 --- a/resources/views/system_settings/partials/navigation.blade.php +++ b/resources/views/system_settings/partials/navigation.blade.php @@ -1,21 +1,23 @@ -