qiuxiang 217b953a03 唯品会JX账户:科目映射与总账备注按行内「账单项目」匹配
- AccountLedgerRemarkMap:JX 页签取 payload.second_remark 作为映射的账单项目,
  与「科目映射表」里按账单项目(处罚款/客服特殊退款-仅退款/盘盈盘亏…)维护的规则对齐
- AccountBills:总账备注筛选对 JX 改为按行内账单项目过滤,其余页签不变
- 测试同步(总账备注筛选按账单项目命中)
2026-10-09 16:57:28 +08:00

696 lines
29 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\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'];
/**
* 时间筛选按「账单期间」(payload.settle_period,形如 2026-07)而不是按发生时间过滤的页签。
*
* 唯品会两个页签里「订单结算时间」其实是一个账期月份,用它筛选才跟页面列对得上。
*/
private const SETTLE_PERIOD_TABS = ['vipshop', 'vipshop-jx'];
/**
* 通用账单列表(店铺收款账户账单 › 各账户类型 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(),
// 唯品会JX 的「账单项目」是账单行里的业务备注(处罚款 / 客服特殊退款-仅退款 / 盘盈盘亏 …),
// 与页面「账单项目」列同源;MP 页签仍按账单项目列取值。
'bill_items' => $bill_type === 'vipshop-jx'
? (clone $query)
->whereRaw("COALESCE(JSON_UNQUOTE(JSON_EXTRACT(payload, '$.second_remark')), '') <> ''")
->selectRaw("DISTINCT JSON_UNQUOTE(JSON_EXTRACT(payload, '$.second_remark')) AS item")
->orderBy('item')
->pluck('item')->all()
: (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'] ?? []) !== []) {
// 唯品会JX 的科目映射按行内「账单项目」维护,总账备注筛选也按它匹配。
if ($billType === 'vipshop-jx') {
$placeholders = implode(',', array_fill(0, count($ledgerFilters['bill_items']), '?'));
$query->whereRaw(
"JSON_UNQUOTE(JSON_EXTRACT(payload, '$.second_remark')) IN ({$placeholders})",
array_values($ledgerFilters['bill_items']),
);
} else {
$query->whereIn('bill_item', $ledgerFilters['bill_items']);
}
}
if ($item = $request->get('bill_item')) {
if ($billType === 'vipshop-jx') {
// 唯品会JX 的账单项目筛选按行内业务备注匹配,和页面「账单项目」列一致。
$query->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(payload, '$.second_remark')) = ?", [$item]);
} else {
$query->where('bill_item', $item);
}
}
$start = $request->get('start');
$end = $request->get('end');
if (in_array($billType, self::SETTLE_PERIOD_TABS, true)) {
// 账单期间是「YYYY-MM」,与日期区间按月份取交集:只要月份落在所选区间内就算命中。
$period = "JSON_UNQUOTE(JSON_EXTRACT(payload, '$.settle_period'))";
if ($start) {
$query->whereRaw($period . ' >= ?', [substr((string) $start, 0, 7)]);
}
if ($end) {
$query->whereRaw($period . ' <= ?', [substr((string) $end, 0, 7)]);
}
} else {
if ($start) {
$query->where('occurred_at', '>=', $start);
}
if ($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, '当前账号没有店铺收款账户账单权限。');
}
}