CaiWuXiTong/modules/EcommerceFinance/Http/Controllers/TaobaoWechatAccountBills.php
qiuxiang c647559c40 fix: 淘系微信期初余额暂置为零 (#985)
淘系微信期初余额暂固定为零,待录入各店铺6月期末余额
2026-11-07 17:02:24 +08:00

243 lines
9.7 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 Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Modules\EcommerceFinance\AccountBillColumns;
use Modules\EcommerceFinance\Http\Concerns\DecoratesLedgerRemarks;
use Modules\EcommerceFinance\Repositories\TaobaoAlipaySettlementBillRepository;
use Modules\EcommerceFinance\Repositories\TaobaoWechatSettlementBillRepository;
use RuntimeException;
class TaobaoWechatAccountBills extends Controller
{
use DecoratesLedgerRemarks;
public function index(
Request $request,
TaobaoWechatSettlementBillRepository $repository,
TaobaoAlipaySettlementBillRepository $alipayRepository,
)
{
$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();
$filters = $this->withLedgerRemarkFilter($filters, 'taobao-wechat');
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() !== 'taobao_wechat_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' => $this->emptySummary(),
'columns' => AccountBillColumns::for('taobao-wechat'),
], 503);
}
$alipaySummary = null;
try {
// 微信账单没有余额列;期初待各店铺6月期末余额录入,暂时固定为0,
// 期末仍按现有口径取同一筛选条件下淘系支付宝的账户余额。
$alipaySummary = $alipayRepository->summary($filters);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'taobao_alipay_authoritative_bills_not_ready') {
throw $exception;
}
}
$data['summary'] = array_merge($data['summary'], [
'opening_balance' => 0,
'opening_balance_date' => null,
'opening_balance_source' => 'pending_june_ending',
'balance' => $alipaySummary['balance'] ?? null,
'balance_source' => $alipaySummary === null ? 'unavailable' : 'taobao_alipay',
]);
$data['columns'] = AccountBillColumns::for('taobao-wechat');
return response()->json($this->withLedgerRemarks($data, 'taobao-wechat'));
}
/** @return array<string,int|float|null> */
private function emptySummary(): array
{
return [
'income' => 0,
'expense' => 0,
'opening_balance' => 0,
'opening_balance_date' => null,
'opening_balance_source' => 'pending_june_ending',
'balance' => null,
'balance_source' => 'unavailable',
'count' => 0,
];
}
public function filterOptions(TaobaoWechatSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('taobao-wechat');
try {
return response()->json($repository->accountBillFilterOptions() + ['ledger_remarks' => $ledgerRemarks]);
} catch (RuntimeException $exception) {
if ($exception->getMessage() !== 'taobao_wechat_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json([
'message' => '淘系微信完整账单尚未完成首次导入。',
'shops' => [],
'bill_items' => [],
'ledger_remarks' => $ledgerRemarks,
], 503);
}
}
public function export(Request $request, TaobaoWechatSettlementBillRepository $repository)
{
$this->authorizeStoreReceivingBills();
return $this->exportResponse($request, $repository);
}
private function exportResponse(Request $request, TaobaoWechatSettlementBillRepository $repository)
{
$exportToken = $this->exportProgressToken($request);
$exportTotal = max(0, (int) $request->get('total', 0));
$this->putExportProgress($exportToken, 0, $exportTotal);
$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();
$filters = $this->withLedgerRemarkFilter($filters, 'taobao-wechat');
$definition = AccountBillColumns::for('taobao-wechat');
$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() !== 'taobao_wechat_authoritative_bills_not_ready') {
throw $exception;
}
return response()->json(['message' => '淘系微信完整账单尚未完成首次导入。'], 503);
}
$rows = $this->ledgerRemarkRows($rows, 'taobao-wechat');
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, "\xEF\xBB\xBF");
fputcsv($csv, $columns->pluck('label')->all());
flush();
$written = 0;
foreach ($rows as $row) {
fputcsv($csv, $columns->map(
fn (array $column): string => $this->exportValue($row, $column)
)->all());
$written++;
if ($written % 5000 === 0) {
$this->putExportProgress($exportToken, $written, $exportTotal);
}
}
fclose($csv);
$this->putExportProgress($exportToken, $written, $exportTotal, 'done');
}, 'taobao-wechat_account_bills.csv', [
'Content-Type' => 'text/csv; charset=UTF-8',
'Cache-Control' => 'private, no-store',
'X-Accel-Buffering' => 'no',
]);
}
/** @return array<string,array<int,string>> */
private function filterRules(): array
{
return [
'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'],
];
}
/** @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, '当前账号没有店铺收款账户账单权限。');
}
}