connection = $databases->connection( (string) config('upstream_mirrors.connection', 'business_mirror'), ); } /** * @param array $data * @return array */ public function update(array $data): array { $date = $this->date($data['flow_date'] ?? null); $accountShort = $this->text($data['account_short'] ?? ''); $openingCents = $this->cents($data['opening_balance'] ?? 0); $inflowCents = $this->cents($data['inflow'] ?? 0); $outflowCents = $this->cents($data['outflow'] ?? 0); $closingCents = $this->cents($data['closing_balance'] ?? 0); $opening = $openingCents / 100; $inflow = $inflowCents / 100; $outflow = $outflowCents / 100; $closing = $closingCents / 100; $remark = $this->nullableText($data['remark'] ?? null); if ($openingCents + $inflowCents - $outflowCents !== $closingCents) { throw ValidationException::withMessages([ 'closing_balance' => '期末余额必须等于期初余额 + 当日入账 - 当日出账。', ]); } $this->connection->transaction(function () use ( $date, $accountShort, $opening, $inflow, $outflow, $closing, $remark, ): void { $account = $this->connection->table('fin_accounts') ->where('account_short', $accountShort) ->lockForUpdate() ->first(); if ($account === null) { throw ValidationException::withMessages([ 'account_short' => '账户简称不存在,请从账户总表中选择。', ]); } $before = $this->existingFlow($date, $accountShort); $values = [ 'opening_balance' => $opening, 'inflow' => $inflow, 'outflow' => $outflow, 'closing_balance' => $closing, 'remark' => $remark, 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => now(), ]; $existing = $this->connection->table('fin_daily_flow_overrides') ->where('flow_date', $date) ->where('account_short', $accountShort) ->first(); if ($existing === null) { $this->connection->table('fin_daily_flow_overrides')->insert($values + [ 'flow_date' => $date, 'account_short' => $accountShort, 'created_at' => now(), ]); } else { $this->connection->table('fin_daily_flow_overrides') ->where('id', $existing->id) ->update($values); } $this->connection->table('fin_ledger_edit_events')->insert([ 'entity_type' => 'daily_flow', 'entity_key' => $date . '|' . $accountShort, 'changes' => json_encode([ 'before' => $before, 'after' => [ 'flow_date' => $date, 'account_short' => $accountShort, 'opening_balance' => $opening, 'inflow' => $inflow, 'outflow' => $outflow, 'closing_balance' => $closing, 'remark' => $remark, ], ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'user_id' => user()?->id, 'user_name' => (string) (user()?->name ?? ''), 'created_at' => now(), ]); $this->calculator->recalculate(); }); return $this->existingFlow($date, $accountShort) ?? []; } /** * @return array|null */ public function reset(string $flowDate, string $accountShort): ?array { $date = $this->date($flowDate); $accountShort = $this->text($accountShort); $this->connection->transaction(function () use ($date, $accountShort): void { $before = $this->existingFlow($date, $accountShort); $deleted = $this->connection->table('fin_daily_flow_overrides') ->where('flow_date', $date) ->where('account_short', $accountShort) ->delete(); if ($deleted > 0) { $this->connection->table('fin_ledger_edit_events')->insert([ 'entity_type' => 'daily_flow_reset', 'entity_key' => $date . '|' . $accountShort, 'changes' => json_encode([ 'before' => $before, 'after' => null, ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'user_id' => user()?->id, 'user_name' => (string) (user()?->name ?? ''), 'created_at' => now(), ]); } $this->calculator->recalculate(); }); return $this->existingFlow($date, $accountShort); } /** @return array|null */ public function existingFlow(string $date, string $accountShort): ?array { $row = $this->connection->table('fin_daily_flows') ->whereDate('flow_date', $date) ->where('account_short', $accountShort) ->first(); if ($row === null) { return null; } return [ 'id' => (int) $row->id, 'flow_date' => substr((string) $row->flow_date, 0, 10), 'company_name' => $row->company_name, 'account_short' => $row->account_short, 'opening_balance' => round((float) $row->opening_balance, 2), 'inflow' => round((float) $row->inflow, 2), 'outflow' => round((float) $row->outflow, 2), 'closing_balance' => round((float) $row->closing_balance, 2), 'remark' => $row->remark, 'manual_override' => (bool) ($row->manual_override ?? false), 'updated_by' => $row->updated_by ?? null, ]; } private function date(mixed $value): string { $text = trim((string) $value); $date = CarbonImmutable::createFromFormat('!Y-m-d', $text); if (! $date instanceof CarbonImmutable || $date->format('Y-m-d') !== $text) { throw ValidationException::withMessages([ 'flow_date' => '流水日期必须使用 YYYY-MM-DD 格式。', ]); } return $text; } private function cents(mixed $value): int { return (int) round((float) $value * 100); } private function text(mixed $value): string { return preg_replace('/\s+/u', ' ', trim((string) $value)) ?? trim((string) $value); } private function nullableText(mixed $value): ?string { $text = $this->text($value); return $text !== '' ? $text : null; } }