205 lines
8.2 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Http\Controllers;
use App\Utilities\SidebarPermissions;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\EcommerceFinance\AccountBillColumns;
use Modules\EcommerceFinance\Http\Concerns\DecoratesLedgerRemarks;
use Modules\EcommerceFinance\Repositories\PinduoduoSettlementBillRepository;
use RuntimeException;
class PinduoduoAccountBills extends Controller
{
use DecoratesLedgerRemarks;
public function index(Request $request, PinduoduoSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
if ($request->boolean('export')) {
return $this->exportResponse($request, $repository);
}
$validated = $request->validate($this->filterRules() + [
'sort' => ['nullable', 'in:occurred_at,income,expense,record_code'],
'dir' => ['nullable', 'in:asc,desc'],
'page' => ['nullable', 'integer', 'min:1'],
'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
$filters = $this->withLedgerRemarkFilter($filters, 'pinduoduo');
try {
$data = $repository->paginateAccountBills(
$filters,
(string) ($validated['sort'] ?? 'occurred_at'),
(string) ($validated['dir'] ?? 'desc'),
(int) ($validated['page'] ?? 1),
(int) ($validated['limit'] ?? 10),
);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'pinduoduo_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json([
'message' => '拼多多完整货款账单尚未完成首次导入,当前不会回退到不完整的聚水潭辅助表。',
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => (int) ($validated['limit'] ?? 10),
'summary' => ['income' => 0, 'expense' => 0, 'balance' => 0, 'count' => 0],
'columns' => AccountBillColumns::for('pinduoduo'),
], 503);
}
$data['columns'] = AccountBillColumns::for('pinduoduo');
return response()->json($this->withLedgerRemarks($data, 'pinduoduo'));
}
public function filterOptions(PinduoduoSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('pinduoduo');
try {
return response()->json($repository->accountBillFilterOptions() + ['ledger_remarks' => $ledgerRemarks]);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'pinduoduo_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json([
'message' => '拼多多完整货款账单尚未完成首次导入。',
'shops' => [],
'bill_items' => [],
'ledger_remarks' => $ledgerRemarks,
], 503);
}
}
public function export(Request $request, PinduoduoSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
return $this->exportResponse($request, $repository);
}
private function exportResponse(Request $request, PinduoduoSettlementBillRepository $repository)
{
$exportToken = $this->exportProgressToken($request);
$exportTotal = max(0, (int) $request->get('total', 0));
$this->putExportProgress($exportToken, 0, $exportTotal);
$validated = $request->validate($this->filterRules() + [
'sort' => ['nullable', 'in:occurred_at,income,expense,record_code'],
'dir' => ['nullable', 'in:asc,desc'],
]);
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
$filters = $this->withLedgerRemarkFilter($filters, 'pinduoduo');
$definition = AccountBillColumns::for('pinduoduo');
$available = collect($definition['columns'] ?? [])->keyBy('field');
$requested = $request->query('columns', []);
if (is_string($requested)) {
$requested = array_filter(array_map('trim', explode(',', $requested)));
}
if (! is_array($requested) || count($requested) > 40) {
$requested = [];
}
$requested = collect($requested)
->filter(static fn (mixed $value): bool => is_scalar($value))
->map(static fn (mixed $value): string => mb_substr(trim((string) $value), 0, 128))
->filter()
->unique();
$columns = $requested->isEmpty()
? $available->values()
: $requested->map(fn (string $field) => $available->get($field))->filter()->values();
abort_if($columns->isEmpty(), 422, '至少选择一个可导出的字段。');
try {
$rows = $repository->exportAccountBills(
$filters,
(string) ($validated['sort'] ?? 'occurred_at'),
(string) ($validated['dir'] ?? 'asc'),
);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'pinduoduo_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json(['message' => '拼多多完整货款账单尚未完成首次导入。'], 503);
}
$rows = $this->ledgerRemarkRows($rows, 'pinduoduo');
return response()->streamDownload(function () use ($rows, $columns, $exportToken, $exportTotal): void {
// 大账期逐行生成较慢:先发文件头并关掉输出缓冲,让浏览器立刻开始下载。
while (ob_get_level() > 0) {
ob_end_flush();
}
$csv = fopen('php://output', 'w');
fputs($csv, "\xEF\xBB\xBF");
fputcsv($csv, $columns->pluck('label')->all());
flush();
$written = 0;
foreach ($rows as $row) {
fputcsv($csv, $columns->map(
fn (array $column): string => $this->exportValue($row, $column)
)->all());
$written++;
if ($written % 5000 === 0) {
$this->putExportProgress($exportToken, $written, $exportTotal);
}
}
fclose($csv);
$this->putExportProgress($exportToken, $written, $exportTotal, 'done');
}, 'pinduoduo_account_bills.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
'Cache-Control' => 'private, no-store',
'X-Accel-Buffering' => 'no',
]);
}
/** @return array<string,array<int,string>> */
private function filterRules(): array
{
return [
'store_name' => ['nullable', 'string', 'max:255'],
'ledger_remark' => ['nullable', 'string', 'max:191'],
'bill_item' => ['nullable', 'string', 'max:255'],
'start' => ['nullable', 'date_format:Y-m-d'],
'end' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start'],
'keyword' => ['nullable', 'string', 'max:255'],
];
}
/** @param array<string,mixed> $row */
private function exportValue(array $row, array $column): string
{
$field = (string) ($column['field'] ?? '');
$value = str_starts_with($field, 'payload.')
? data_get($row['payload'] ?? [], substr($field, 8))
: data_get($row, $field);
$text = (string) ($value ?? '');
if (preg_match('/^\d{12,}$/', $text) === 1) {
return '="' . $text . '"';
}
return preg_match('/^[=+\-@]/u', $text) === 1 ? "'" . $text : $text;
}
private function authorizeStoreReceivingBills(): void
{
$section = collect(SidebarPermissions::navigationFor(user()))
->firstWhere('slug', 'billing-transactions');
$page = collect($section['pages'] ?? [])
->firstWhere('slug', 'store-receiving-bills');
abort_if(empty($page), 403, '当前账号没有店铺收款账户账单权限。');
}
}