connection = $databases->connection( (string) config('upstream_mirrors.connection', 'business_mirror'), ); } /** * @param array $data * @return array */ public function update(int $id, array $data): array { $date = $this->date($data['txn_date'] ?? null); $accountShort = $this->text($data['account_short'] ?? ''); $detail = $this->text($data['detail'] ?? ''); $incomeCents = $this->cents($data['income'] ?? 0); $expenseCents = $this->cents($data['expense'] ?? 0); $approvalNo = $this->nullableText($data['approval_no'] ?? null); if ($detail === '') { throw ValidationException::withMessages([ 'detail' => '请填写明细。', ]); } if (($incomeCents > 0) === ($expenseCents > 0)) { throw ValidationException::withMessages([ 'income' => '收入和支出只能填写一项。', ]); } return $this->connection->transaction(function () use ( $id, $date, $accountShort, $detail, $incomeCents, $expenseCents, $approvalNo, ): array { $before = $this->manualRow($id, true); $account = $this->connection->table('fin_accounts') ->where('account_short', $accountShort) ->orderBy('id') ->first(['account_short', 'company_name']); if ($account === null) { throw ValidationException::withMessages([ 'account_short' => '账户简称不存在,请从账户总表中选择。', ]); } $now = now(); $income = $incomeCents / 100; $expense = $expenseCents / 100; $after = [ 'txn_date' => $date, 'account_short' => (string) $account->account_short, 'company_name' => (string) $account->company_name, 'detail' => $detail, 'income' => $income, 'expense' => $expense, 'approval_no' => $approvalNo, ]; $this->connection->table('fin_transactions')->where('id', $id)->update([ 'txn_date' => $date, 'account_short' => $accountShort, 'detail' => $detail, 'income' => $income, 'expense' => $expense, 'approval_no' => $approvalNo, 'updated_by' => (string) (user()?->name ?? ''), 'updated_at' => $now, ]); $this->audit( 'manual_transaction_update', (string) $before['row_key'], ['before' => $before, 'after' => $after], ); $this->calculator->recalculate(); return $after + [ 'id' => $id, 'source' => FinanceLedgerTransactionWriter::MANUAL_SOURCE, ]; }); } /** * @return array */ public function delete(int $id): array { return $this->connection->transaction(function () use ($id): array { $before = $this->manualRow($id, true); $this->connection->table('fin_transactions')->where('id', $id)->delete(); $this->audit( 'manual_transaction_delete', (string) $before['row_key'], ['before' => $before, 'after' => null], ); $this->calculator->recalculate(); return $before; }); } /** * @return array */ private function manualRow(int $id, bool $lock = false): array { $query = $this->connection->table('fin_transactions') ->where('id', $id) ->where('source', FinanceLedgerTransactionWriter::MANUAL_SOURCE); if ($lock) { $query->lockForUpdate(); } $row = $query->first(); if ($row === null) { throw new NotFoundHttpException('只能编辑或删除手工录入的动账明细。'); } return [ 'id' => (int) $row->id, 'row_key' => (string) $row->row_key, 'txn_date' => substr((string) $row->txn_date, 0, 10), 'account_short' => (string) ($row->account_short ?? ''), 'detail' => (string) ($row->detail ?? ''), 'income' => round((float) $row->income, 2), 'expense' => round((float) $row->expense, 2), 'approval_no' => $row->approval_no !== null ? (string) $row->approval_no : null, ]; } /** * @param array $changes */ private function audit(string $entityType, string $rowKey, array $changes): void { $this->connection->table('fin_ledger_edit_events')->insert([ 'entity_type' => $entityType, 'entity_key' => 'manual_transaction|' . $rowKey, 'changes' => json_encode($changes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'user_id' => user()?->id, 'user_name' => (string) (user()?->name ?? ''), 'created_at' => now(), ]); } 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([ 'txn_date' => '动账日期必须使用 YYYY-MM-DD 格式。', ]); } return $text; } private function cents(mixed $value): int { return (int) round((float) $value * 100); } private function text(mixed $value): string { $text = trim((string) $value); return preg_replace('/\s+/u', ' ', $text) ?? $text; } private function nullableText(mixed $value): ?string { $text = $this->text($value); return $text !== '' ? mb_substr($text, 0, 100) : null; } }