246 lines
10 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 Modules\EcommerceFinance\Http\Concerns\DecoratesLedgerRemarks;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\EcommerceFinance\Models\AlipayBill;
use Modules\EcommerceFinance\Repositories\StoreReceivingAlipayBillMirrorRepository;
use RuntimeException;
class AlipayBills extends Controller
{
use DecoratesLedgerRemarks;
public const EXPORT_COLUMNS = [
'record_code' => '记录编码',
'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<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, '当前账号没有店铺收款账户账单权限。');
}
}