654 lines
26 KiB
PHP
654 lines
26 KiB
PHP
<?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\AccountBillColumns;
|
||
use Modules\EcommerceFinance\JdBillPlatform;
|
||
use Modules\EcommerceFinance\Models\AccountBill;
|
||
use Modules\EcommerceFinance\Repositories\JdDailyBillRepository;
|
||
use Modules\EcommerceFinance\Repositories\StoreReceivingJdWalletBillRepository;
|
||
use Modules\EcommerceFinance\Repositories\StoreReceivingDouyinBillMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\StoreReceivingDewuBillMirrorRepository;
|
||
use Modules\EcommerceFinance\Repositories\StoreReceivingPinduoduoBillMirrorRepository;
|
||
use RuntimeException;
|
||
|
||
class AccountBills extends Controller
|
||
{
|
||
use DecoratesLedgerRemarks;
|
||
/** 走共享业务镜像的账户类型(其余类型读本系统账单表 ec_account_bills)。 */
|
||
private const MIRROR_TABS = ['douyin', 'pinduoduo', 'dewu', 'jd-transactions', 'jingxi', 'jd-wallet'];
|
||
|
||
/**
|
||
* 通用账单列表(店铺收款账户账单 › 各账户类型 tab)。
|
||
* 经 admin 路由组(web 会话)暴露,前端带 CSRF 调用。
|
||
* 按 bill_type 过滤,返回分页数据 + 汇总 + 该类型的列配置。
|
||
*/
|
||
public function index(Request $request, string $bill_type)
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
$billType = $bill_type;
|
||
|
||
if (! AccountBillColumns::has($billType)) {
|
||
abort(404, '未知的账户类型。');
|
||
}
|
||
|
||
if (in_array($billType, self::MIRROR_TABS, true)) {
|
||
return $request->boolean('export')
|
||
? $this->export($request, $billType)
|
||
: $this->mirrorIndex($request, $billType);
|
||
}
|
||
|
||
$request->validate([
|
||
'store_name' => ['nullable', 'string', 'max:255'],
|
||
'ledger_remark' => ['nullable', 'string', 'max:191'],
|
||
'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 = $this->filteredQuery($request, $billType);
|
||
|
||
$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';
|
||
$query->orderBy($sort, $dir);
|
||
|
||
if ($request->boolean('export')) {
|
||
return $this->exportQuery($request, $query, $billType);
|
||
}
|
||
|
||
$perPage = min(max((int) $request->get('limit', 10), 1), 100);
|
||
$rows = $query->paginate($perPage);
|
||
|
||
// 汇总(分页前)
|
||
$summaryRow = (clone $query)->selectRaw(
|
||
'COALESCE(SUM(income), 0) as income, ' .
|
||
'COALESCE(SUM(expense), 0) as expense, ' .
|
||
'COUNT(*) as count'
|
||
)->first();
|
||
|
||
$latestBalance = (clone $query)
|
||
->orderByDesc('occurred_at')
|
||
->orderByDesc('id')
|
||
->value('balance');
|
||
|
||
$data = $rows->toArray();
|
||
$data['data'] = collect($rows->items())
|
||
->map(fn (AccountBill $bill): array => $this->billRow($bill))
|
||
->all();
|
||
$income = (float) ($summaryRow->income ?? 0);
|
||
$expense = (float) ($summaryRow->expense ?? 0);
|
||
$data['summary'] = [
|
||
'income' => $income,
|
||
'expense' => $expense,
|
||
'count' => (int) ($summaryRow->count ?? 0),
|
||
// 唯品会JX账单没有账户余额列,该页签以收支净额作为汇总口径。
|
||
'balance' => $billType === 'vipshop-jx'
|
||
? round($income - $expense, 2)
|
||
: ($latestBalance !== null ? (float) $latestBalance : 0),
|
||
];
|
||
$data['columns'] = AccountBillColumns::for($billType);
|
||
|
||
return response()->json($this->withLedgerRemarks($data, $billType));
|
||
}
|
||
|
||
public function export(Request $request, string $bill_type)
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
// 导出进度:前端带 token(+列表总数)发起下载,导出过程中把已导出行数写进缓存,前端轮询显示。
|
||
$exportToken = $this->exportProgressToken($request);
|
||
$exportTotal = max(0, (int) $request->get('total', 0));
|
||
$this->putExportProgress($exportToken, 0, $exportTotal);
|
||
|
||
abort_unless(AccountBillColumns::has($bill_type), 404, '未知的账户类型。');
|
||
|
||
if (in_array($bill_type, self::MIRROR_TABS, true)) {
|
||
return $this->exportMirror($request, $bill_type);
|
||
}
|
||
|
||
$filters = $request->validate([
|
||
'store_name' => ['nullable', 'string', 'max:255'],
|
||
'ledger_remark' => ['nullable', 'string', 'max:191'],
|
||
'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'],
|
||
]);
|
||
$query = $this->filteredQuery($request, $bill_type);
|
||
$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';
|
||
$query->orderBy($sort, $dir);
|
||
|
||
return $this->exportQuery($request, $query, $bill_type);
|
||
}
|
||
|
||
public function filterOptions(string $bill_type)
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab($bill_type);
|
||
|
||
if (in_array($bill_type, ['vipshop', 'vipshop-jx'], true)) {
|
||
$query = AccountBill::query()
|
||
->where('company_id', company_id())
|
||
->where('bill_type', $bill_type);
|
||
|
||
return response()->json([
|
||
'shops' => (clone $query)->whereNotNull('store_name')->where('store_name', '<>', '')
|
||
->distinct()->orderBy('store_name')->pluck('store_name')->all(),
|
||
'bill_items' => (clone $query)->whereNotNull('bill_item')->where('bill_item', '<>', '')
|
||
->distinct()->orderBy('bill_item')->pluck('bill_item')->all(),
|
||
'ledger_remarks' => $ledgerRemarks,
|
||
]);
|
||
}
|
||
|
||
abort_unless(in_array($bill_type, self::MIRROR_TABS, true), 404, '该账户类型没有可用的筛选选项。');
|
||
|
||
$label = $this->mirrorLabel($bill_type);
|
||
|
||
try {
|
||
return response()->json(
|
||
$this->mirrorRepository($bill_type)->filterOptions() + ['ledger_remarks' => $ledgerRemarks]
|
||
);
|
||
} catch (RuntimeException $exception) {
|
||
if (! in_array($exception->getMessage(), [
|
||
'store_receiving_alipay_bill_mirror_not_ready',
|
||
'store_receiving_bill_filter_options_busy',
|
||
'jd_daily_bill_mirror_not_ready',
|
||
'jd_wallet_bill_mirror_not_ready',
|
||
], true)) {
|
||
throw $exception;
|
||
}
|
||
|
||
return response()->json([
|
||
'message' => $exception->getMessage() === 'store_receiving_bill_filter_options_busy'
|
||
? $label . '账户账单筛选选项正在生成,请稍后重试。'
|
||
: $label . '账户账单共享业务镜像正在首次同步,筛选选项稍后可用。',
|
||
'shops' => [],
|
||
'bill_items' => [],
|
||
'ledger_remarks' => $ledgerRemarks,
|
||
], 503);
|
||
}
|
||
}
|
||
|
||
/** 账单表格按 Y-m-d H:i:s 展示日期,避免 Eloquent 默认序列化成 ISO8601(UTC)。 */
|
||
private function billRow(AccountBill $bill): array
|
||
{
|
||
return collect($bill->toArray())
|
||
->map(function (mixed $value, string $field) use ($bill): mixed {
|
||
$attribute = $bill->getAttribute($field);
|
||
|
||
return $attribute instanceof \DateTimeInterface
|
||
? $attribute->format('Y-m-d H:i:s')
|
||
: $value;
|
||
})
|
||
->all();
|
||
}
|
||
|
||
private function exportQuery(Request $request, $query, string $billType)
|
||
{
|
||
$definition = AccountBillColumns::for($billType);
|
||
$available = collect($definition['columns'] ?? [])->keyBy('field');
|
||
$requested = $request->input('columns', []);
|
||
if (is_string($requested)) {
|
||
$requested = array_filter(array_map('trim', explode(',', $requested)));
|
||
}
|
||
if (! is_array($requested)) {
|
||
$requested = [];
|
||
}
|
||
if ($requested !== []) {
|
||
$requested = collect($requested)->unique()->values();
|
||
$invalid = $requested->reject(fn (string $field): bool => $available->has($field));
|
||
abort_if($invalid->isNotEmpty(), 422, '包含不可导出的字段:' . $invalid->implode('、'));
|
||
}
|
||
$columns = $requested === []
|
||
? $available->values()
|
||
: $requested->map(fn (string $field) => $available->get($field))->values();
|
||
|
||
abort_if($columns->isEmpty(), 422, '至少选择一个可导出的字段。');
|
||
|
||
return response()->streamDownload(function () use ($query, $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, $columns->pluck('label')->all());
|
||
flush();
|
||
|
||
$written = 0;
|
||
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());
|
||
$written++;
|
||
if ($written % 5000 === 0) {
|
||
$this->putExportProgress($exportToken, $written, $exportTotal);
|
||
}
|
||
}
|
||
fclose($csv);
|
||
$this->putExportProgress($exportToken, $written, $exportTotal, 'done');
|
||
}, $billType . '_account_bills.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
'X-Accel-Buffering' => 'no',
|
||
]);
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
private function filteredQuery(Request $request, string $billType)
|
||
{
|
||
$query = AccountBill::query()
|
||
->where('company_id', company_id())
|
||
->where('bill_type', $billType);
|
||
|
||
if ($store = $request->get('store_name')) {
|
||
$query->where('store_name', $store);
|
||
}
|
||
|
||
$ledgerFilters = $this->withLedgerRemarkFilter(
|
||
['ledger_remark' => $request->get('ledger_remark')],
|
||
$billType,
|
||
);
|
||
if (($ledgerFilters['bill_items'] ?? []) !== []) {
|
||
$query->whereIn('bill_item', $ledgerFilters['bill_items']);
|
||
}
|
||
|
||
if ($item = $request->get('bill_item')) {
|
||
$query->where('bill_item', $item);
|
||
}
|
||
|
||
if ($start = $request->get('start')) {
|
||
$query->where('occurred_at', '>=', $start);
|
||
}
|
||
|
||
if ($end = $request->get('end')) {
|
||
$query->where('occurred_at', '<=', $end . ' 23:59:59');
|
||
}
|
||
|
||
if ($keyword = $request->get('keyword')) {
|
||
$query->where(function ($q) use ($keyword): void {
|
||
$like = '%' . $keyword . '%';
|
||
$q->where('record_code', 'like', $like)
|
||
->orWhere('order_no', 'like', $like)
|
||
->orWhere('remark', 'like', $like)
|
||
->orWhere('bill_item', 'like', $like);
|
||
});
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
private function mirrorIndex(Request $request, string $billType)
|
||
{
|
||
$filters = $this->mirrorFilters($request, $billType);
|
||
$sort = $this->sort($request);
|
||
$direction = $this->direction($request);
|
||
$page = max((int) $request->get('page', 1), 1);
|
||
$perPage = min(max((int) $request->get('limit', 10), 1), 100);
|
||
$label = $this->mirrorLabel($billType);
|
||
|
||
try {
|
||
$data = $this->mirrorRepository($billType)->paginate(
|
||
$filters,
|
||
$sort,
|
||
$direction,
|
||
$page,
|
||
$perPage,
|
||
);
|
||
} catch (RuntimeException $exception) {
|
||
if (! in_array($exception->getMessage(), ['store_receiving_alipay_bill_mirror_not_ready', 'jd_daily_bill_mirror_not_ready', 'jd_wallet_bill_mirror_not_ready'], true)) {
|
||
throw $exception;
|
||
}
|
||
|
||
return response()->json([
|
||
'message' => $label . '账户账单共享业务镜像正在首次同步,完成后将自动显示。',
|
||
'data' => [],
|
||
'total' => 0,
|
||
'current_page' => 1,
|
||
'last_page' => 1,
|
||
'per_page' => $perPage,
|
||
'summary' => ['income' => 0, 'expense' => 0, 'balance' => 0, 'count' => 0],
|
||
'columns' => AccountBillColumns::for($billType),
|
||
], 503);
|
||
}
|
||
|
||
$data['columns'] = AccountBillColumns::for($billType);
|
||
|
||
return response()->json($this->withLedgerRemarks($data, $billType));
|
||
}
|
||
|
||
/** 导出进度查询:前端带 token 轮询,展示「已导出 X / 共 Y 行」。 */
|
||
public function exportProgress(Request $request)
|
||
{
|
||
$token = $this->exportProgressToken($request);
|
||
abort_if($token === '', 400, '导出标识无效。');
|
||
|
||
$state = \Illuminate\Support\Facades\Cache::get(self::exportProgressKey($token));
|
||
|
||
return response()->json($state ?? ['status' => 'unknown', 'rows' => 0, 'total' => 0], 200, [
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/** 新建后台导出任务:立刻返回,导出在后台进程里跑,页面到「导出进度」列表里看状态并下载。 */
|
||
public function createExportTask(Request $request)
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
$tab = trim((string) $request->input('tab', ''));
|
||
$columns = $this->exportColumnsFor($tab);
|
||
abort_if($columns === [], 422, '该页签暂不支持后台导出。');
|
||
|
||
$requested = array_filter(array_map(
|
||
static fn (mixed $field): string => trim((string) $field),
|
||
(array) $request->input('columns', []),
|
||
));
|
||
$selected = [];
|
||
foreach ($requested as $field) {
|
||
if (isset($columns[$field])) {
|
||
$selected[] = ['field' => $field, 'label' => $columns[$field]];
|
||
}
|
||
}
|
||
if ($selected === []) {
|
||
foreach ($columns as $field => $label) {
|
||
$selected[] = ['field' => (string) $field, 'label' => (string) $label];
|
||
}
|
||
}
|
||
|
||
$filters = $request->except(['columns', 'tab', '_token']);
|
||
$tasks = app(\Modules\EcommerceFinance\Services\AccountBillsExportTasks::class);
|
||
$task = $tasks->create(
|
||
(int) user()->id,
|
||
$tab,
|
||
$this->mirrorLabel($tab),
|
||
$filters,
|
||
$selected,
|
||
max(0, (int) $request->input('total', 0)),
|
||
);
|
||
$tasks->dispatch($task);
|
||
|
||
return response()->json(['status' => 'ok', 'token' => $task['token']], 200, [
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/** 导出任务列表(当前用户,最近 50 条)。 */
|
||
public function exportTasks()
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
$tasks = app(\Modules\EcommerceFinance\Services\AccountBillsExportTasks::class);
|
||
$userId = (int) user()->id;
|
||
|
||
return response()->json([
|
||
'status' => 'ok',
|
||
'data' => array_map(static fn (array $task): array => $tasks->present($task), $tasks->all($userId)),
|
||
], 200, ['Cache-Control' => 'private, no-store']);
|
||
}
|
||
|
||
/** 下载已完成的导出文件。 */
|
||
public function downloadExportTask(Request $request, string $token)
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
$tasks = app(\Modules\EcommerceFinance\Services\AccountBillsExportTasks::class);
|
||
$task = $tasks->find((int) user()->id, $token);
|
||
abort_if($task === null, 404, '导出任务不存在或已过期。');
|
||
abort_unless(($task['status'] ?? '') === 'done', 409, '导出尚未完成,请稍后刷新。');
|
||
|
||
$path = $tasks->csvPath($task);
|
||
abort_unless(is_file($path), 404, '导出文件已过期,请重新导出。');
|
||
|
||
$name = str_replace(['/', '\\'], '-', (string) ($task['tab_label'] ?? 'account-bills'))
|
||
. '_' . substr($token, 0, 8) . '.csv';
|
||
|
||
return response()->download($path, $name, ['Content-Type' => 'text/csv; charset=UTF-8']);
|
||
}
|
||
|
||
/** 取消导出任务:写入取消标记,后台进程检测到后停止并删除半成品文件。 */
|
||
public function cancelExportTask(Request $request, string $token)
|
||
{
|
||
$this->authorizeStoreReceivingBills();
|
||
|
||
$tasks = app(\Modules\EcommerceFinance\Services\AccountBillsExportTasks::class);
|
||
$task = $tasks->find((int) user()->id, $token);
|
||
abort_if($task === null, 404, '导出任务不存在或已过期。');
|
||
|
||
$status = (string) ($task['status'] ?? '');
|
||
abort_if(in_array($status, ['done', 'failed', 'cancelled'], true), 409, '该任务已结束,无需取消。');
|
||
|
||
// 立刻显示「已取消」(排队中的任务没有进程去改状态,必须在这里定稿);
|
||
// 运行中的任务会在下一个检查点停下并删掉半成品文件。
|
||
$tasks->patch($task, [
|
||
'cancel' => true,
|
||
'status' => 'cancelled',
|
||
'finished_at' => \Modules\EcommerceFinance\Services\AccountBillsExportTasks::beijingNow(),
|
||
]);
|
||
|
||
return response()->json(['status' => 'ok'], 200, ['Cache-Control' => 'private, no-store']);
|
||
}
|
||
|
||
/**
|
||
* 该页签可导出的字段:field => label。
|
||
*
|
||
* @return array<string,string>
|
||
*/
|
||
private function exportColumnsFor(string $tab): array
|
||
{
|
||
if ($tab === 'taobao-alipay') {
|
||
return AlipayBills::EXPORT_COLUMNS;
|
||
}
|
||
|
||
$definition = AccountBillColumns::for($tab);
|
||
$columns = [];
|
||
foreach ((array) ($definition['columns'] ?? []) as $column) {
|
||
$field = (string) ($column['field'] ?? '');
|
||
if ($field !== '') {
|
||
$columns[$field] = (string) ($column['label'] ?? $field);
|
||
}
|
||
}
|
||
|
||
return $columns;
|
||
}
|
||
|
||
private function exportMirror(Request $request, string $billType)
|
||
{
|
||
$exportToken = $this->exportProgressToken($request);
|
||
$exportTotal = max(0, (int) $request->get('total', 0));
|
||
$this->putExportProgress($exportToken, 0, $exportTotal);
|
||
|
||
$filters = $this->mirrorFilters($request, $billType);
|
||
$definition = AccountBillColumns::for($billType);
|
||
$columns = collect($definition['columns'] ?? []);
|
||
$requested = $request->query('columns', []);
|
||
if (is_string($requested)) {
|
||
$requested = array_filter(array_map('trim', explode(',', $requested)));
|
||
}
|
||
if (! is_array($requested)) {
|
||
$requested = [];
|
||
}
|
||
$requested = collect($requested)->map(static fn (string $field): string => trim($field))->filter()->unique()->values();
|
||
if ($requested->isEmpty()) {
|
||
$selected = $columns;
|
||
} else {
|
||
$columnsByField = $columns->keyBy('field');
|
||
$invalid = $requested->reject(static fn (string $field): bool => $columnsByField->has($field));
|
||
abort_if($invalid->isNotEmpty(), 422, '包含不可导出的字段:' . $invalid->implode('、'));
|
||
$selected = $requested->map(static fn (string $field) => $columnsByField->get($field))->values();
|
||
abort_if($selected->isEmpty(), 422, '未选择有效的导出字段。');
|
||
}
|
||
|
||
$sort = $this->sort($request);
|
||
$direction = $this->direction($request);
|
||
try {
|
||
$rows = $this->mirrorRepository($billType)
|
||
->exportRows($filters, $sort, $direction);
|
||
} catch (RuntimeException $exception) {
|
||
if (! in_array($exception->getMessage(), ['store_receiving_alipay_bill_mirror_not_ready', 'jd_daily_bill_mirror_not_ready', 'jd_wallet_bill_mirror_not_ready'], true)) {
|
||
throw $exception;
|
||
}
|
||
|
||
return response()->json(['message' => $this->mirrorLabel($billType) . '账户账单共享业务镜像正在首次同步,完成后将自动导出。'], 503);
|
||
}
|
||
|
||
$headers = $selected->pluck('label')->all();
|
||
|
||
$rows = $this->ledgerRemarkRows($rows, $billType);
|
||
|
||
return response()->streamDownload(function () use ($rows, $selected, $headers, $exportToken, $exportTotal): void {
|
||
while (ob_get_level() > 0) {
|
||
ob_end_flush();
|
||
}
|
||
$csv = fopen('php://output', 'w');
|
||
fputs($csv, "\xEF\xBB\xBF");
|
||
fputcsv($csv, $headers);
|
||
flush();
|
||
$written = 0;
|
||
foreach ($rows as $row) {
|
||
fputcsv($csv, $selected->map(
|
||
fn (array $column): string => $this->exportArrayValue($row, $column)
|
||
)->all());
|
||
$written++;
|
||
if ($written % 5000 === 0) {
|
||
$this->putExportProgress($exportToken, $written, $exportTotal);
|
||
}
|
||
}
|
||
fclose($csv);
|
||
$this->putExportProgress($exportToken, $written, $exportTotal, 'done');
|
||
}, $billType . '_account_bills.csv', [
|
||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||
'Cache-Control' => 'private, no-store',
|
||
'X-Accel-Buffering' => 'no',
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,string> */
|
||
private function mirrorFilters(Request $request, string $billType): array
|
||
{
|
||
$filters = $request->validate([
|
||
'store_name' => ['nullable', 'string', 'max:255'],
|
||
'ledger_remark' => ['nullable', 'string', 'max:191'],
|
||
'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'],
|
||
'keyword_match' => ['nullable', 'in:contains,exact-record'],
|
||
]);
|
||
|
||
$filters = array_map(static fn (mixed $value): string => trim((string) $value), $filters);
|
||
|
||
return $this->withLedgerRemarkFilter($filters, $billType);
|
||
}
|
||
|
||
private function mirrorRepository(string $billType): object
|
||
{
|
||
if (JdBillPlatform::isJdBillTab($billType)) {
|
||
// 京东账单与京喜账单共用同一个京东日账单仓库,只用平台口径区分数据。
|
||
return app(JdDailyBillRepository::class)
|
||
->forPlatform(JdBillPlatform::platformForBillType($billType));
|
||
}
|
||
|
||
return match ($billType) {
|
||
'jd-wallet' => app(StoreReceivingJdWalletBillRepository::class),
|
||
'pinduoduo' => app(StoreReceivingPinduoduoBillMirrorRepository::class),
|
||
'dewu' => app(StoreReceivingDewuBillMirrorRepository::class),
|
||
default => app(StoreReceivingDouyinBillMirrorRepository::class),
|
||
};
|
||
}
|
||
|
||
private function mirrorLabel(string $billType): string
|
||
{
|
||
return match ($billType) {
|
||
'jd-transactions' => '京东账单',
|
||
'jingxi' => '京喜账单',
|
||
'jd-wallet' => '京东钱包',
|
||
'pinduoduo' => '拼多多',
|
||
'dewu' => '得物',
|
||
'taobao-alipay' => '淘系支付宝账户',
|
||
'taobao-wechat' => '淘系微信账户',
|
||
'vipshop' => '唯品会MP账户',
|
||
'vipshop-jx' => '唯品会JX账户',
|
||
'douyin' => '抖音账户',
|
||
default => $billType,
|
||
};
|
||
}
|
||
|
||
private function sort(Request $request): string
|
||
{
|
||
return in_array($request->get('sort'), ['occurred_at', 'income', 'expense', 'balance', 'record_code'], true)
|
||
? (string) $request->get('sort')
|
||
: 'occurred_at';
|
||
}
|
||
|
||
private function direction(Request $request): string
|
||
{
|
||
return $request->get('dir') === 'asc' ? 'asc' : 'desc';
|
||
}
|
||
|
||
private function exportArrayValue(array $row, array $column): string
|
||
{
|
||
$field = (string) ($column['field'] ?? '');
|
||
$value = str_starts_with($field, 'payload.')
|
||
? data_get($row['payload'] ?? [], substr($field, 8))
|
||
: data_get($row, $field);
|
||
|
||
$text = trim((string) ($value ?? ''));
|
||
if (preg_match('/^\d{12,}$/', $text) === 1) {
|
||
return '="' . $text . '"';
|
||
}
|
||
|
||
return preg_match('/^[=+\-@]/u', $text) === 1 ? "'" . $text : $text;
|
||
}
|
||
|
||
public function show(AccountBill $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, '当前账号没有店铺收款账户账单权限。');
|
||
}
|
||
}
|