diff --git a/modules/EcommerceFinance/Http/Controllers/AccountBills.php b/modules/EcommerceFinance/Http/Controllers/AccountBills.php index 1b8e5104..beccc706 100644 --- a/modules/EcommerceFinance/Http/Controllers/AccountBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AccountBills.php @@ -25,6 +25,17 @@ class AccountBills extends Controller abort(404, '未知的账户类型。'); } + $request->validate([ + 'store_name' => ['nullable', 'string', 'max:255'], + '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'], + 'export' => ['nullable', 'boolean'], + 'columns' => ['nullable', 'array', 'max:40'], + 'columns.*' => ['string', 'max:128'], + ]); + $query = AccountBill::query() ->where('company_id', company_id()) ->where('bill_type', $billType); @@ -60,6 +71,10 @@ class AccountBills extends Controller $dir = $request->get('dir') === 'asc' ? 'asc' : 'desc'; $query->orderBy($sort, $dir); + if ($request->boolean('export')) { + return $this->export($request, $query, $billType); + } + $perPage = min(max((int) $request->get('limit', 10), 1), 100); $rows = $query->paginate($perPage); @@ -87,6 +102,52 @@ class AccountBills extends Controller return response()->json($data); } + private function export(Request $request, $query, string $billType) + { + $definition = AccountBillColumns::for($billType); + $available = collect($definition['columns'] ?? [])->keyBy('field'); + $requested = $request->input('columns', []); + $columns = $requested === [] + ? $available->values() + : collect($requested)->unique()->map(fn (string $field) => $available->get($field))->filter()->values(); + + abort_if($columns->isEmpty(), 422, '至少选择一个可导出的字段。'); + + return response()->streamDownload(function () use ($query, $columns): void { + $csv = fopen('php://output', 'w'); + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fputcsv($csv, $columns->pluck('label')->all()); + + foreach ($query->cursor() as $row) { + fputcsv($csv, $columns->map(function (array $column) use ($row): string { + $value = $this->columnValue($row, $column['field']); + + return $this->csvValue($value, $column['kind']); + })->all()); + } + fclose($csv); + }, $billType . '_account_bills.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); + } + + private function columnValue(AccountBill $row, string $field): mixed + { + if (str_starts_with($field, 'payload.')) { + return data_get($row->payload ?? [], substr($field, 8), ''); + } + + return $row->getAttribute($field) ?? ''; + } + + private function csvValue(mixed $value, string $kind): string + { + $text = (string) ($value ?? ''); + if ($kind === 'num' && preg_match('/^\d{12,}$/', $text)) { + return "\t" . $text; + } + + return preg_match('/^[=+\-@]/', $text) ? "'" . $text : $text; + } + public function show(AccountBill $bill) { $this->authorizeStoreReceivingBills(); diff --git a/modules/EcommerceFinance/Http/Controllers/AlipayBills.php b/modules/EcommerceFinance/Http/Controllers/AlipayBills.php index 7cdcf6bc..14471a83 100644 --- a/modules/EcommerceFinance/Http/Controllers/AlipayBills.php +++ b/modules/EcommerceFinance/Http/Controllers/AlipayBills.php @@ -11,6 +11,34 @@ use RuntimeException; class AlipayBills extends Controller { + private const EXPORT_COLUMNS = [ + 'record_code' => '记录编码', + 'store_name' => '原始店铺', + '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 调用即可。 @@ -68,8 +96,13 @@ class AlipayBills extends Controller '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']); + try { $rows = $repository->exportRows(array_map(static fn (mixed $value): string => trim((string) $value), $filters)); } catch (RuntimeException $exception) { @@ -80,17 +113,49 @@ class AlipayBills extends Controller return response()->json(['message' => '淘系支付宝账单共享业务镜像正在首次同步,完成后将自动显示。'], 503); } - return response()->streamDownload(function () use ($rows): void { + return response()->streamDownload(function () use ($rows, $columns): void { $csv = fopen('php://output', 'w'); fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); - fputcsv($csv, ['记录编码', '原始店铺', '平台渠道', '账单项目', '账务订单号', '业务流水号', '商户订单号', '业务基础订单号', '商品编码', '发生时间', '收入(元)', '支出(元)', '收支净额(元)', '收支类型', '线上单号', '业务描述', '业务账单来源', '创建时间', '修改时间']); + fputcsv($csv, array_values($columns)); foreach ($rows as $row) { - fputcsv($csv, [$row['record_code'], $row['store_name'], $row['alipay_account'], $row['bill_item'], $row['account_order_no'], $row['business_serial_no'], $row['merchant_order_no'], $row['business_base_order_no'], $row['goods_name'], $row['occurred_at'], $row['income'], $row['expense'], $row['balance'], $row['business_type'], $row['online_order_no'], $row['business_description'], $row['business_bill_source'], $row['created_at'], $row['updated_at']]); + fputcsv($csv, array_map( + fn (string $field): string => $this->csvValue($row[$field] ?? '', $field), + array_keys($columns), + )); } fclose($csv); }, 'taobao_alipay_bills.csv', ['Content-Type' => 'text/csv; charset=UTF-8']); } + /** @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(); diff --git a/resources/assets/js/components/business/AccountBills.vue b/resources/assets/js/components/business/AccountBills.vue index 9a3dea5b..a32aed35 100644 --- a/resources/assets/js/components/business/AccountBills.vue +++ b/resources/assets/js/components/business/AccountBills.vue @@ -1,342 +1,72 @@ diff --git a/resources/assets/js/components/business/TableColumnSettings.vue b/resources/assets/js/components/business/TableColumnSettings.vue new file mode 100644 index 00000000..938a7441 --- /dev/null +++ b/resources/assets/js/components/business/TableColumnSettings.vue @@ -0,0 +1,224 @@ + + + + + diff --git a/resources/assets/js/components/business/TaobaoAlipayBills.vue b/resources/assets/js/components/business/TaobaoAlipayBills.vue index 6b5d9c6a..096997ae 100644 --- a/resources/assets/js/components/business/TaobaoAlipayBills.vue +++ b/resources/assets/js/components/business/TaobaoAlipayBills.vue @@ -1,482 +1,119 @@ diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php index 416d3a0f..540626c1 100644 --- a/resources/views/business/pages/show.blade.php +++ b/resources/views/business/pages/show.blade.php @@ -1060,6 +1060,7 @@ 'biz-card mt-6', 'business-blank-page' => ($page['slug'] ?? null) === 'store-receiving-bills', ]) + @if (($page['slug'] ?? null) === 'store-receiving-bills') data-table-preferences-user="{{ user()->id }}" @endif aria-label="{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}" >