yueyang af10f3c972 接入聚胜算得物完整账单 (#245)
保留得物、退货退款订单和扣减其他费用明细三类渠道;增加受控幂等导入、原子替换与页面权威表查询。
2026-09-22 19:07:11 +08:00

179 lines
6.8 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Http\Controllers;
use App\Utilities\SidebarPermissions;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\EcommerceFinance\AccountBillColumns;
use Modules\EcommerceFinance\Repositories\DewuSettlementBillRepository;
use RuntimeException;
class DewuAccountBills extends Controller
{
public function index(Request $request, DewuSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
if ($request->boolean('export')) {
return $this->exportResponse($request, $repository);
}
$validated = $request->validate($this->filterRules() + [
'sort' => ['nullable', 'in:occurred_at,income,expense,record_code'],
'dir' => ['nullable', 'in:asc,desc'],
'page' => ['nullable', 'integer', 'min:1'],
'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
try {
$data = $repository->paginateAccountBills(
$filters,
(string) ($validated['sort'] ?? 'occurred_at'),
(string) ($validated['dir'] ?? 'desc'),
(int) ($validated['page'] ?? 1),
(int) ($validated['limit'] ?? 10),
);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'dewu_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json([
'message' => '得物完整账单尚未完成首次导入,当前不会回退到不完整的聚水潭辅助表。',
'data' => [],
'total' => 0,
'current_page' => 1,
'last_page' => 1,
'per_page' => (int) ($validated['limit'] ?? 10),
'summary' => ['income' => 0, 'expense' => 0, 'balance' => 0, 'count' => 0],
'columns' => AccountBillColumns::for('dewu'),
], 503);
}
$data['columns'] = AccountBillColumns::for('dewu');
return response()->json($data);
}
public function filterOptions(DewuSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
try {
return response()->json($repository->accountBillFilterOptions());
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'dewu_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json([
'message' => '得物完整账单尚未完成首次导入。',
'shops' => [],
'bill_items' => [],
], 503);
}
}
public function export(Request $request, DewuSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
return $this->exportResponse($request, $repository);
}
private function exportResponse(Request $request, DewuSettlementBillRepository $repository)
{
$validated = $request->validate($this->filterRules() + [
'sort' => ['nullable', 'in:occurred_at,income,expense,record_code'],
'dir' => ['nullable', 'in:asc,desc'],
]);
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
$definition = AccountBillColumns::for('dewu');
$available = collect($definition['columns'] ?? [])->keyBy('field');
$requested = $request->query('columns', []);
if (is_string($requested)) {
$requested = array_filter(array_map('trim', explode(',', $requested)));
}
if (! is_array($requested) || count($requested) > 40) {
$requested = [];
}
$requested = collect($requested)
->filter(static fn (mixed $value): bool => is_scalar($value))
->map(static fn (mixed $value): string => mb_substr(trim((string) $value), 0, 128))
->filter()
->unique();
$columns = $requested->isEmpty()
? $available->values()
: $requested->map(fn (string $field) => $available->get($field))->filter()->values();
abort_if($columns->isEmpty(), 422, '至少选择一个可导出的字段。');
try {
$rows = $repository->exportAccountBills(
$filters,
(string) ($validated['sort'] ?? 'occurred_at'),
(string) ($validated['dir'] ?? 'asc'),
);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'dewu_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json(['message' => '得物完整账单尚未完成首次导入。'], 503);
}
return response()->streamDownload(function () use ($rows, $columns): void {
$csv = fopen('php://output', 'w');
fputs($csv, "\xEF\xBB\xBF");
fputcsv($csv, $columns->pluck('label')->all());
foreach ($rows as $row) {
fputcsv($csv, $columns->map(
fn (array $column): string => $this->exportValue($row, $column)
)->all());
}
fclose($csv);
}, 'dewu_account_bills.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
'Cache-Control' => 'private, no-store',
]);
}
/** @return array<string,array<int,string>> */
private function filterRules(): array
{
return [
'store_name' => ['nullable', 'string', 'max:255'],
'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'],
];
}
/** @param array<string,mixed> $row */
private function exportValue(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 = (string) ($value ?? '');
if (preg_match('/^\d{12,}$/', $text) === 1) {
return '="' . $text . '"';
}
return preg_match('/^[=+\-@]/u', $text) === 1 ? "'" . $text : $text;
}
private 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, '当前账号没有店铺收款账户账单权限。');
}
}