From 8b76e100cfd76718cf8a8226e95d2e13d3684fea Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Tue, 20 Oct 2026 11:39:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=BB=B4=E6=8A=A4?= =?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=8F=B0=E8=B4=A6=E8=B4=A6=E6=88=B7=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Business/FinanceLedger.php | 58 +++++++++ app/Utilities/FinanceLedgerAccess.php | 7 +- ..._finance_ledger_account_type_overrides.php | 40 ++++++ docs/财务台账开发说明.md | 11 +- .../View/FinanceLedgerPageData.php | 68 ++++++++-- .../pages/financial-ledger/ledger.blade.php | 121 ++++++++++-------- routes/admin.php | 4 + .../FinanceLedgerAccountStatusTest.php | 77 +++++++++++ 8 files changed, 318 insertions(+), 68 deletions(-) create mode 100644 database/migrations/2026_10_20_140000_create_finance_ledger_account_type_overrides.php diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index 854ffd86..98be1e6b 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -107,4 +107,62 @@ class FinanceLedger extends Controller 'label' => FinanceLedgerAccountStatus::label($status), ], 200, ['Cache-Control' => 'private, no-store']); } + + public function updateAccountType(Request $request): JsonResponse + { + abort_unless(FinanceLedgerAccess::canEditAccountType(user()), 403, '只有财务台账管理员可以修改账户类型。'); + + $data = $request->validate([ + 'account_short' => ['required', 'string', 'max:100'], + 'account_type' => ['nullable', 'string', 'max:50'], + ]); + + $accountShort = trim((string) $data['account_short']); + $accountType = trim((string) ($data['account_type'] ?? '')); + abort_if($accountShort === '', 422, '账户简称不能为空。'); + + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + abort_unless( + $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(), + 422, + '账户简称不存在,请刷新账户总表后重试。', + ); + + if ($accountType !== '') { + $allowedTypes = array_values(array_unique(array_merge( + $connection->table('fin_accounts') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->pluck('account_type')->all(), + $connection->table('fin_account_type_overrides') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->pluck('account_type')->all(), + ))); + abort_unless(in_array($accountType, $allowedTypes, true), 422, '请选择现有账户类型。'); + } + + $overrides = $connection->table('fin_account_type_overrides'); + $existing = $overrides->where('account_short', $accountShort)->first(); + $now = now(); + $values = [ + 'account_type' => $accountType !== '' ? $accountType : null, + 'updated_by' => (string) user()->name, + 'updated_at' => $now, + ]; + + if ($existing === null) { + $overrides->insert($values + [ + 'account_short' => $accountShort, + 'created_at' => $now, + ]); + } else { + $overrides->where('account_short', $accountShort)->update($values); + } + + return response()->json([ + 'ok' => true, + 'account_short' => $accountShort, + 'account_type' => $accountType, + 'label' => $accountType !== '' ? $accountType : '未填写', + ], 200, ['Cache-Control' => 'private, no-store']); + } } diff --git a/app/Utilities/FinanceLedgerAccess.php b/app/Utilities/FinanceLedgerAccess.php index 9fb40a73..68ed953e 100644 --- a/app/Utilities/FinanceLedgerAccess.php +++ b/app/Utilities/FinanceLedgerAccess.php @@ -30,10 +30,15 @@ class FinanceLedgerAccess } /** - * 账户状态与账户映射使用同一组财务台账编辑角色。 + * 账户状态、账户类型与账户映射使用同一组财务台账编辑角色。 */ public static function canEditAccountStatus(?User $user = null): bool { return self::canEditMapping($user); } + + public static function canEditAccountType(?User $user = null): bool + { + return self::canEditMapping($user); + } } diff --git a/database/migrations/2026_10_20_140000_create_finance_ledger_account_type_overrides.php b/database/migrations/2026_10_20_140000_create_finance_ledger_account_type_overrides.php new file mode 100644 index 00000000..54c23c0b --- /dev/null +++ b/database/migrations/2026_10_20_140000_create_finance_ledger_account_type_overrides.php @@ -0,0 +1,40 @@ +connection()); + + if (! $schema->hasTable('fin_account_type_overrides')) { + $schema->create('fin_account_type_overrides', function (Blueprint $table): void { + $table->bigIncrements('id'); + $table->string('account_short', 100)->unique('fin_account_type_overrides_account_unique'); + $table->string('account_type', 50)->nullable(); + $table->string('updated_by', 100)->nullable(); + $table->timestamps(); + + $table->index(['account_type'], 'fin_account_type_overrides_type_index'); + }); + } + } + + public function down(): void + { + Schema::connection($this->connection())->dropIfExists('fin_account_type_overrides'); + } +}; diff --git a/docs/财务台账开发说明.md b/docs/财务台账开发说明.md index 05f5d291..7e7e91ce 100644 --- a/docs/财务台账开发说明.md +++ b/docs/财务台账开发说明.md @@ -17,9 +17,11 @@ | `fin_account_map` | 来源值 → 账户简称 | | `fin_account_map_events` | 映射修改审计 | | `fin_account_statuses` | 账户状态维护(正常、网银异常、限额、工商注销) | +| `fin_account_type_overrides` | 账户类型人工维护值 | 迁移:`database/migrations/2026_10_19_160000_create_finance_ledger_tables.php`、 -`database/migrations/2026_10_20_120000_create_finance_ledger_account_statuses.php`。 +`database/migrations/2026_10_20_120000_create_finance_ledger_account_statuses.php`、 +`database/migrations/2026_10_20_140000_create_finance_ledger_account_type_overrides.php`。 ## 本地导入 @@ -61,11 +63,12 @@ docker compose -f docker-compose.local.yml exec -u www-data app \ - 路由:`/workspace/financial-ledger/ledger` - 页面:账户总表、店铺台账、每日资金流水、动账明细、账户映射、数据来源 -- 写权限:账户映射和账户状态修改仅系统管理员及配置的财务台账管理员角色;其他账号只读 +- 写权限:账户映射、账户状态和账户类型修改仅系统管理员及配置的财务台账管理员角色;其他账号只读 - 调度:`finance-ledger:sync-rpt` 每天 02:30(Asia/Shanghai) -账户状态独立保存在 `fin_account_statuses`,重新导入账户基础数据不会覆盖; -页面修改使用异步保存,不触发整页刷新。 +账户状态独立保存在 `fin_account_statuses`,账户类型人工维护值独立保存在 +`fin_account_type_overrides`,重新导入账户基础数据均不会覆盖;页面修改使用 +异步保存,不触发整页刷新。 生产宿主机 `/etc/cron.d` 仍需增加每日调用入口;本项目当前没有运行 `schedule:run` 的容器,调度登记不能替代宿主机 cron。 diff --git a/modules/EcommerceFinance/View/FinanceLedgerPageData.php b/modules/EcommerceFinance/View/FinanceLedgerPageData.php index 64bb5ff2..2150ca04 100644 --- a/modules/EcommerceFinance/View/FinanceLedgerPageData.php +++ b/modules/EcommerceFinance/View/FinanceLedgerPageData.php @@ -59,6 +59,7 @@ class FinanceLedgerPageData 'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS, 'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()), 'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()), + 'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()), ]; } @@ -102,20 +103,46 @@ class FinanceLedgerPageData } } - foreach (['accounts_region' => 'region', 'accounts_type' => 'account_type'] as $parameter => $column) { - $value = trim((string) request($parameter, '')); - if ($value === '') { - continue; - } - if ($value === self::ACCOUNT_FILTER_EMPTY) { - $query->where(function (Builder $builder) use ($column): void { - $builder->whereNull($column)->orWhere($column, ''); + $region = trim((string) request('accounts_region', '')); + if ($region !== '') { + if ($region === self::ACCOUNT_FILTER_EMPTY) { + $query->where(function (Builder $builder): void { + $builder->whereNull('region')->orWhere('region', ''); }); } else { - $query->where($column, $value); + $query->where('region', $region); } } + $accountType = trim((string) request('accounts_type', '')); + if ($accountType !== '') { + $query->where(function (Builder $effectiveType) use ($accountType): void { + $effectiveType->where(function (Builder $baseType) use ($accountType): void { + $baseType->whereNotIn( + 'account_short', + $this->db()->table('fin_account_type_overrides')->select('account_short'), + ); + if ($accountType === self::ACCOUNT_FILTER_EMPTY) { + $baseType->where(function (Builder $blank): void { + $blank->whereNull('account_type')->orWhere('account_type', ''); + }); + } else { + $baseType->where('account_type', $accountType); + } + }); + + $overrideType = $this->db()->table('fin_account_type_overrides')->select('account_short'); + if ($accountType === self::ACCOUNT_FILTER_EMPTY) { + $overrideType->where(function (Builder $blank): void { + $blank->whereNull('account_type')->orWhere('account_type', ''); + }); + } else { + $overrideType->where('account_type', $accountType); + } + $effectiveType->orWhereIn('account_short', $overrideType); + }); + } + $summaryQuery = clone $query; $summary = [ 'count' => (clone $query)->count(), @@ -135,8 +162,16 @@ class FinanceLedgerPageData : $this->db()->table('fin_account_statuses') ->whereIn('account_short', $accountShorts) ->pluck('status', 'account_short'); - $rows->getCollection()->transform(static function ($row) use ($statuses) { + $typeOverrides = $accountShorts === [] + ? collect() + : $this->db()->table('fin_account_type_overrides') + ->whereIn('account_short', $accountShorts) + ->pluck('account_type', 'account_short'); + $rows->getCollection()->transform(static function ($row) use ($statuses, $typeOverrides) { $row->account_status = FinanceLedgerAccountStatus::normalize($statuses[$row->account_short] ?? null); + if ($typeOverrides->has($row->account_short)) { + $row->account_type = (string) ($typeOverrides->get($row->account_short) ?? ''); + } return $row; }); @@ -319,13 +354,20 @@ class FinanceLedgerPageData private function accountFilterOptions(): array { + $baseTypes = $this->db()->table('fin_accounts') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->orderBy('account_type')->pluck('account_type')->all(); + $overrideTypes = $this->db()->table('fin_account_type_overrides') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->orderBy('account_type')->pluck('account_type')->all(); + $types = array_values(array_unique(array_merge($baseTypes, $overrideTypes))); + sort($types, SORT_NATURAL | SORT_FLAG_CASE); + return [ 'regions' => $this->db()->table('fin_accounts') ->whereNotNull('region')->where('region', '<>', '') ->distinct()->orderBy('region')->pluck('region')->all(), - 'types' => $this->db()->table('fin_accounts') - ->whereNotNull('account_type')->where('account_type', '<>', '') - ->distinct()->orderBy('account_type')->pluck('account_type')->all(), + 'types' => $types, ]; } diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index 9d2d1bff..448ad1fe 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -4,8 +4,10 @@ $activeTab = $ledgerTab ?? 'accounts'; $canEdit = (bool) ($ledgerCanEditMapping ?? false); $canEditAccountStatus = (bool) ($ledgerCanEditAccountStatus ?? false); + $canEditAccountType = (bool) ($ledgerCanEditAccountType ?? false); $accountStatusOptions = $ledgerAccountStatusOptions ?? \App\Utilities\FinanceLedgerAccountStatus::OPTIONS; $accountFilterOptions = $ledgerAccountFilterOptions ?? ['regions' => [], 'types' => []]; + $accountTypeOptions = $accountFilterOptions['types'] ?? []; $money = static fn ($value): string => $value === null || $value === '' ? '' : number_format((float) $value, 2); $date = static fn ($value): string => $value ? \Illuminate\Support\Carbon::parse($value)->format('Y-m-d') : ''; $tabUrl = static fn (string $tab): string => route('business.pages.show', ['section' => 'financial-ledger', 'page' => 'ledger', 'tab' => $tab]); @@ -69,14 +71,16 @@ .ledger-map-input.is-saving { border-color:var(--vip-content-accent, #B9954F); background:#fffdf6; } .ledger-map-input.is-error { border-color:var(--vip-danger, #B80000); background:#fff5f5; } .ledger-table .ledger-status-column { width:4.6rem; min-width:4.6rem; padding-right:.2rem; padding-left:.2rem; text-align:center; } - .ledger-status-select { display:block; width:4.15rem; min-width:4.15rem; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; } - .ledger-status-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); } - .ledger-status-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); } - .ledger-status-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); } - .ledger-status-select[data-status="limited"] { border-color:rgba(147,95,7,.36); background:#fff8eb; color:var(--vip-warning, #935F07); } - .ledger-status-select[data-status="business_deregistered"] { border-color:rgba(89,89,89,.32); background:#f3f1ec; color:var(--vip-content-muted, #595959); } - .ledger-status-select.is-saving { opacity:.62; cursor:wait; } - .ledger-status-select.is-error { border-color:var(--vip-danger, #B80000); box-shadow:0 0 0 3px rgba(184,0,0,.12); } + .ledger-inline-select { display:block; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; } + .ledger-status-select { width:4.15rem; min-width:4.15rem; } + .ledger-type-select { width:5.2rem; min-width:5.2rem; } + .ledger-inline-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); } + .ledger-inline-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); } + .ledger-inline-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); } + .ledger-inline-select[data-status="limited"] { border-color:rgba(147,95,7,.36); background:#fff8eb; color:var(--vip-warning, #935F07); } + .ledger-inline-select[data-status="business_deregistered"] { border-color:rgba(89,89,89,.32); background:#f3f1ec; color:var(--vip-content-muted, #595959); } + .ledger-inline-select.is-saving { opacity:.62; cursor:wait; } + .ledger-inline-select.is-error { border-color:var(--vip-danger, #B80000); box-shadow:0 0 0 3px rgba(184,0,0,.12); } .ledger-status-badge { display:inline-flex; align-items:center; justify-content:center; min-height:1.65rem; padding:.1rem .3rem; border:1px solid transparent; border-radius:999px; font-size:.68rem; font-weight:700; } .ledger-status-badge[data-status="normal"] { border-color:rgba(83,121,62,.28); background:#f3f7ee; color:var(--vip-success, #53793E); } .ledger-status-badge[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.24); background:#fff5f5; color:var(--vip-danger, #B80000); } @@ -143,7 +147,7 @@