店铺收款账户账单:所有页签新增「总账备注」筛选(页面与导出同口径)
- AccountLedgerRemarkMap:新增 页签→平台→账单项目 的翻译(remarksForTab / billItemGroupsForTab / billItemsForTab);淘系支付宝、淘系微信同页签混有多个平台,按店铺名前缀再分平台 - LedgerRemarkFilter(新):把「总账备注」翻译成 SQL 条件,单平台页签 whereIn(账单项目),多平台页签 (店铺前缀 AND 账单项目 IN ...) OR ... - 控制器/仓库:AccountBills、AlipayBills、Pinduoduo/Dewu/TaobaoWechat 账户账单 + 对应仓库(含抖音权威、京东日账单、京东钱包、共享原始账单镜像)全部接入 ledger_remark 筛选;filter-options 统一返回 ledger_remarks 供下拉 - 前端:AccountBills.vue / TaobaoAlipayBills.vue 在「店铺」右侧新增「总账备注」下拉,参与查询、重置与导出 - 测试:新增映射翻译、SQL 条件、唯品会JX 页面+导出筛选用例;同步 filter-options 断言
This commit is contained in:
parent
24ded6fe7f
commit
7175d3f73e
@ -11,11 +11,48 @@ use Modules\EcommerceFinance\Services\AccountLedgerRemarkMap;
|
||||
*/
|
||||
trait DecoratesLedgerRemarks
|
||||
{
|
||||
/** 「总账备注」在本页签没有对应账单项目时的占位条件(保证命中 0 行)。 */
|
||||
private const LEDGER_REMARK_NO_MATCH_ITEM = '__no_such_ledger_remark_item__';
|
||||
|
||||
protected function ledgerRemarkMap(): AccountLedgerRemarkMap
|
||||
{
|
||||
return app(AccountLedgerRemarkMap::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把「总账备注」筛选翻译成账单项目集合。
|
||||
*
|
||||
* 页面、列设置与导出都调用这里,保证同一份筛选口径;
|
||||
* bill_items 供各仓库 whereIn(账单项目) 使用,bill_item_groups 供
|
||||
* 淘系这种同一页签混多平台的仓库按店铺名前缀再细分。
|
||||
*
|
||||
* @param array<string,mixed> $filters
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
protected function withLedgerRemarkFilter(array $filters, string $tab): array
|
||||
{
|
||||
$remark = trim((string) ($filters['ledger_remark'] ?? ''));
|
||||
if ($remark === '') {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$groups = $this->ledgerRemarkMap()->billItemGroupsForTab($tab, $remark);
|
||||
$filters['bill_item_groups'] = $groups;
|
||||
|
||||
$items = [];
|
||||
foreach ($groups as $group) {
|
||||
foreach ($group['items'] as $item) {
|
||||
$items[$item] = true;
|
||||
}
|
||||
}
|
||||
$filters['bill_items'] = array_keys($items);
|
||||
if ($filters['bill_items'] === []) {
|
||||
$filters['bill_items'] = [self::LEDGER_REMARK_NO_MATCH_ITEM];
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页结果(含 data 行数组)补总账备注。
|
||||
*
|
||||
|
||||
@ -45,6 +45,7 @@ class AccountBills extends Controller
|
||||
|
||||
$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'],
|
||||
@ -54,35 +55,8 @@ class AccountBills extends Controller
|
||||
'columns.*' => ['string', 'max:128'],
|
||||
]);
|
||||
|
||||
$query = AccountBill::query()
|
||||
->where('company_id', company_id())
|
||||
->where('bill_type', $billType);
|
||||
|
||||
if ($store = $request->get('store_name')) {
|
||||
$query->where('store_name', $store);
|
||||
}
|
||||
|
||||
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) {
|
||||
$like = '%' . $keyword . '%';
|
||||
$q->where('record_code', 'like', $like)
|
||||
->orWhere('order_no', 'like', $like)
|
||||
->orWhere('remark', 'like', $like)
|
||||
->orWhere('bill_item', 'like', $like);
|
||||
});
|
||||
}
|
||||
// 与导出共用同一份筛选(含「总账备注」翻译出的账单项目集合)。
|
||||
$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';
|
||||
@ -140,6 +114,7 @@ class AccountBills extends Controller
|
||||
|
||||
$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'],
|
||||
@ -158,6 +133,8 @@ class AccountBills extends Controller
|
||||
{
|
||||
$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())
|
||||
@ -168,6 +145,7 @@ class AccountBills extends Controller
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
|
||||
@ -177,7 +155,7 @@ class AccountBills extends Controller
|
||||
|
||||
try {
|
||||
return response()->json(
|
||||
$this->mirrorRepository($bill_type)->filterOptions()
|
||||
$this->mirrorRepository($bill_type)->filterOptions() + ['ledger_remarks' => $ledgerRemarks]
|
||||
);
|
||||
} catch (RuntimeException $exception) {
|
||||
if (! in_array($exception->getMessage(), [
|
||||
@ -195,6 +173,7 @@ class AccountBills extends Controller
|
||||
: $label . '账户账单共享业务镜像正在首次同步,筛选选项稍后可用。',
|
||||
'shops' => [],
|
||||
'bill_items' => [],
|
||||
'ledger_remarks' => $ledgerRemarks,
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
@ -283,6 +262,14 @@ class AccountBills extends Controller
|
||||
$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);
|
||||
}
|
||||
@ -310,7 +297,7 @@ class AccountBills extends Controller
|
||||
|
||||
private function mirrorIndex(Request $request, string $billType)
|
||||
{
|
||||
$filters = $this->mirrorFilters($request);
|
||||
$filters = $this->mirrorFilters($request, $billType);
|
||||
$sort = $this->sort($request);
|
||||
$direction = $this->direction($request);
|
||||
$page = max((int) $request->get('page', 1), 1);
|
||||
@ -349,7 +336,7 @@ class AccountBills extends Controller
|
||||
|
||||
private function exportMirror(Request $request, string $billType)
|
||||
{
|
||||
$filters = $this->mirrorFilters($request);
|
||||
$filters = $this->mirrorFilters($request, $billType);
|
||||
$definition = AccountBillColumns::for($billType);
|
||||
$columns = collect($definition['columns'] ?? []);
|
||||
$requested = $request->query('columns', []);
|
||||
@ -404,10 +391,11 @@ class AccountBills extends Controller
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function mirrorFilters(Request $request): array
|
||||
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'],
|
||||
@ -415,7 +403,9 @@ class AccountBills extends Controller
|
||||
'keyword_match' => ['nullable', 'in:contains,exact-record'],
|
||||
]);
|
||||
|
||||
return array_map(static fn (mixed $value): string => trim((string) $value), $filters);
|
||||
$filters = array_map(static fn (mixed $value): string => trim((string) $value), $filters);
|
||||
|
||||
return $this->withLedgerRemarkFilter($filters, $billType);
|
||||
}
|
||||
|
||||
private function mirrorRepository(string $billType): object
|
||||
|
||||
@ -55,6 +55,7 @@ class AlipayBills extends Controller
|
||||
$dir = $request->get('dir') === 'asc' ? 'asc' : 'desc';
|
||||
$filters = $request->validate([
|
||||
'store_name' => ['nullable', 'string', 'max:255'],
|
||||
'ledger_remark' => ['nullable', 'string', 'max:191'],
|
||||
'source_channel' => ['nullable', 'string', 'max:128'],
|
||||
'bill_item' => ['nullable', 'string', 'max:255'],
|
||||
'business_type' => ['nullable', 'string', 'max:255'],
|
||||
@ -62,11 +63,15 @@ class AlipayBills extends Controller
|
||||
'end' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start'],
|
||||
'keyword' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
$filters = $this->withLedgerRemarkFilter(
|
||||
array_map(static fn (mixed $value): string => trim((string) $value), $filters),
|
||||
'taobao-alipay',
|
||||
);
|
||||
|
||||
try {
|
||||
return response()->json($this->withLedgerRemarks(
|
||||
$repository->paginate(
|
||||
array_map(static fn (mixed $value): string => trim((string) $value), $filters),
|
||||
$filters,
|
||||
$sort,
|
||||
$dir,
|
||||
max((int) $request->get('page', 1), 1),
|
||||
@ -95,10 +100,12 @@ class AlipayBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('taobao-alipay');
|
||||
|
||||
try {
|
||||
return response()->json($repository->filterOptionsForChannel(
|
||||
'alipayCSV', StoreReceivingAlipayBillMirrorRepository::DATASET, true
|
||||
));
|
||||
) + ['ledger_remarks' => $ledgerRemarks]);
|
||||
} catch (RuntimeException $exception) {
|
||||
if (! in_array($exception->getMessage(), [
|
||||
'store_receiving_alipay_bill_mirror_not_ready',
|
||||
@ -113,6 +120,7 @@ class AlipayBills extends Controller
|
||||
: '淘系支付宝账单共享业务镜像正在首次同步,筛选选项稍后可用。',
|
||||
'shops' => [],
|
||||
'bill_items' => [],
|
||||
'ledger_remarks' => $ledgerRemarks,
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
@ -122,6 +130,7 @@ class AlipayBills extends Controller
|
||||
$this->authorizeStoreReceivingBills();
|
||||
$filters = $request->validate([
|
||||
'store_name' => ['nullable', 'string', 'max:255'],
|
||||
'ledger_remark' => ['nullable', 'string', 'max:191'],
|
||||
'source_channel' => ['nullable', 'string', 'max:128'],
|
||||
'bill_item' => ['nullable', 'string', 'max:255'],
|
||||
'business_type' => ['nullable', 'string', 'max:255'],
|
||||
@ -134,16 +143,16 @@ class AlipayBills extends Controller
|
||||
|
||||
$columns = $this->requestedExportColumns($filters['columns'] ?? []);
|
||||
unset($filters['columns']);
|
||||
$filters = $this->withLedgerRemarkFilter(
|
||||
array_map(static fn (mixed $value): string => trim((string) $value), $filters),
|
||||
'taobao-alipay',
|
||||
);
|
||||
|
||||
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,
|
||||
);
|
||||
$rows = $repository->exportRows($filters, $sort, $dir);
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'store_receiving_alipay_bill_mirror_not_ready') {
|
||||
throw $exception;
|
||||
|
||||
@ -28,6 +28,7 @@ class DewuAccountBills extends Controller
|
||||
'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
]);
|
||||
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
|
||||
$filters = $this->withLedgerRemarkFilter($filters, 'dewu');
|
||||
|
||||
try {
|
||||
$data = $repository->paginateAccountBills(
|
||||
@ -63,8 +64,10 @@ class DewuAccountBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('dewu');
|
||||
|
||||
try {
|
||||
return response()->json($repository->accountBillFilterOptions());
|
||||
return response()->json($repository->accountBillFilterOptions() + ['ledger_remarks' => $ledgerRemarks]);
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'dewu_authoritative_bills_not_ready') {
|
||||
throw $exception;
|
||||
@ -74,6 +77,7 @@ class DewuAccountBills extends Controller
|
||||
'message' => '得物完整账单尚未完成首次导入。',
|
||||
'shops' => [],
|
||||
'bill_items' => [],
|
||||
'ledger_remarks' => $ledgerRemarks,
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
@ -92,6 +96,7 @@ class DewuAccountBills extends Controller
|
||||
'dir' => ['nullable', 'in:asc,desc'],
|
||||
]);
|
||||
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
|
||||
$filters = $this->withLedgerRemarkFilter($filters, 'dewu');
|
||||
$definition = AccountBillColumns::for('dewu');
|
||||
$available = collect($definition['columns'] ?? [])->keyBy('field');
|
||||
$requested = $request->query('columns', []);
|
||||
@ -148,6 +153,7 @@ class DewuAccountBills extends Controller
|
||||
{
|
||||
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'],
|
||||
|
||||
@ -28,6 +28,7 @@ class PinduoduoAccountBills extends Controller
|
||||
'limit' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
]);
|
||||
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
|
||||
$filters = $this->withLedgerRemarkFilter($filters, 'pinduoduo');
|
||||
|
||||
try {
|
||||
$data = $repository->paginateAccountBills(
|
||||
@ -63,8 +64,10 @@ class PinduoduoAccountBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('pinduoduo');
|
||||
|
||||
try {
|
||||
return response()->json($repository->accountBillFilterOptions());
|
||||
return response()->json($repository->accountBillFilterOptions() + ['ledger_remarks' => $ledgerRemarks]);
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'pinduoduo_authoritative_bills_not_ready') {
|
||||
throw $exception;
|
||||
@ -74,6 +77,7 @@ class PinduoduoAccountBills extends Controller
|
||||
'message' => '拼多多完整货款账单尚未完成首次导入。',
|
||||
'shops' => [],
|
||||
'bill_items' => [],
|
||||
'ledger_remarks' => $ledgerRemarks,
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
@ -92,6 +96,7 @@ class PinduoduoAccountBills extends Controller
|
||||
'dir' => ['nullable', 'in:asc,desc'],
|
||||
]);
|
||||
$filters = collect($validated)->only(array_keys($this->filterRules()))->all();
|
||||
$filters = $this->withLedgerRemarkFilter($filters, 'pinduoduo');
|
||||
$definition = AccountBillColumns::for('pinduoduo');
|
||||
$available = collect($definition['columns'] ?? [])->keyBy('field');
|
||||
$requested = $request->query('columns', []);
|
||||
@ -148,6 +153,7 @@ class PinduoduoAccountBills extends Controller
|
||||
{
|
||||
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'],
|
||||
|
||||
@ -28,6 +28,7 @@ class TaobaoWechatAccountBills extends Controller
|
||||
'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(
|
||||
@ -63,8 +64,10 @@ class TaobaoWechatAccountBills extends Controller
|
||||
{
|
||||
$this->authorizeStoreReceivingBills();
|
||||
|
||||
$ledgerRemarks = $this->ledgerRemarkMap()->remarksForTab('taobao-wechat');
|
||||
|
||||
try {
|
||||
return response()->json($repository->accountBillFilterOptions());
|
||||
return response()->json($repository->accountBillFilterOptions() + ['ledger_remarks' => $ledgerRemarks]);
|
||||
} catch (RuntimeException $exception) {
|
||||
if ($exception->getMessage() !== 'taobao_wechat_authoritative_bills_not_ready') {
|
||||
throw $exception;
|
||||
@ -74,6 +77,7 @@ class TaobaoWechatAccountBills extends Controller
|
||||
'message' => '淘系微信完整账单尚未完成首次导入。',
|
||||
'shops' => [],
|
||||
'bill_items' => [],
|
||||
'ledger_remarks' => $ledgerRemarks,
|
||||
], 503);
|
||||
}
|
||||
}
|
||||
@ -92,6 +96,7 @@ class TaobaoWechatAccountBills extends Controller
|
||||
'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', []);
|
||||
@ -148,6 +153,7 @@ class TaobaoWechatAccountBills extends Controller
|
||||
{
|
||||
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'],
|
||||
|
||||
@ -7,6 +7,7 @@ use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class DewuSettlementBillRepository
|
||||
@ -224,6 +225,8 @@ class DewuSettlementBillRepository
|
||||
$query->where('bill_item', $billItem);
|
||||
}
|
||||
|
||||
$this->applyLedgerRemarkFilter($query, $filters);
|
||||
|
||||
$from = $this->date(isset($filters['start']) ? (string) $filters['start'] : null);
|
||||
if ($from !== null) {
|
||||
$query->where('bill_date', '>=', $from->format('Y-m-d') . ' 00:00:00');
|
||||
@ -250,6 +253,25 @@ class DewuSettlementBillRepository
|
||||
}
|
||||
}
|
||||
|
||||
/** 「总账备注」筛选:翻译成账单项目集合(得物单平台)。 */
|
||||
private function applyLedgerRemarkFilter(Builder $query, array $filters): void
|
||||
{
|
||||
$groups = is_array($filters['bill_item_groups'] ?? null) ? $filters['bill_item_groups'] : [];
|
||||
if ($groups !== []) {
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
$groups,
|
||||
'bill_item',
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'),
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LedgerRemarkFilter::itemsWhereIn($query, (array) ($filters['bill_items'] ?? []), 'bill_item');
|
||||
}
|
||||
|
||||
private function accountBillSortColumn(string $sort): string
|
||||
{
|
||||
return [
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Modules\EcommerceFinance\Repositories;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class DouyinAuthoritativeBillRepository
|
||||
@ -121,6 +122,7 @@ class DouyinAuthoritativeBillRepository
|
||||
}
|
||||
}
|
||||
if (($filters['bill_item'] ?? '') !== '') $query->where($item, 'like', '%' . $this->escape($filters['bill_item']) . '%');
|
||||
LedgerRemarkFilter::itemsWhereIn($query, (array) ($filters['bill_items'] ?? []), $item);
|
||||
if (($filters['start'] ?? '') !== '') $query->where($date, '>=', $filters['start'] . ' 00:00:00');
|
||||
if (($filters['end'] ?? '') !== '') $query->where($date, '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00');
|
||||
if (($filters['keyword'] ?? '') !== '') {
|
||||
|
||||
@ -5,6 +5,7 @@ namespace Modules\EcommerceFinance\Repositories;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Modules\EcommerceFinance\JdBillPlatform;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class JdDailyBillRepository
|
||||
@ -85,6 +86,7 @@ class JdDailyBillRepository
|
||||
foreach (['store_name', 'bill_item'] as $field) {
|
||||
if (($filters[$field] ?? '') !== '') $q->where($field, $filters[$field]);
|
||||
}
|
||||
LedgerRemarkFilter::itemsWhereIn($q, (array) ($filters['bill_items'] ?? []), 'bill_item');
|
||||
if (($filters['start'] ?? '') !== '') $q->where('occurred_at', '>=', $filters['start'] . ' 00:00:00');
|
||||
if (($filters['end'] ?? '') !== '') $q->where('occurred_at', '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00');
|
||||
if (($filters['keyword'] ?? '') !== '') {
|
||||
|
||||
@ -7,6 +7,7 @@ use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class PinduoduoSettlementBillRepository
|
||||
@ -224,6 +225,8 @@ class PinduoduoSettlementBillRepository
|
||||
$query->where('bill_item', $billItem);
|
||||
}
|
||||
|
||||
$this->applyLedgerRemarkFilter($query, $filters);
|
||||
|
||||
$from = $this->date(isset($filters['start']) ? (string) $filters['start'] : null);
|
||||
if ($from !== null) {
|
||||
$query->where('occurred_at', '>=', $from->format('Y-m-d') . ' 00:00:00');
|
||||
@ -249,6 +252,25 @@ class PinduoduoSettlementBillRepository
|
||||
}
|
||||
}
|
||||
|
||||
/** 「总账备注」筛选:翻译成账单项目集合(拼多多单平台)。 */
|
||||
private function applyLedgerRemarkFilter(Builder $query, array $filters): void
|
||||
{
|
||||
$groups = is_array($filters['bill_item_groups'] ?? null) ? $filters['bill_item_groups'] : [];
|
||||
if ($groups !== []) {
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
$groups,
|
||||
'bill_item',
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'),
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LedgerRemarkFilter::itemsWhereIn($query, (array) ($filters['bill_items'] ?? []), 'bill_item');
|
||||
}
|
||||
|
||||
private function accountBillSortColumn(string $sort): string
|
||||
{
|
||||
return [
|
||||
|
||||
@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
/** 店铺收款账单·淘系支付宝账户只读经营分析副本镜像。 */
|
||||
@ -446,6 +447,19 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
if (($filters['bill_item'] ?? '') !== '') {
|
||||
$query->where('b.fee_name', 'like', '%' . $this->escapeLike((string) $filters['bill_item']) . '%');
|
||||
}
|
||||
// 「总账备注」:共享原始账单镜像只有 shop_id,平台归属用店铺资料表的店铺名前缀判定。
|
||||
$groups = is_array($filters['bill_item_groups'] ?? null) ? $filters['bill_item_groups'] : [];
|
||||
if ($groups !== []) {
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
$groups,
|
||||
'b.fee_name',
|
||||
fn (Builder $sub, string $prefix) => $sub->whereIn('b.shop_id', $this->shopIdsLike($prefix)),
|
||||
fn (Builder $sub, string $prefix) => $sub->whereNotIn('b.shop_id', $this->shopIdsLike($prefix)),
|
||||
);
|
||||
} else {
|
||||
LedgerRemarkFilter::itemsWhereIn($query, (array) ($filters['bill_items'] ?? []), 'b.fee_name');
|
||||
}
|
||||
if (($filters['business_type'] ?? '') !== '') {
|
||||
$query->where('b.inout_type', $this->directionCode((string) $filters['business_type']));
|
||||
}
|
||||
@ -507,6 +521,14 @@ class StoreReceivingAlipayBillMirrorRepository
|
||||
$query->whereNotIn('b.shop_id', $excludedShopIds);
|
||||
}
|
||||
|
||||
/** 店铺名前缀 → shop_id 子查询(共享原始账单镜像按店铺资料表判定平台)。 */
|
||||
private function shopIdsLike(string $prefix): Builder
|
||||
{
|
||||
return $this->mirror->connection()->table('ops_jst_shop_info')
|
||||
->where('shop_name', 'like', $this->escapeLike($prefix) . '%')
|
||||
->select('shop_id');
|
||||
}
|
||||
|
||||
/** @param array<string,scalar|list<scalar>> $fixedFilters */
|
||||
private function channelCoversDataset(string|array|null $channel, array $fixedFilters): bool
|
||||
{
|
||||
|
||||
@ -4,6 +4,7 @@ namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class StoreReceivingJdWalletBillRepository
|
||||
@ -72,6 +73,7 @@ class StoreReceivingJdWalletBillRepository
|
||||
foreach (['store_name', 'bill_item'] as $field) {
|
||||
if (($filters[$field] ?? '') !== '') $q->where($field, $filters[$field]);
|
||||
}
|
||||
LedgerRemarkFilter::itemsWhereIn($q, (array) ($filters['bill_items'] ?? []), 'bill_item');
|
||||
if (($filters['start'] ?? '') !== '') $q->where('occurred_at', '>=', $filters['start'] . ' 00:00:00');
|
||||
if (($filters['end'] ?? '') !== '') $q->where('occurred_at', '<', date('Y-m-d', strtotime($filters['end'] . ' +1 day')) . ' 00:00:00');
|
||||
if (($filters['keyword'] ?? '') !== '') {
|
||||
|
||||
@ -7,6 +7,7 @@ use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class TaobaoAlipaySettlementBillRepository
|
||||
@ -119,6 +120,19 @@ class TaobaoAlipaySettlementBillRepository
|
||||
if ($channel !== '') $query->where('business_bill_source', 'like', '%' . $this->escapeLike($channel) . '%');
|
||||
$item = trim((string) ($filters['bill_item'] ?? ''));
|
||||
if ($item !== '') $query->where('bill_item', $item);
|
||||
// 「总账备注」:淘系支付宝页签同页混有天猫/淘宝/阿里巴巴店铺,按店铺名前缀分平台过滤。
|
||||
$groups = is_array($filters['bill_item_groups'] ?? null) ? $filters['bill_item_groups'] : [];
|
||||
if ($groups !== []) {
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
$groups,
|
||||
'bill_item',
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'),
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'),
|
||||
);
|
||||
} else {
|
||||
LedgerRemarkFilter::itemsWhereIn($query, (array) ($filters['bill_items'] ?? []), 'bill_item');
|
||||
}
|
||||
$type = trim((string) ($filters['business_type'] ?? ''));
|
||||
if ($type === '收入') $query->where('income_amount', '>', 0);
|
||||
if ($type === '支出') $query->where('expense_amount', '>', 0);
|
||||
|
||||
@ -7,6 +7,7 @@ use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\DatabaseManager;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use RuntimeException;
|
||||
|
||||
class TaobaoWechatSettlementBillRepository
|
||||
@ -224,6 +225,8 @@ class TaobaoWechatSettlementBillRepository
|
||||
$query->where('bill_item', $billItem);
|
||||
}
|
||||
|
||||
$this->applyLedgerRemarkFilter($query, $filters);
|
||||
|
||||
$from = $this->date(isset($filters['start']) ? (string) $filters['start'] : null);
|
||||
if ($from !== null) {
|
||||
$query->where('occurred_at', '>=', $from->format('Y-m-d') . ' 00:00:00');
|
||||
@ -250,6 +253,25 @@ class TaobaoWechatSettlementBillRepository
|
||||
}
|
||||
}
|
||||
|
||||
/** 「总账备注」筛选:淘系微信页签同页混有天猫/淘宝店铺,按店铺名前缀分平台过滤。 */
|
||||
private function applyLedgerRemarkFilter(Builder $query, array $filters): void
|
||||
{
|
||||
$groups = is_array($filters['bill_item_groups'] ?? null) ? $filters['bill_item_groups'] : [];
|
||||
if ($groups !== []) {
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
$groups,
|
||||
'bill_item',
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'),
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LedgerRemarkFilter::itemsWhereIn($query, (array) ($filters['bill_items'] ?? []), 'bill_item');
|
||||
}
|
||||
|
||||
private function accountBillSortColumn(string $sort): string
|
||||
{
|
||||
return [
|
||||
|
||||
@ -31,6 +31,25 @@ class AccountLedgerRemarkMap
|
||||
'vipshop-jx' => '唯品会jx',
|
||||
];
|
||||
|
||||
/**
|
||||
* 页签 → 该页签可能出现的平台。
|
||||
*
|
||||
* 大部分页签只有一个平台;淘系支付宝与淘系微信账户同一页签内混有
|
||||
* 天猫/淘宝/阿里巴巴的原始店铺,需要按店铺名前缀再分平台。
|
||||
*/
|
||||
private const TAB_PLATFORMS = [
|
||||
'taobao-alipay' => ['淘宝', '天猫', '阿里巴巴'],
|
||||
'taobao-wechat' => ['淘宝', '天猫'],
|
||||
'pinduoduo' => ['拼多多'],
|
||||
'jd-transactions' => ['京东'],
|
||||
'jingxi' => ['京喜'],
|
||||
'jd-wallet' => ['京东'],
|
||||
'douyin' => ['抖音'],
|
||||
'dewu' => ['得物'],
|
||||
'vipshop' => ['唯品会MP'],
|
||||
'vipshop-jx' => ['唯品会jx'],
|
||||
];
|
||||
|
||||
/** 店铺名前缀可识别的平台(顺序敏感:先长后短)。 */
|
||||
private const STORE_PREFIX_PLATFORMS = [
|
||||
'唯品会JX' => '唯品会jx',
|
||||
@ -86,6 +105,123 @@ class AccountLedgerRemarkMap
|
||||
return self::TAB_PLATFORM[$tab] ?? null;
|
||||
}
|
||||
|
||||
/** 页签涉及的平台(筛选下拉与账单项目翻译共用)。 */
|
||||
public function platformsForTab(string $tab): array
|
||||
{
|
||||
if (isset(self::TAB_PLATFORMS[$tab])) {
|
||||
return self::TAB_PLATFORMS[$tab];
|
||||
}
|
||||
|
||||
$default = $this->defaultPlatform($tab);
|
||||
|
||||
return $default === null ? [] : [$default];
|
||||
}
|
||||
|
||||
/** 平台 → 店铺名前缀(一个平台可能对应多个前缀)。 */
|
||||
private function prefixesForPlatform(string $platform): array
|
||||
{
|
||||
$prefixes = [];
|
||||
foreach (self::STORE_PREFIX_PLATFORMS as $prefix => $candidate) {
|
||||
if ($candidate === $platform) {
|
||||
$prefixes[] = $prefix;
|
||||
}
|
||||
}
|
||||
|
||||
return $prefixes;
|
||||
}
|
||||
|
||||
/** 页签可选的「总账备注」(该页签涉及平台的去重并集)。 */
|
||||
public function remarksForTab(string $tab): array
|
||||
{
|
||||
$map = $this->map();
|
||||
$remarks = [];
|
||||
foreach ($this->platformsForTab($tab) as $platform) {
|
||||
foreach (array_values($map[$platform] ?? []) as $remark) {
|
||||
$remark = trim((string) $remark);
|
||||
if ($remark !== '') {
|
||||
$remarks[$remark] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$list = array_keys($remarks);
|
||||
sort($list, SORT_NATURAL | SORT_FLAG_CASE);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「总账备注」→ 按平台分组的账单项目集合。
|
||||
*
|
||||
* 单平台页签只有一组、且不做店铺名前缀限制,可直接 whereIn(bill_item, items);
|
||||
* 多平台页签(淘系支付宝 / 淘系微信)必须连店铺名前缀一起过滤,才能与页面上
|
||||
* 按「店铺名前缀 → 平台 + 账单项目」逐行取到的总账备注完全一致。
|
||||
*
|
||||
* @return list<array{platform:string,prefixes:list<string>,excludePrefixes:list<string>,items:list<string>,isDefault:bool}>
|
||||
*/
|
||||
public function billItemGroupsForTab(string $tab, ?string $remark): array
|
||||
{
|
||||
$want = trim((string) $remark);
|
||||
if ($want === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$map = $this->map();
|
||||
if ($map === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$platforms = $this->platformsForTab($tab);
|
||||
$single = count($platforms) <= 1;
|
||||
$default = $this->defaultPlatform($tab);
|
||||
$known = array_keys(self::STORE_PREFIX_PLATFORMS);
|
||||
|
||||
$groups = [];
|
||||
foreach ($platforms as $platform) {
|
||||
$items = [];
|
||||
foreach ($map[$platform] ?? [] as $item => $value) {
|
||||
if (trim((string) $value) === $want) {
|
||||
$items[] = (string) $item;
|
||||
}
|
||||
}
|
||||
if ($items === []) {
|
||||
continue;
|
||||
}
|
||||
sort($items);
|
||||
|
||||
$prefixes = $single ? [] : $this->prefixesForPlatform($platform);
|
||||
$isDefault = ! $single && $platform === $default;
|
||||
|
||||
$groups[] = [
|
||||
'platform' => $platform,
|
||||
// 单平台页签不做店铺名前缀限制(页签本身就是该平台口径)。
|
||||
'prefixes' => $prefixes,
|
||||
// 默认平台还要包含「没有已知平台前缀」的店铺。
|
||||
'excludePrefixes' => $isDefault ? array_values(array_diff($known, $prefixes)) : [],
|
||||
'items' => $items,
|
||||
'isDefault' => $isDefault,
|
||||
];
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/** 「总账备注」→ 该页签全部相关账单项目(多平台页签为各平台并集)。 */
|
||||
public function billItemsForTab(string $tab, ?string $remark): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($this->billItemGroupsForTab($tab, $remark) as $group) {
|
||||
foreach ($group['items'] as $item) {
|
||||
$items[$item] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$list = array_keys($items);
|
||||
sort($list);
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/** 解析某一行账单所属平台:优先店铺名前缀,其次页签默认。 */
|
||||
public function platform(?string $storeName, ?string $tabPlatform): ?string
|
||||
{
|
||||
|
||||
106
modules/EcommerceFinance/Services/LedgerRemarkFilter.php
Normal file
106
modules/EcommerceFinance/Services/LedgerRemarkFilter.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
|
||||
/**
|
||||
* 「总账备注」筛选的 SQL 条件翻译(页面与导出共用)。
|
||||
*
|
||||
* 单平台页签:账单项目 IN (该平台下映射到所选总账备注的账单项目)。
|
||||
* 多平台页签(淘系支付宝 / 淘系微信):同一页签混有天猫、淘宝、阿里巴巴店铺,
|
||||
* 必须按店铺名前缀分平台,否则同名账单项目在别的平台可能对应另一个总账备注。
|
||||
*/
|
||||
final class LedgerRemarkFilter
|
||||
{
|
||||
/** @param list<string> $items */
|
||||
public static function itemsWhereIn(Builder $query, array $items, string $itemColumn): void
|
||||
{
|
||||
$values = self::clean($items);
|
||||
if ($values === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereIn($itemColumn, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按平台分组收窄查询:
|
||||
* (店铺属于平台A AND 账单项目 IN A的项目) OR (店铺属于平台B AND ...) ...
|
||||
*
|
||||
* 店铺归属由调用方表达:普通账单表用店铺名列,共享原始账单镜像用 shop_id 子查询。
|
||||
*
|
||||
* @param list<array{prefixes?:list<string>,excludePrefixes?:list<string>,items?:list<string>}> $groups
|
||||
* @param callable(Builder $sub, string $prefix): void $storeLike
|
||||
* @param callable(Builder $sub, string $prefix): void $storeNotLike
|
||||
*/
|
||||
public static function applyGroups(
|
||||
Builder $query,
|
||||
array $groups,
|
||||
string $itemColumn,
|
||||
callable $storeLike,
|
||||
callable $storeNotLike,
|
||||
): void {
|
||||
$plans = [];
|
||||
foreach ($groups as $group) {
|
||||
$items = self::clean((array) ($group['items'] ?? []));
|
||||
if ($items === []) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$prefixes = self::clean((array) ($group['prefixes'] ?? []));
|
||||
$excludes = array_values(array_diff(self::clean((array) ($group['excludePrefixes'] ?? [])), $prefixes));
|
||||
$plans[] = ['prefixes' => $prefixes, 'excludes' => $excludes, 'items' => $items];
|
||||
}
|
||||
|
||||
if ($plans === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$query->where(function (Builder $outer) use ($plans, $itemColumn, $storeLike, $storeNotLike): void {
|
||||
foreach ($plans as $plan) {
|
||||
$outer->orWhere(function (Builder $sub) use ($plan, $itemColumn, $storeLike, $storeNotLike): void {
|
||||
if ($plan['prefixes'] !== [] || $plan['excludes'] !== []) {
|
||||
$sub->where(function (Builder $scope) use ($plan, $storeLike, $storeNotLike): void {
|
||||
if ($plan['prefixes'] !== []) {
|
||||
// 本平台前缀的店铺
|
||||
$scope->orWhere(function (Builder $own) use ($plan, $storeLike): void {
|
||||
foreach ($plan['prefixes'] as $prefix) {
|
||||
$storeLike($own, $prefix);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($plan['excludes'] !== []) {
|
||||
// 没有其它平台前缀的店铺归默认平台
|
||||
$scope->orWhere(function (Builder $other) use ($plan, $storeNotLike): void {
|
||||
foreach ($plan['excludes'] as $prefix) {
|
||||
$storeNotLike($other, $prefix);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$sub->whereIn($itemColumn, $plan['items']);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,mixed> $values
|
||||
* @return list<string>
|
||||
*/
|
||||
private static function clean(array $values): array
|
||||
{
|
||||
$clean = [];
|
||||
foreach ($values as $value) {
|
||||
$text = trim((string) $value);
|
||||
if ($text !== '') {
|
||||
$clean[$text] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return array_keys($clean);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,12 @@
|
||||
</el-select>
|
||||
<input v-else v-model.trim="filters.store_name" placeholder="店铺名称" />
|
||||
</label>
|
||||
<label>总账备注
|
||||
<el-select v-if="usesMirrorOptions && !filterOptionsError" v-model="filters.ledger_remark" filterable clearable :loading="filterOptionsLoading" placeholder="全部总账备注" aria-label="总账备注筛选" popper-class="account-bill-filter-options" no-match-text="没有匹配的总账备注" no-data-text="暂无总账备注" @change="applyFilters">
|
||||
<el-option v-for="remark in filterOptions.ledger_remarks" :key="remark" :label="remark" :value="remark" />
|
||||
</el-select>
|
||||
<input v-else v-model.trim="filters.ledger_remark" placeholder="总账备注" />
|
||||
</label>
|
||||
<label>账单项目
|
||||
<el-select v-if="usesMirrorOptions && !filterOptionsError" v-model="filters.bill_item" filterable clearable :loading="filterOptionsLoading" placeholder="全部账单项目" aria-label="账单项目筛选" popper-class="account-bill-filter-options" no-match-text="没有匹配的账单项目" no-data-text="暂无账单项目" @change="applyFilters">
|
||||
<el-option v-for="item in filterOptions.bill_items" :key="item" :label="item" :value="item" />
|
||||
@ -79,10 +85,10 @@ const JD_BILL_TABS = ['jd-transactions','jingxi'];
|
||||
// 时间筛选标签与该页签的时间列名保持一致(其余页签沿用「发生时间」)。
|
||||
const DATE_FILTER_LABELS = {'jd-transactions':'费用结算时间','jingxi':'费用结算时间','jd-wallet':'费用结算时间','douyin':'结算时间','vipshop':'结算时间','vipshop-jx':'结算时间'};
|
||||
const dateConfig = {dateFormat:'Y-m-d',locale:Mandarin,disableMobile:true,allowInput:false,onReady:(_,value,picker)=>picker.calendarContainer.classList.add('account-bill-calendar')};
|
||||
const emptyFilters = billType => ({store_name:'',bill_item:'',start:'',end:'',keyword:'',keyword_match:billType==='douyin'?'exact-record':'contains'});
|
||||
const emptyFilters = billType => ({store_name:'',ledger_remark:'',bill_item:'',start:'',end:'',keyword:'',keyword_match:billType==='douyin'?'exact-record':'contains'});
|
||||
export default {
|
||||
name:'AccountBills', components:{ TableColumnSettings, flatPicker, [Select.name]:Select, [Option.name]:Option }, props:{ apiBase:{type:String,required:true}, billType:{type:String,required:true}, userKey:{type:[String,Number],required:true} },
|
||||
data(){return{loading:false,loadError:false,sync:null,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:emptyFilters(this.billType),queryFilters:emptyFilters(this.billType),resultFilters:null,filterOptions:{shops:[],bill_items:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible'};},
|
||||
data(){return{loading:false,loadError:false,sync:null,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:emptyFilters(this.billType),queryFilters:emptyFilters(this.billType),resultFilters:null,filterOptions:{shops:[],bill_items:[],ledger_remarks:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible'};},
|
||||
computed:{
|
||||
isDouyin(){return this.billType==='douyin';},
|
||||
// 费用结算时间标签、来源说明与收支净额统计口径按京东系列页签(京东账单/京喜账单)统一。
|
||||
@ -104,7 +110,7 @@ export default {
|
||||
},
|
||||
mounted(){if(this.usesMirrorOptions)this.fetchFilterOptions();this.fetch();},
|
||||
methods:{
|
||||
async fetchFilterOptions(){this.filterOptionsLoading=true;this.filterOptionsError=false;try{const response=await window.axios.get(`${this.apiBase}/filter-options`);const body=response.data||{};this.filterOptions={shops:Array.isArray(body.shops)?body.shops:[],bill_items:Array.isArray(body.bill_items)?body.bill_items:[]};}catch(error){this.filterOptionsError=true;}finally{this.filterOptionsLoading=false;}},
|
||||
async fetchFilterOptions(){this.filterOptionsLoading=true;this.filterOptionsError=false;try{const response=await window.axios.get(`${this.apiBase}/filter-options`);const body=response.data||{};this.filterOptions={shops:Array.isArray(body.shops)?body.shops:[],bill_items:Array.isArray(body.bill_items)?body.bill_items:[],ledger_remarks:Array.isArray(body.ledger_remarks)?body.ledger_remarks:[]};}catch(error){this.filterOptionsError=true;}finally{this.filterOptionsLoading=false;}},
|
||||
async fetch(){const requestId=++this.requestSerial;const requestedFilters={...this.queryFilters};this.loading=true;this.loadError=false;try{const response=await window.axios.get(this.apiBase,{params:{...requestedFilters,sort:this.sort.key,dir:this.sort.dir,limit:this.perPage,page:this.currentPage}});if(requestId!==this.requestSerial)return;const body=response.data||{};this.resultFilters=requestedFilters;this.sync=body.sync||null;this.rows=body.data||[];this.total=body.total||0;this.currentPage=body.current_page||1;this.lastPage=body.last_page||1;this.perPage=body.per_page||this.perPage;this.summary=body.summary||{income:0,expense:0,balance:0,count:0};if(body.columns){this.columnsTitle=body.columns.title||'';this.sourceGroups=body.columns.groups||[];this.sourceColumns=this.attachGroups(body.columns.columns||body.columns||[],this.sourceGroups);}}catch(error){if(requestId!==this.requestSerial)return;this.rows=[];this.total=0;this.sync=null;this.summary={income:0,expense:0,balance:0,count:0};this.loadError=true;}finally{if(requestId===this.requestSerial)this.loading=false;}},
|
||||
attachGroups(columns,groups){let index=0;return columns.map((column,columnIndex)=>{while(groups[index]&&columnIndex>=groups.slice(0,index+1).reduce((sum,group)=>sum+Number(group.span||0),0))index+=1;return{...column,key:column.field||`column-${columnIndex}`,group:groups[index]?groups[index].label:''};});},
|
||||
applyFilters(){if(this.invalidDateRange)return;this.queryFilters={...this.filters};this.currentPage=1;return this.fetch();},resetFilters(){this.filters=emptyFilters(this.billType);return this.applyFilters();},changePage(page){this.currentPage=page;this.fetch();this.$nextTick(()=>{if(this.$refs.scroll)this.$refs.scroll.scrollTop=0;});},
|
||||
|
||||
@ -15,6 +15,12 @@
|
||||
</el-select>
|
||||
<input v-else v-model.trim="filters.store_name" placeholder="店铺名称" />
|
||||
</label>
|
||||
<label>总账备注
|
||||
<el-select v-if="!filterOptionsError" v-model="filters.ledger_remark" filterable clearable :loading="filterOptionsLoading" placeholder="全部总账备注" aria-label="总账备注筛选" popper-class="alipay-bill-filter-options" no-match-text="没有匹配的总账备注" no-data-text="暂无总账备注" @change="applyFilters">
|
||||
<el-option v-for="remark in filterOptions.ledger_remarks" :key="remark" :label="remark" :value="remark" />
|
||||
</el-select>
|
||||
<input v-else v-model.trim="filters.ledger_remark" placeholder="总账备注" />
|
||||
</label>
|
||||
<label>数据源渠道<input v-model.trim="filters.source_channel" placeholder="如 alipayCSV" /></label>
|
||||
<label>账单项目
|
||||
<el-select v-if="!filterOptionsError" v-model="filters.bill_item" filterable clearable :loading="filterOptionsLoading" placeholder="全部账单项目" aria-label="账单项目筛选" popper-class="alipay-bill-filter-options" no-match-text="没有匹配的账单项目" no-data-text="暂无账单项目" @change="applyFilters">
|
||||
@ -114,7 +120,7 @@ const SORTABLE = ['occurred_at', 'income', 'expense', 'balance', 'record_code'];
|
||||
export default {
|
||||
name: 'TaobaoAlipayBills', components: { TableColumnSettings, flatPicker, [Select.name]: Select, [Option.name]: Option },
|
||||
props: { apiBase: { type: String, required: true }, exportBase: { type: String, required: true }, userKey: { type: [String, Number], required: true } },
|
||||
data() { return { loading: false, error: '', rows: [], configuredColumns: [], summary: { income: 0, expense: 0, balance: 0, count: 0 }, total: 0, currentPage: 1, lastPage: 1, perPage: 20, filters: { store_name: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }, filterOptions: { shops: [], bill_items: [] }, filterOptionsLoading: false, filterOptionsError: false, sort: { key: 'occurred_at', dir: 'desc' }, exportMode: 'filtered-visible' }; },
|
||||
data() { return { loading: false, error: '', rows: [], configuredColumns: [], summary: { income: 0, expense: 0, balance: 0, count: 0 }, total: 0, currentPage: 1, lastPage: 1, perPage: 20, filters: { store_name: '', ledger_remark: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }, filterOptions: { shops: [], bill_items: [], ledger_remarks: [] }, filterOptionsLoading: false, filterOptionsError: false, sort: { key: 'occurred_at', dir: 'desc' }, exportMode: 'filtered-visible' }; },
|
||||
computed: {
|
||||
defaultColumns() { return COLUMNS; }, visibleColumns() { return this.configuredColumns.filter((column) => column.visible); },
|
||||
visibleGroups() { return this.visibleColumns.reduce((groups, column) => { const last = groups[groups.length - 1]; if (last && last.label === column.group) last.span += 1; else groups.push({ label: column.group, span: 1 }); return groups; }, []); },
|
||||
@ -126,9 +132,9 @@ export default {
|
||||
},
|
||||
mounted() { this.fetchFilterOptions(); this.fetch(); },
|
||||
methods: {
|
||||
async fetchFilterOptions() { this.filterOptionsLoading = true; this.filterOptionsError = false; try { const response = await window.axios.get(`${this.apiBase}/filter-options`); const body = response.data || {}; this.filterOptions = { shops: Array.isArray(body.shops) ? body.shops : [], bill_items: Array.isArray(body.bill_items) ? body.bill_items : [] }; } catch (error) { this.filterOptionsError = true; } finally { this.filterOptionsLoading = false; } },
|
||||
async fetchFilterOptions() { this.filterOptionsLoading = true; this.filterOptionsError = false; try { const response = await window.axios.get(`${this.apiBase}/filter-options`); const body = response.data || {}; this.filterOptions = { shops: Array.isArray(body.shops) ? body.shops : [], bill_items: Array.isArray(body.bill_items) ? body.bill_items : [], ledger_remarks: Array.isArray(body.ledger_remarks) ? body.ledger_remarks : [] }; } catch (error) { this.filterOptionsError = true; } finally { this.filterOptionsLoading = false; } },
|
||||
async fetch() { this.loading = true; this.error = ''; try { const response = await window.axios.get(this.apiBase, { params: { ...this.filters, sort: this.sort.key, dir: this.sort.dir, limit: this.perPage, page: this.currentPage } }); const body = response.data || {}; this.rows = body.data || []; this.total = body.total || 0; this.currentPage = body.current_page || 1; this.lastPage = body.last_page || 1; this.perPage = body.per_page || this.perPage; this.summary = body.summary || { income: 0, expense: 0, balance: 0, count: 0 }; } catch (error) { this.rows = []; this.total = 0; this.summary = { income: 0, expense: 0, balance: 0, count: 0 }; this.error = (error.response && error.response.data && error.response.data.message) || '淘系支付宝账单暂时无法读取,请稍后重试。'; } finally { this.loading = false; } },
|
||||
applyFilters() { if (this.invalidDateRange) return; this.currentPage = 1; this.fetch(); }, resetFilters() { this.filters = { store_name: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }; this.applyFilters(); },
|
||||
applyFilters() { if (this.invalidDateRange) return; this.currentPage = 1; this.fetch(); }, resetFilters() { this.filters = { store_name: '', ledger_remark: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }; this.applyFilters(); },
|
||||
openDatePicker(refName) { const picker = this.$refs[refName]; if (picker && picker.fp) picker.fp.open(); },
|
||||
changeDate(field, value) { const date = value || ''; if (this.filters[field] === date) return; this.filters[field] = date; this.applyFilters(); },
|
||||
changePage(page) { this.currentPage = page; this.fetch(); this.$nextTick(() => { if (this.$refs.scroll) this.$refs.scroll.scrollTop = 0; }); },
|
||||
|
||||
@ -91,6 +91,7 @@ class DewuReceivingBillsTest extends FeatureTestCase
|
||||
->assertExactJson([
|
||||
'shops' => ['得物-A店', '得物-B店'],
|
||||
'bill_items' => ['交易收入', '平台服务费'],
|
||||
'ledger_remarks' => [],
|
||||
]);
|
||||
|
||||
$response = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
|
||||
@ -496,7 +496,7 @@ class PagesTest extends FeatureTestCase
|
||||
$this->loginAs()
|
||||
->getJson(route('ecommerce-finance.alipay-bills.filter-options'))
|
||||
->assertOk()
|
||||
->assertExactJson(['shops' => ['天猫A店'], 'bill_items' => ['交易收款']]);
|
||||
->assertExactJson(['shops' => ['天猫A店'], 'bill_items' => ['交易收款'], 'ledger_remarks' => []]);
|
||||
}
|
||||
|
||||
public function testTaobaoAlipayFilterOptionsReportAnUnreadyMirror(): void
|
||||
@ -512,7 +512,8 @@ class PagesTest extends FeatureTestCase
|
||||
->getJson(route('ecommerce-finance.alipay-bills.filter-options'))
|
||||
->assertStatus(503)
|
||||
->assertJsonPath('shops', [])
|
||||
->assertJsonPath('bill_items', []);
|
||||
->assertJsonPath('bill_items', [])
|
||||
->assertJsonPath('ledger_remarks', []);
|
||||
}
|
||||
|
||||
public function testTaobaoAlipayBillsPageProvidesTheVueMountPoint()
|
||||
@ -657,6 +658,7 @@ class PagesTest extends FeatureTestCase
|
||||
->assertExactJson([
|
||||
'shops' => ['放心购旗舰店', '放心购专营店'],
|
||||
'bill_items' => ['订单收入', '交易佣金'],
|
||||
'ledger_remarks' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@ class PinduoduoReceivingBillsTest extends FeatureTestCase
|
||||
->assertExactJson([
|
||||
'shops' => ['拼多多-A店', '拼多多-B店'],
|
||||
'bill_items' => ['交易收入', '技术服务费'],
|
||||
'ledger_remarks' => [],
|
||||
]);
|
||||
|
||||
$response = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
|
||||
@ -81,6 +81,7 @@ class TaobaoWechatReceivingBillsTest extends FeatureTestCase
|
||||
->assertExactJson([
|
||||
'shops' => ['淘系微信-A店', '淘系微信-B店'],
|
||||
'bill_items' => ['交易收入', '技术服务费'],
|
||||
'ledger_remarks' => [],
|
||||
]);
|
||||
|
||||
$response = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\EcommerceFinance\Services\AccountLedgerRemarkMap;
|
||||
use Modules\EcommerceFinance\Models\AccountBill;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
@ -57,6 +59,7 @@ class VipshopJxReceivingBillsTest extends FeatureTestCase
|
||||
->assertExactJson([
|
||||
'shops' => ['唯品会JITX_测试店'],
|
||||
'bill_items' => ['客退', '销售'],
|
||||
'ledger_remarks' => [],
|
||||
]);
|
||||
|
||||
$export = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
@ -73,6 +76,48 @@ class VipshopJxReceivingBillsTest extends FeatureTestCase
|
||||
$this->assertStringNotContainsString('999.00', $csv);
|
||||
}
|
||||
|
||||
public function test_ledger_remark_filter_uses_mapped_bill_items_on_page_and_export(): void
|
||||
{
|
||||
$this->allowPage();
|
||||
$this->bill('vipshop-jx', '26060921475309', '销售', '2026-06-09 00:00:00', 82.48, 0);
|
||||
$this->bill('vipshop-jx', '26070393670736', '客退', '2026-07-03 00:00:00', 0, 68.60);
|
||||
$this->bill('vipshop', '26060100000001', '订单退款', '2026-06-01 00:00:00', 0, 999.00);
|
||||
|
||||
$now = now();
|
||||
DB::table('ec_account_ledger_remarks')->insert([
|
||||
['platform' => '唯品会jx', 'bill_item' => '销售', 'ledger_remark' => '订单', 'created_at' => $now, 'updated_at' => $now],
|
||||
['platform' => '唯品会jx', 'bill_item' => '客退', 'ledger_remark' => '订单退款', 'created_at' => $now, 'updated_at' => $now],
|
||||
]);
|
||||
app(AccountLedgerRemarkMap::class)->forget();
|
||||
|
||||
$this->loginAs()
|
||||
->getJson(route('ecommerce-finance.account-bills.filter-options', ['bill_type' => 'vipshop-jx']))
|
||||
->assertOk()
|
||||
->assertExactJson([
|
||||
'shops' => ['唯品会JITX_测试店'],
|
||||
'bill_items' => ['客退', '销售'],
|
||||
'ledger_remarks' => ['订单', '订单退款'],
|
||||
]);
|
||||
|
||||
$page = $this->loginAs()->getJson(route('ecommerce-finance.account-bills.index', [
|
||||
'bill_type' => 'vipshop-jx',
|
||||
'ledger_remark' => '订单',
|
||||
'limit' => 20,
|
||||
]))->assertOk();
|
||||
$this->assertSame(1, $page->json('total'));
|
||||
$this->assertSame('销售', $page->json('data.0.bill_item'));
|
||||
$this->assertSame(82.48, (float) $page->json('data.0.income'));
|
||||
|
||||
$export = $this->loginAs()->get(route('ecommerce-finance.account-bills.export', [
|
||||
'bill_type' => 'vipshop-jx',
|
||||
'ledger_remark' => '订单',
|
||||
]));
|
||||
$export->assertOk()->assertDownload('vipshop-jx_account_bills.csv');
|
||||
$csv = $export->streamedContent();
|
||||
$this->assertStringContainsString('26060921475309', $csv);
|
||||
$this->assertStringNotContainsString('26070393670736', $csv);
|
||||
}
|
||||
|
||||
private function bill(string $billType, string $orderNo, string $item, string $occurredAt, float $income, float $expense): void
|
||||
{
|
||||
AccountBill::create([
|
||||
|
||||
@ -55,6 +55,50 @@ class AccountLedgerRemarkMapTest extends TestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_ledger_remark_filter_options_and_bill_items_are_scoped_per_tab(): void
|
||||
{
|
||||
$now = now();
|
||||
DB::table('ec_account_ledger_remarks')->insert([
|
||||
['platform' => '京东', 'bill_item' => '货款(收)', 'ledger_remark' => '订单收入', 'created_at' => $now, 'updated_at' => $now],
|
||||
['platform' => '京东', 'bill_item' => '佣金', 'ledger_remark' => '佣金', 'created_at' => $now, 'updated_at' => $now],
|
||||
['platform' => '京喜', 'bill_item' => '货款(收)', 'ledger_remark' => '订单收入', 'created_at' => $now, 'updated_at' => $now],
|
||||
['platform' => '淘宝', 'bill_item' => '服务费', 'ledger_remark' => '违约赔付', 'created_at' => $now, 'updated_at' => $now],
|
||||
['platform' => '天猫', 'bill_item' => '服务费', 'ledger_remark' => '服务费', 'created_at' => $now, 'updated_at' => $now],
|
||||
]);
|
||||
|
||||
$map = new AccountLedgerRemarkMap();
|
||||
|
||||
$this->assertSame(['佣金', '订单收入'], $map->remarksForTab('jd-transactions'));
|
||||
$this->assertSame(['订单收入'], $map->remarksForTab('jingxi'));
|
||||
$this->assertSame(['货款(收)'], $map->billItemsForTab('jd-transactions', '订单收入'));
|
||||
$this->assertSame([], $map->billItemsForTab('jd-transactions', '不存在的备注'));
|
||||
|
||||
// 单平台页签不做店铺名前缀限制,直接按账单项目集合过滤。
|
||||
$single = $map->billItemGroupsForTab('jd-transactions', '订单收入');
|
||||
$this->assertCount(1, $single);
|
||||
$this->assertFalse($single[0]['isDefault']);
|
||||
$this->assertSame([], $single[0]['prefixes']);
|
||||
$this->assertSame([], $single[0]['excludePrefixes']);
|
||||
$this->assertSame(['货款(收)'], $single[0]['items']);
|
||||
|
||||
// 淘系支付宝同一页签混有淘宝/天猫店铺:同名账单项目在两边对应不同总账备注。
|
||||
$taobao = $map->billItemGroupsForTab('taobao-alipay', '违约赔付');
|
||||
$this->assertCount(1, $taobao);
|
||||
$this->assertSame('淘宝', $taobao[0]['platform']);
|
||||
$this->assertTrue($taobao[0]['isDefault']);
|
||||
$this->assertSame(['淘宝'], $taobao[0]['prefixes']);
|
||||
$this->assertContains('天猫', $taobao[0]['excludePrefixes']);
|
||||
$this->assertSame(['服务费'], $taobao[0]['items']);
|
||||
|
||||
$tmall = $map->billItemGroupsForTab('taobao-alipay', '服务费');
|
||||
$this->assertCount(1, $tmall);
|
||||
$this->assertSame('天猫', $tmall[0]['platform']);
|
||||
$this->assertFalse($tmall[0]['isDefault']);
|
||||
$this->assertSame(['天猫'], $tmall[0]['prefixes']);
|
||||
$this->assertSame([], $tmall[0]['excludePrefixes']);
|
||||
$this->assertSame(['服务费'], $tmall[0]['items']);
|
||||
}
|
||||
|
||||
public function test_it_matches_platform_and_bill_item_then_decorates_rows(): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
62
tests/Unit/EcommerceFinance/LedgerRemarkFilterTest.php
Normal file
62
tests/Unit/EcommerceFinance/LedgerRemarkFilterTest.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Modules\EcommerceFinance\Services\LedgerRemarkFilter;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LedgerRemarkFilterTest extends TestCase
|
||||
{
|
||||
public function test_single_platform_filter_uses_bill_item_list(): void
|
||||
{
|
||||
$query = DB::connection()->table('ec_account_bills');
|
||||
|
||||
LedgerRemarkFilter::itemsWhereIn($query, ['销售', '客退'], 'bill_item');
|
||||
LedgerRemarkFilter::itemsWhereIn($query, ['', ' '], 'bill_item');
|
||||
|
||||
$this->assertStringContainsString('in (?, ?)', $query->toSql());
|
||||
$this->assertSame(['销售', '客退'], $query->getBindings());
|
||||
}
|
||||
|
||||
public function test_multi_platform_filter_scopes_store_prefixes_per_platform(): void
|
||||
{
|
||||
$query = DB::connection()->table('taobao_wechat_bill_rows');
|
||||
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
[
|
||||
['platform' => '淘宝', 'prefixes' => ['淘宝'], 'excludePrefixes' => ['天猫'], 'items' => ['服务费'], 'isDefault' => true],
|
||||
['platform' => '天猫', 'prefixes' => ['天猫'], 'excludePrefixes' => [], 'items' => ['服务费'], 'isDefault' => false],
|
||||
],
|
||||
'bill_item',
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'),
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'),
|
||||
);
|
||||
|
||||
$sql = $query->toSql();
|
||||
$this->assertStringContainsString('"store_name" like ?', $sql);
|
||||
$this->assertStringContainsString('"store_name" not like ?', $sql);
|
||||
$this->assertStringContainsString('"bill_item" in (?)', $sql);
|
||||
// 淘宝组(本平台前缀 或 不含其它已知平台前缀)+ 天猫组
|
||||
$this->assertSame(['淘宝%', '天猫%', '服务费', '天猫%', '服务费'], $query->getBindings());
|
||||
}
|
||||
|
||||
public function test_empty_group_list_does_not_touch_the_query(): void
|
||||
{
|
||||
$query = DB::connection()->table('ec_account_bills');
|
||||
$before = $query->toSql();
|
||||
|
||||
LedgerRemarkFilter::applyGroups(
|
||||
$query,
|
||||
[],
|
||||
'bill_item',
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'like', $prefix . '%'),
|
||||
static fn (Builder $sub, string $prefix) => $sub->where('store_name', 'not like', $prefix . '%'),
|
||||
);
|
||||
|
||||
$this->assertSame($before, $query->toSql());
|
||||
$this->assertSame([], $query->getBindings());
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user