qiuxiang 13a9697447 店铺收款账户账单新增「总账备注」筛选;每日账单脚本支持胜算异步分片导出
- 所有账户页签在「店铺」右侧新增「总账备注」下拉:按(平台+账单项目)翻译成账单项目集合过滤,页面/列设置/导出同口径;淘系支付宝、淘系微信同页签混多平台时按店铺名前缀细分
- 每日账单脚本:胜算超出同步导出上限(url=null+taskId)时自动从「异步导出下载管理」取回本次任务全部分片并合并成一份 CSV,导入前按页面笔数核对(不一致则跳过导入)
- 实测:京东 7 月异步导出两片 219158+235846=455004 笔 / 收入 17695094.84 / 支出 8035128.21,与已导入口径一致
2026-10-06 18:46:57 +08:00

226 lines
9.1 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;
private 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();
$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): 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, '当前账号没有店铺收款账户账单权限。');
}
}