CaiWuXiTong/modules/EcommerceFinance/View/FinanceLedgerPageData.php
92451 a8c2169134 调整财务台账店铺表头 (#629)
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-26 18:43:44 +08:00

804 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\View;
use App\Utilities\FinanceLedgerAccountStatus;
use App\Utilities\FinanceLedgerAccountShouldKeep;
use App\Utilities\FinanceLedgerAccountType;
use App\Utilities\FinanceLedgerAccess;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
/**
* 财务台账页面数据。
*
* 所有页签一次性读取,页签切换只做前端显示/隐藏;各页签使用独立的分页
* 参数,避免在同一 URL 上互相覆盖。
*/
class FinanceLedgerPageData
{
public const TABS = [
'accounts' => ['title' => '账户总表', 'icon' => 'account_balance'],
'shops' => ['title' => '店铺台账', 'icon' => 'storefront'],
'flows' => ['title' => '每日资金流水', 'icon' => 'waterfall_chart'],
'transactions' => ['title' => '动账明细', 'icon' => 'receipt_long'],
'mapping' => ['title' => '账户映射', 'icon' => 'alt_route'],
'sources' => ['title' => '数据来源', 'icon' => 'database'],
];
private const ACCOUNT_FILTER_EMPTY = '__empty__';
public function page(?string $tab = null): array
{
$tab = (string) (request('tab') ?: $tab);
$tab = array_key_exists($tab, self::TABS) ? $tab : 'accounts';
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
$accounts = $this->accounts();
$shops = $this->shops();
$flows = $this->flows();
$transactions = $this->transactions();
return [
'view' => 'business.pages.financial-ledger.ledger',
'ledgerTab' => $tab,
'ledgerTabs' => self::TABS,
'ledgerOpeningDate' => $openingDate,
'ledgerAccounts' => $accounts['rows'],
'ledgerAccountSummary' => $accounts['summary'],
'ledgerShops' => $shops['rows'],
'ledgerShopSummary' => $shops['summary'],
'ledgerShopBalanceDate' => $this->latestShopBalanceDate(),
'ledgerFlows' => $flows['rows'],
'ledgerFlowSummary' => $flows['summary'],
'ledgerTransactions' => $transactions['rows'],
'ledgerTransactionSummary' => $transactions['summary'],
'ledgerMapping' => $this->mapping(),
'ledgerSourceSummary' => $this->sourceSummary(),
'ledgerMeta' => $this->meta(),
'ledgerAccountOptions' => $this->accountOptions(),
'ledgerManualTransactionAccountOptions' => $this->manualTransactionAccountOptions(),
'ledgerTransactionAccountHeaderOptions' => $this->transactionAccountHeaderOptions(),
'ledgerTransactionDateHeaderOptions' => $this->transactionDateHeaderOptions(),
'ledgerAccountFilterOptions' => $this->accountFilterOptions(),
'ledgerCompanyOptions' => $this->companyOptions(),
'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS,
'ledgerAccountPlanOptions' => $this->accountPlanOptions(),
'ledgerAccountKeepOptions' => FinanceLedgerAccountShouldKeep::OPTIONS,
'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()),
'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()),
'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()),
];
}
private function accounts(): array
{
$query = $this->db()->table('fin_accounts as a');
$keyword = trim((string) request('accounts_q', ''));
if ($keyword !== '') {
$query->where(function (Builder $builder) use ($keyword): void {
foreach (['a.company_name', 'a.account_short', 'a.account_no', 'a.owner'] as $column) {
$builder->orWhere($column, 'like', '%' . $keyword . '%');
}
});
}
$accountShort = trim((string) request('accounts_short', ''));
if ($accountShort !== '') {
$query->where('a.account_short', 'like', '%' . $accountShort . '%');
}
$status = trim((string) request('accounts_status', ''));
if ($status !== '' && array_key_exists($status, FinanceLedgerAccountStatus::OPTIONS)) {
if ($status === FinanceLedgerAccountStatus::NORMAL) {
$nonNormalStatuses = array_values(array_diff(
array_keys(FinanceLedgerAccountStatus::OPTIONS),
[FinanceLedgerAccountStatus::NORMAL],
));
$query->whereNotIn(
'a.account_short',
$this->db()->table('fin_account_statuses')
->select('account_short')
->whereIn('status', $nonNormalStatuses),
);
} else {
$query->whereIn(
'a.account_short',
$this->db()->table('fin_account_statuses')
->select('account_short')
->where('status', $status),
);
}
}
$region = trim((string) request('accounts_region', ''));
if ($region !== '') {
if ($region === self::ACCOUNT_FILTER_EMPTY) {
$query->where(function (Builder $builder): void {
$builder->whereNull('a.region')->orWhere('a.region', '');
});
} else {
$query->where('a.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(
'a.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('a.account_type')->orWhere('a.account_type', '');
});
} else {
$baseType->where('a.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('a.account_short', $overrideType);
});
}
$balanceRange = trim((string) request('accounts_balance', ''));
if ($balanceRange === '0-5') {
$query->where('a.current_balance', '>=', 0)->where('a.current_balance', '<', 50000);
} elseif ($balanceRange === '5-20') {
$query->where('a.current_balance', '>=', 50000)->where('a.current_balance', '<', 200000);
} elseif ($balanceRange === '20-plus') {
$query->where('a.current_balance', '>=', 200000);
}
$headerColumns = $this->accountHeaderColumns();
$this->applyLedgerHeaderFilter($query, 'accounts', $headerColumns);
$summaryQuery = clone $query;
$summary = [
'count' => (clone $query)->count(),
'unretained' => FinanceLedgerAccountShouldKeep::applyUnretainedCondition(
clone $query,
'a.account_should_keep',
'a.company_plan',
)->count(),
'current_balance' => round((float) (clone $summaryQuery)->sum('a.current_balance'), 2),
'balance' => round((float) (clone $summaryQuery)->sum('a.daily_available'), 2),
'blocked' => round((float) (clone $summaryQuery)->sum('a.blocked_amount'), 2),
];
$sorted = $this->applyLedgerHeaderSort($query, 'accounts', $headerColumns);
if (! $sorted) {
$query->orderBy('a.company_name')->orderBy('a.account_short');
}
$rows = $query
->paginate(100, ['*'], 'accounts_page')
->withQueryString();
$accountShorts = $rows->pluck('account_short')->filter()->unique()->values()->all();
$statuses = $accountShorts === []
? collect()
: $this->db()->table('fin_account_statuses')
->whereIn('account_short', $accountShorts)
->pluck('status', 'account_short');
$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) ?? '');
}
$row->account_should_keep = FinanceLedgerAccountShouldKeep::display(
$row->account_should_keep ?? null,
$row->company_plan ?? null,
);
return $row;
});
return ['rows' => $rows, 'summary' => $summary];
}
private function shops(): array
{
$query = $this->db()->table('fin_shops');
foreach (['shops_q' => ['shop_name', 'platform', 'company_name'], 'shops_platform' => ['platform'], 'shops_company' => ['company_name']] as $param => $columns) {
$value = trim((string) request($param, ''));
if ($value === '') {
continue;
}
$query->where(function (Builder $builder) use ($columns, $value): void {
foreach ($columns as $column) {
$builder->orWhere($column, 'like', '%' . $value . '%');
}
});
}
$headerColumns = $this->shopHeaderColumns();
$this->applyLedgerHeaderFilter($query, 'shops', $headerColumns);
$summary = [
'count' => (clone $query)->count(),
'balance' => round((float) (clone $query)->sum('shop_balance'), 2),
];
$sorted = $this->applyLedgerHeaderSort($query, 'shops', $headerColumns);
if (! $sorted) {
$query->orderBy('company_name')->orderBy('shop_name');
}
return [
'rows' => $query
->paginate(100, ['*'], 'shops_page')->withQueryString(),
'summary' => $summary,
];
}
private function flows(): array
{
$query = $this->db()->table('fin_daily_flows');
foreach (['flows_date' => 'flow_date', 'flows_company' => 'company_name', 'flows_account' => 'account_short'] as $param => $column) {
$value = trim((string) request($param, ''));
if ($value !== '') {
$query->where($column, $param === 'flows_company' || $param === 'flows_account' ? 'like' : '=', $param === 'flows_company' || $param === 'flows_account' ? '%' . $value . '%' : $value);
}
}
$headerColumns = $this->flowHeaderColumns();
$this->applyLedgerHeaderFilter($query, 'flows', $headerColumns);
$summary = [
'count' => (clone $query)->count(),
'closing' => round((float) (clone $query)->sum('closing_balance'), 2),
'inflow' => round((float) (clone $query)->sum('inflow'), 2),
'outflow' => round((float) (clone $query)->sum('outflow'), 2),
];
$sorted = $this->applyLedgerHeaderSort($query, 'flows', $headerColumns);
if (! $sorted) {
$query->orderBy('flow_date')->orderBy('account_short');
}
return [
'rows' => $query
->paginate(100, ['*'], 'flows_page')->withQueryString(),
'summary' => $summary,
];
}
private function transactions(): array
{
$query = $this->transactionQuery();
$this->applyTransactionFilters($query);
$headerColumns = $this->transactionHeaderColumns();
$summary = $this->transactionSummaryMetrics($query);
$sorted = $this->applyLedgerHeaderSort($query, 'transactions', $headerColumns);
if (! $sorted) {
$query->orderByDesc('t.txn_date')->orderByDesc('t.id');
}
return [
'rows' => $query
->paginate(100, ['*'], 'txns_page')->withQueryString(),
'summary' => $summary,
];
}
public function transactionSummary(): array
{
$query = $this->transactionQuery();
$this->applyTransactionFilters($query);
return $this->transactionSummaryMetrics($query);
}
private function applyTransactionFilters(Builder $query): void
{
$keyword = trim((string) request('txns_q', ''));
if ($keyword !== '') {
$query->where(function (Builder $builder) use ($keyword): void {
$builder->where('t.detail', 'like', '%' . $keyword . '%')
->orWhere('t.approval_no', 'like', '%' . $keyword . '%')
->orWhere('t.account_short', 'like', '%' . $keyword . '%')
->orWhere('m.account_short', 'like', '%' . $keyword . '%');
});
}
$account = trim((string) request('txns_account', ''));
if ($account !== '') {
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
$query->whereRaw($effectiveAccount . ' LIKE ?', ['%' . $account . '%']);
}
$from = trim((string) request('txns_from', ''));
if ($from !== '') {
$query->where('t.txn_date', '>=', $from);
}
$to = trim((string) request('txns_to', ''));
if ($to !== '') {
$query->where('t.txn_date', '<=', $to);
}
$source = trim((string) request('txns_source', ''));
if ($source !== '') {
$query->where('t.source', $source);
}
$this->applyLedgerHeaderFilter($query, 'transactions', $this->transactionHeaderColumns());
}
private function transactionSummaryMetrics(Builder $query): array
{
return [
'count' => (clone $query)->count(),
'income' => round((float) (clone $query)->sum('t.income'), 2),
'expense' => round((float) (clone $query)->sum('t.expense'), 2),
];
}
private function transactionQuery(): Builder
{
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
return $this->db()->table('fin_transactions as t')
->leftJoin('fin_account_map as m', function ($join): void {
$join->on('m.source', '=', 't.source')->on('m.source_value', '=', 't.source_value');
})
->select([
't.id',
't.txn_date',
't.account_short',
't.detail',
't.income',
't.expense',
't.approval_no',
't.source',
't.source_value',
't.process_type',
't.row_key',
't.synced_at',
't.created_at',
't.updated_at',
])
->selectRaw($effectiveAccount . ' AS effective_account_short');
}
private function transactionAccountHeaderOptions(): array
{
$effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort'];
return $this->db()->table('fin_transactions as t')
->leftJoin('fin_account_map as m', function ($join): void {
$join->on('m.source', '=', 't.source')
->on('m.source_value', '=', 't.source_value');
})
->selectRaw($effectiveAccount . ' AS filter_value')
->distinct()
->orderBy('filter_value')
->pluck('filter_value')
->map(static fn (mixed $value): string => trim((string) $value))
->filter()
->unique()
->mapWithKeys(static fn (string $value): array => [$value => $value])
->all();
}
private function transactionDateHeaderOptions(): array
{
return $this->db()->table('fin_transactions')
->select('txn_date')
->distinct()
->orderByDesc('txn_date')
->pluck('txn_date')
->map(static fn (mixed $value): string => (string) $value)
->filter()
->unique()
->mapWithKeys(static fn (string $value): array => [$value => $value])
->all();
}
private function mapping(): array
{
$query = $this->db()->table('fin_account_map');
$source = trim((string) request('mapping_source', ''));
if ($source !== '') {
$query->where('source', $source);
}
$keyword = trim((string) request('mapping_q', ''));
if ($keyword !== '') {
$query->where(function (Builder $builder) use ($keyword): void {
$builder->where('source_value', 'like', '%' . $keyword . '%')
->orWhere('account_short', 'like', '%' . $keyword . '%');
});
}
if (request()->boolean('mapping_unmapped')) {
$query->where(function (Builder $builder): void {
$builder->whereNull('account_short')->orWhere('account_short', '');
});
}
$headerColumns = $this->mappingHeaderColumns();
$this->applyLedgerHeaderFilter($query, 'mapping', $headerColumns);
$total = (clone $query)->count();
$mapped = (clone $query)->whereNotNull('account_short')->where('account_short', '<>', '')->count();
$sorted = $this->applyLedgerHeaderSort($query, 'mapping', $headerColumns);
if (! $sorted) {
$query->orderByDesc('txn_count')->orderBy('source_value');
}
return [
'rows' => $query
->paginate(100, ['*'], 'mapping_page')->withQueryString(),
'total' => $total,
'mapped' => $mapped,
'all_total' => $this->db()->table('fin_account_map')->count(),
'all_mapped' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(),
'sources' => $this->db()->table('fin_account_map')->select('source')->distinct()->orderBy('source')->pluck('source'),
];
}
private function sourceSummary(): array
{
$rows = $this->db()->table('fin_transactions')
->selectRaw('source, process_type, COUNT(*) AS rows_count, SUM(income) AS income, SUM(expense) AS expense, MIN(txn_date) AS first_date, MAX(txn_date) AS last_date')
->groupBy('source', 'process_type')
->orderBy('source')
->orderByDesc('rows_count')
->get()
->map(static function ($row): array {
$data = (array) $row;
$data['date_range'] = trim((string) ($data['first_date'] ?? '') . ' ~ ' . (string) ($data['last_date'] ?? ''));
return $data;
})
->all();
return $this->filterAndSortHeaderRows($rows, 'sources', $this->sourceHeaderColumns());
}
private function meta(): array
{
return [
'accounts' => $this->db()->table('fin_accounts')->count(),
'shops' => $this->db()->table('fin_shops')->count(),
'companies' => $this->db()->table('fin_companies')->count(),
'opening_rows' => $this->db()->table('fin_account_opening')->count(),
'opening_sum' => round((float) $this->db()->table('fin_account_opening')->sum('opening_balance'), 2),
'transactions' => $this->db()->table('fin_transactions')->count(),
'income' => round((float) $this->db()->table('fin_transactions')->sum('income'), 2),
'expense' => round((float) $this->db()->table('fin_transactions')->sum('expense'), 2),
'flows' => $this->db()->table('fin_daily_flows')->count(),
'mapping_total' => $this->db()->table('fin_account_map')->count(),
'mapping_done' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(),
'last_synced_at' => $this->db()->table('fin_transactions')->max('synced_at'),
];
}
private function latestShopBalanceDate(): ?string
{
if (! Schema::connection($this->db()->getName())->hasTable(JushuitanShopBalanceRepository::TABLE)) {
return null;
}
$latest = $this->db()->table(JushuitanShopBalanceRepository::TABLE)
->where('balance_date', '<=', now()->startOfDay()->subDay()->toDateString())
->max('balance_date');
return $latest === null ? null : substr((string) $latest, 0, 10);
}
private function accountOptions(): array
{
return $this->db()->table('fin_accounts')
->select('account_short')->distinct()->orderBy('account_short')->pluck('account_short')->all();
}
private function manualTransactionAccountOptions(): array
{
return $this->db()->table('fin_accounts')
->select(['account_short', 'company_name'])
->whereNotNull('account_short')
->where('account_short', '<>', '')
->orderBy('account_short')
->orderBy('company_name')
->orderBy('id')
->get()
->groupBy('account_short')
->map(static function ($rows, string $accountShort): array {
$first = $rows->first();
$companyName = trim((string) ($first->company_name ?? ''));
$accountCount = $rows->count();
return [
'value' => $accountShort,
'label' => $accountShort
. ($companyName !== '' ? ' · ' . $companyName : '')
. ($accountCount > 1 ? '(' . $accountCount . ' 个账号)' : ''),
];
})
->values()
->all();
}
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 = FinanceLedgerAccountType::options($baseTypes, $overrideTypes);
return [
'regions' => $this->db()->table('fin_accounts')
->whereNotNull('region')->where('region', '<>', '')
->distinct()->orderBy('region')->pluck('region')->all(),
'types' => $types,
];
}
private function accountPlanOptions(): array
{
return $this->db()->table('fin_accounts')
->whereNotNull('company_plan')
->where('company_plan', '<>', '')
->distinct()
->orderBy('company_plan')
->pluck('company_plan')
->map(static fn (mixed $value): string => trim((string) $value))
->filter()
->unique()
->values()
->all();
}
private function companyOptions(): array
{
return $this->db()->table('fin_companies')
->whereNotNull('name')
->pluck('name')
->merge(
$this->db()->table('fin_accounts')
->whereNotNull('company_name')
->distinct()
->pluck('company_name'),
)
->map(static fn (mixed $value): string => trim((string) $value))
->filter()
->unique()
->sortBy(static fn (string $value): string => mb_strtolower($value))
->values()
->all();
}
private function applyLedgerHeaderFilter(Builder $query, string $tab, array $columns): void
{
$column = trim((string) request($tab . '_filter_column', ''));
$value = trim((string) request($tab . '_filter_value', ''));
if ($value === '' || ! isset($columns[$column])) {
return;
}
$definition = $columns[$column];
$expression = $definition['filter'] ?? $definition['sort'] ?? null;
if (! is_string($expression) || $expression === '') {
return;
}
$type = $definition['type'] ?? 'text';
if ($type === 'number') {
if (is_numeric($value)) {
$query->whereRaw($expression . ' = ?', [$value]);
}
return;
}
if (in_array($type, ['enum', 'date'], true)) {
$query->whereRaw($expression . ' = ?', [$value]);
return;
}
$query->whereRaw($expression . ' LIKE ?', ['%' . $value . '%']);
}
private function applyLedgerHeaderSort(Builder $query, string $tab, array $columns): bool
{
$column = trim((string) request($tab . '_sort', ''));
if (! isset($columns[$column])) {
return false;
}
$expression = $columns[$column]['sort'] ?? null;
if (! is_string($expression) || $expression === '') {
return false;
}
$direction = strtolower((string) request($tab . '_direction', 'asc')) === 'desc' ? 'desc' : 'asc';
$query->orderByRaw($expression . ' ' . $direction);
return true;
}
private function filterAndSortHeaderRows(array $rows, string $tab, array $columns): array
{
$filterColumn = trim((string) request($tab . '_filter_column', ''));
$filterValue = trim((string) request($tab . '_filter_value', ''));
if ($filterValue !== '' && isset($columns[$filterColumn])) {
$valueColumn = $columns[$filterColumn]['value'] ?? $filterColumn;
$type = $columns[$filterColumn]['type'] ?? 'text';
$rows = array_values(array_filter(
$rows,
static function (array $row) use ($valueColumn, $filterValue, $type): bool {
$value = trim((string) ($row[$valueColumn] ?? ''));
if ($type === 'number') {
return is_numeric($filterValue) && is_numeric($value) && (float) $value === (float) $filterValue;
}
if (in_array($type, ['enum', 'date'], true)) {
return $value === $filterValue;
}
return mb_stripos($value, $filterValue) !== false;
},
));
}
$sortColumn = trim((string) request($tab . '_sort', ''));
if (isset($columns[$sortColumn])) {
$valueColumn = $columns[$sortColumn]['value'] ?? $sortColumn;
$direction = strtolower((string) request($tab . '_direction', 'asc')) === 'desc' ? -1 : 1;
$type = $columns[$sortColumn]['type'] ?? 'text';
usort($rows, static function (array $left, array $right) use ($valueColumn, $direction, $type): int {
$leftValue = $left[$valueColumn] ?? null;
$rightValue = $right[$valueColumn] ?? null;
if ($type === 'number') {
return ((float) $leftValue <=> (float) $rightValue) * $direction;
}
return strnatcasecmp((string) $leftValue, (string) $rightValue) * $direction;
});
}
return $rows;
}
private function accountHeaderColumns(): array
{
$a = $this->queryAlias('a');
$statusTable = $this->tableName('fin_account_statuses');
$typeTable = $this->tableName('fin_account_type_overrides');
$statusExpression = "COALESCE((SELECT NULLIF(s.status, '') FROM {$statusTable} s WHERE s.account_short = {$a}.account_short LIMIT 1), 'normal')";
$typeExpression = "COALESCE((SELECT NULLIF(o.account_type, '') FROM {$typeTable} o WHERE o.account_short = {$a}.account_short LIMIT 1), NULLIF({$a}.account_type, ''), '')";
$keepExpression = "COALESCE(NULLIF({$a}.account_should_keep, ''), CASE WHEN COALESCE({$a}.company_plan, '') = '' THEN '' WHEN {$a}.company_plan LIKE '%保留%' THEN '是' ELSE '否' END)";
return [
'company_name' => ['sort' => $a . '.company_name', 'type' => 'text'],
'account_short' => ['sort' => $a . '.account_short', 'type' => 'text'],
'account_type' => ['sort' => $typeExpression, 'type' => 'enum'],
'account_status' => ['sort' => $statusExpression, 'type' => 'enum'],
'region' => ['sort' => $a . '.region', 'type' => 'text'],
'account_no' => ['sort' => $a . '.account_no', 'type' => 'text'],
'bank_name' => ['sort' => $a . '.bank_name', 'type' => 'text'],
'bank_city_branch' => ['sort' => $a . '.bank_city_branch', 'type' => 'text'],
'company_plan' => ['sort' => $a . '.company_plan', 'type' => 'enum'],
'account_should_keep' => ['sort' => $keepExpression, 'type' => 'enum'],
'account_last4' => ['sort' => $a . '.account_last4', 'type' => 'text'],
'single_limit' => ['sort' => "CAST({$a}.single_limit AS DECIMAL(18, 2))", 'filter' => $a . '.single_limit', 'type' => 'number'],
'daily_limit' => ['sort' => "CAST({$a}.daily_limit AS DECIMAL(18, 2))", 'filter' => $a . '.daily_limit', 'type' => 'number'],
'monthly_limit' => ['sort' => "CAST({$a}.monthly_limit AS DECIMAL(18, 2))", 'filter' => $a . '.monthly_limit', 'type' => 'number'],
'yearly_limit' => ['sort' => "CAST({$a}.yearly_limit AS DECIMAL(18, 2))", 'filter' => $a . '.yearly_limit', 'type' => 'number'],
'current_balance' => ['sort' => $a . '.current_balance', 'type' => 'number'],
'daily_available' => ['sort' => $a . '.daily_available', 'type' => 'number'],
'blocked_amount' => ['sort' => $a . '.blocked_amount', 'type' => 'number'],
'daily_withdraw' => ['sort' => $a . '.daily_available', 'type' => 'number'],
'clearing_days' => ['sort' => $a . '.clearing_days', 'type' => 'number'],
'raise_target' => ['sort' => "CAST({$a}.raise_target AS DECIMAL(18, 2))", 'filter' => $a . '.raise_target', 'type' => 'number'],
'raise_status' => ['sort' => $a . '.raise_status', 'type' => 'text'],
'raise_actions' => ['sort' => $a . '.raise_actions', 'type' => 'text'],
'done_actions' => ['sort' => $a . '.done_actions', 'type' => 'text'],
'next_action' => ['sort' => $a . '.next_action', 'type' => 'text'],
'remark' => ['sort' => $a . '.remark', 'type' => 'text'],
'owner' => ['sort' => $a . '.owner', 'type' => 'text'],
];
}
private function shopHeaderColumns(): array
{
return [
'platform' => ['sort' => 'platform', 'type' => 'text'],
'shop_name' => ['sort' => 'shop_name', 'type' => 'text'],
'company_name' => ['sort' => 'company_name', 'type' => 'text'],
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
'status' => ['sort' => 'status', 'type' => 'text'],
'payout_account' => ['sort' => 'payout_account', 'type' => 'text'],
'shop_balance' => ['sort' => 'shop_balance', 'type' => 'number'],
'remark' => ['sort' => 'remark', 'type' => 'text'],
];
}
private function flowHeaderColumns(): array
{
return [
'flow_date' => ['sort' => 'flow_date', 'type' => 'date'],
'company_name' => ['sort' => 'company_name', 'type' => 'text'],
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
'opening_balance' => ['sort' => 'opening_balance', 'type' => 'number'],
'inflow' => ['sort' => 'inflow', 'type' => 'number'],
'outflow' => ['sort' => 'outflow', 'type' => 'number'],
'closing_balance' => ['sort' => 'closing_balance', 'type' => 'number'],
'remark' => ['sort' => 'remark', 'type' => 'text'],
];
}
private function transactionHeaderColumns(): array
{
$t = $this->queryAlias('t');
$m = $this->queryAlias('m');
$effectiveAccount = "COALESCE(NULLIF({$m}.account_short, ''), NULLIF({$t}.account_short, ''), '未匹配')";
return [
'txn_date' => ['sort' => $t . '.txn_date', 'type' => 'date'],
'effective_account_short' => ['sort' => $effectiveAccount, 'type' => 'text'],
'detail' => ['sort' => $t . '.detail', 'type' => 'text'],
'source' => ['sort' => $t . '.source', 'type' => 'enum'],
'income' => ['sort' => $t . '.income', 'type' => 'number'],
'expense' => ['sort' => $t . '.expense', 'type' => 'number'],
'approval_no' => ['sort' => $t . '.approval_no', 'type' => 'text'],
];
}
private function mappingHeaderColumns(): array
{
$mappingStatus = "CASE WHEN COALESCE(account_short, '') = '' THEN 'unmapped' ELSE 'mapped' END";
return [
'source' => ['sort' => 'source', 'type' => 'enum'],
'source_value' => ['sort' => 'source_value', 'type' => 'text'],
'txn_count' => ['sort' => 'txn_count', 'type' => 'number'],
'income_sum' => ['sort' => 'income_sum', 'type' => 'number'],
'expense_sum' => ['sort' => 'expense_sum', 'type' => 'number'],
'account_short' => ['sort' => 'account_short', 'type' => 'text'],
'mapping_status' => ['sort' => $mappingStatus, 'type' => 'enum'],
];
}
private function sourceHeaderColumns(): array
{
return [
'source' => ['value' => 'source', 'type' => 'enum'],
'process_type' => ['value' => 'process_type', 'type' => 'text'],
'rows_count' => ['value' => 'rows_count', 'type' => 'number'],
'income' => ['value' => 'income', 'type' => 'number'],
'expense' => ['value' => 'expense', 'type' => 'number'],
'date_range' => ['value' => 'date_range', 'type' => 'text'],
];
}
private function db()
{
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
private function tableName(string $table): string
{
return $this->db()->getTablePrefix() . $table;
}
private function queryAlias(string $alias): string
{
return $this->db()->getTablePrefix() . $alias;
}
}