WuYongJie 89ee272b9d 防止淘系账单筛选重复扫表 (#268)
筛选选项增加稳定缓存与单飞锁,精确行数场景移除多余 COUNT,并加入缓存预热。
2026-09-24 15:22:40 +08:00

209 lines
8.3 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\Http\Controllers;
use App\Utilities\SidebarPermissions;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\EcommerceFinance\Models\AlipayBill;
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
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 调用即可。
*/
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'],
'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'],
]);
try {
return response()->json($repository->paginate(
array_map(static fn (mixed $value): string => trim((string) $value), $filters),
$sort,
$dir,
max((int) $request->get('page', 1), 1),
min(max((int) $request->get('limit', 10), 1), 100),
));
} 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();
try {
return response()->json($repository->filterOptionsForChannel(
'alipayCSV', StoreReceivingAlipayBillMirrorRepository::DATASET, true
));
} 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' => [],
], 503);
}
}
public function export(Request $request, StoreReceivingAlipayBillMirrorRepository $repository)
{
$this->authorizeStoreReceivingBills();
$filters = $request->validate([
'store_name' => ['nullable', 'string', 'max:255'],
'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']);
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(
array_map(static fn (mixed $value): string => trim((string) $value), $filters),
$sort,
$dir,
);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'store_receiving_alipay_bill_mirror_not_ready') {
throw $exception;
}
return response()->json(['message' => '淘系支付宝账单共享业务镜像正在首次同步,完成后将自动显示。'], 503);
}
return response()->streamDownload(function () use ($rows, $columns): void {
$csv = fopen('php://output', 'w');
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($csv, array_values($columns));
foreach ($rows as $row) {
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<string,string> */
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, '当前账号没有店铺收款账户账单权限。');
}
}