'记录编码', 'store_name' => '原始店铺', 'ledger_remark' => '总账备注', 'custom_remark' => '自定义备注', 'bill_item' => '账单项目', 'alipay_account' => '支付宝账号', 'account_order_no' => '账务订单号', 'business_serial_no' => '业务流水号', 'merchant_order_no' => '商户订单号', 'business_base_order_no' => '业务基础订单号', 'goods_name' => '商品名称', 'counterparty_account' => '对方账号', 'occurred_at' => '发生时间', 'income' => '收入', 'expense' => '支出', 'balance' => '余额', 'business_type' => '业务类型', 'remark' => '备注', 'online_order_no' => '线上单号', 'online_real_store' => '线上单号对应实际店铺', 'business_description' => '业务描述', 'business_bill_source' => '业务账单来源', 'business_order_no' => '业务订单号', 'created_at' => '创建时间', 'updated_at' => '修改时间', 'modifier' => '修改人', ]; /** * 淘系支付宝账户账单列表(店铺收款账户账单 › 淘系支付宝账户)。 * 经由 admin 路由组(web 会话)暴露,前端带 CSRF 调用即可。 */ public function index(Request $request, StoreReceivingAlipayBillMirrorRepository $repository) { $this->authorizeStoreReceivingBills(); $sortable = ['occurred_at', 'income', 'expense', 'balance', 'record_code']; $sort = in_array($request->get('sort'), $sortable, true) ? $request->get('sort') : 'occurred_at'; $dir = $request->get('dir') === 'asc' ? 'asc' : 'desc'; $filters = $request->validate([ 'store_name' => ['nullable', 'string', 'max:255'], 'ledger_remark' => ['nullable', 'string', 'max:191'], 'source_channel' => ['nullable', 'string', 'max:128'], 'bill_item' => ['nullable', 'string', 'max:255'], 'business_type' => ['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'], ]); $filters = $this->withLedgerRemarkFilter( array_map(static fn (mixed $value): string => trim((string) $value), $filters), 'taobao-alipay', ); try { return response()->json($this->withLedgerRemarks( $repository->paginate( $filters, $sort, $dir, max((int) $request->get('page', 1), 1), min(max((int) $request->get('limit', 10), 1), 100), ), 'taobao-alipay', )); } catch (RuntimeException $exception) { if ($exception->getMessage() !== 'store_receiving_alipay_bill_mirror_not_ready') { throw $exception; } return response()->json([ 'message' => '淘系支付宝账单共享业务镜像正在首次同步,完成后将自动显示。', 'data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1, 'per_page' => min(max((int) $request->get('limit', 10), 1), 100), 'summary' => ['income' => 0, 'expense' => 0, 'balance' => 0, 'count' => 0], ], 503); } } public function filterOptions(StoreReceivingAlipayBillMirrorRepository $repository) { $this->authorizeStoreReceivingBills(); $ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('taobao-alipay'); try { return response()->json($repository->filterOptionsForChannel( 'alipayCSV', StoreReceivingAlipayBillMirrorRepository::DATASET, true ) + ['ledger_remarks' => $ledgerRemarks]); } catch (RuntimeException $exception) { if (! in_array($exception->getMessage(), [ 'store_receiving_alipay_bill_mirror_not_ready', 'store_receiving_bill_filter_options_busy', ], true)) { throw $exception; } return response()->json([ 'message' => $exception->getMessage() === 'store_receiving_bill_filter_options_busy' ? '淘系支付宝筛选选项正在生成,请稍后重试。' : '淘系支付宝账单共享业务镜像正在首次同步,筛选选项稍后可用。', 'shops' => [], 'bill_items' => [], 'ledger_remarks' => $ledgerRemarks, ], 503); } } public function export(Request $request, StoreReceivingAlipayBillMirrorRepository $repository) { $this->authorizeStoreReceivingBills(); $exportToken = $this->exportProgressToken($request); $exportTotal = max(0, (int) $request->get('total', 0)); $this->putExportProgress($exportToken, 0, $exportTotal); $filters = $request->validate([ 'store_name' => ['nullable', 'string', 'max:255'], 'ledger_remark' => ['nullable', 'string', 'max:191'], 'source_channel' => ['nullable', 'string', 'max:128'], 'bill_item' => ['nullable', 'string', 'max:255'], 'business_type' => ['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'], 'columns' => ['nullable', 'array', 'max:25'], 'columns.*' => ['string', 'max:64'], ]); $columns = $this->requestedExportColumns($filters['columns'] ?? []); unset($filters['columns']); $filters = $this->withLedgerRemarkFilter( array_map(static fn (mixed $value): string => trim((string) $value), $filters), 'taobao-alipay', ); try { $sortable = ['record_code', 'occurred_at', 'income', 'expense', 'balance']; $sort = in_array($request->get('sort'), $sortable, true) ? $request->get('sort') : 'occurred_at'; $dir = $request->get('dir') === 'asc' ? 'asc' : 'desc'; $rows = $repository->exportRows($filters, $sort, $dir); } catch (RuntimeException $exception) { if ($exception->getMessage() !== 'store_receiving_alipay_bill_mirror_not_ready') { throw $exception; } return response()->json(['message' => '淘系支付宝账单共享业务镜像正在首次同步,完成后将自动显示。'], 503); } $rows = $this->ledgerRemarkRows($rows, 'taobao-alipay'); 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, chr(0xEF) . chr(0xBB) . chr(0xBF)); fputcsv($csv, array_values($columns)); flush(); $written = 0; foreach ($rows as $row) { fputcsv($csv, array_map( fn (string $field): string => $this->csvValue($row[$field] ?? '', $field), array_keys($columns), )); $written++; if ($written % 5000 === 0) { $this->putExportProgress($exportToken, $written, $exportTotal); } } fclose($csv); $this->putExportProgress($exportToken, $written, $exportTotal, 'done'); }, 'taobao_alipay_bills.csv', [ 'Content-Type' => 'text/csv; charset=UTF-8', // 反向代理(服务器上的 nginx)不能缓冲这个响应,否则浏览器要等整个文件生成完才开始下载。 'X-Accel-Buffering' => 'no', ]); } /** @return array */ private function requestedExportColumns(array $requested): array { if ($requested === []) { return self::EXPORT_COLUMNS; } $columns = []; foreach ($requested as $field) { if (isset(self::EXPORT_COLUMNS[$field])) { $columns[$field] = self::EXPORT_COLUMNS[$field]; } } abort_if($columns === [], 422, '至少选择一个可导出的字段。'); return $columns; } private function csvValue(mixed $value, string $field): string { $text = (string) ($value ?? ''); if (preg_match('/(?:_no|_code|serial|goods_name)$/', $field) && preg_match('/^\d{12,}$/', $text)) { return "\t" . $text; } return preg_match('/^[=+\-@]/', $text) ? "'" . $text : $text; } public function show(AlipayBill $bill) { $this->authorizeStoreReceivingBills(); return response()->json($bill); } protected 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, '当前账号没有店铺收款账户账单权限。'); } }