qiuxiang 985281c100 店铺收款账户账单新增「总账备注」列,按平台+账单项目匹配映射表
- 新增 ec_account_ledger_remarks 表与 account-ledger-remarks:import 导入命令(平台/账单项目/总账备注)
- 新增 AccountLedgerRemarkMap:按店铺名前缀识别平台(天猫/淘宝/阿里巴巴/抖音/得物/京东/京喜/拼多多/唯品会),无前缀用页签默认平台
- 全部账户页签在「店铺/店铺名称」右侧新增「总账备注」列(含淘系支付宝自有列定义),页面、列设置、导出共用同一份列定义
- 控制器在读取分页与导出流时补齐 payload.ledger_remark,不改动上游账单与镜像数据
- 用例:新增 AccountLedgerRemarkMapTest;修正唯品会 JX/MP 的列清单断言
2026-10-06 15:41:10 +08:00

217 lines
8.6 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'],
'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'],
]);
try {
return response()->json($this->withLedgerRemarks(
$repository->paginate(
array_map(static fn (mixed $value): string => trim((string) $value), $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();
try {
return response()->json($repository->filterOptionsForChannel(
'alipayCSV', StoreReceivingAlipayBillMirrorRepository::DATASET, true
));
} 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' => [],
], 503);
}
}
public function export(Request $request, StoreReceivingAlipayBillMirrorRepository $repository)
{
$this->authorizeStoreReceivingBills();
$filters = $request->validate([
'store_name' => ['nullable', 'string', 'max:255'],
'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']);
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(
array_map(static fn (mixed $value): string => trim((string) $value), $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, '当前账号没有店铺收款账户账单权限。');
}
}