diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index a3ce2bbb..6b0a86ce 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -3,7 +3,6 @@ namespace App\Http\Controllers\Business; use App\Models\Banking\MarketingAccountBill; -use App\Models\Banking\MarketingTaobaoAlipayBill; use App\Models\StoreReceivables\StoreZhongcaiReconciliation; use App\Utilities\SidebarPermissions; use Illuminate\Routing\Controller; @@ -11,6 +10,7 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Modules\EcommerceFinance\Repositories\EdgeBandingRepository; use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository; +use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository; use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory; use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException; use Throwable; @@ -258,49 +258,76 @@ class Pages extends Controller return null; } - // 淘系支付宝 - 使用独立的流水账单表(8字段) + // 淘系支付宝 - 从上游 JianSun.tmall_spread_bills 读取并按表1字段展示(9列) if ($tab === 'taobao-alipay') { $request = request(); $storeName = mb_substr(trim((string) $request->query('store_name')), 0, 191); $fromDate = $this->dateFilter($request->query('from')); $toDate = $this->dateFilter($request->query('to')); + $flowType = trim((string) $request->query('flow_type')); + $flowType = in_array($flowType, ['income', 'expense'], true) ? $flowType : ''; + $page = max(1, (int) $request->query('page', 1)); + $pageSize = min(max((int) setting('default.list_limit', 25), 1), 100); - $query = MarketingTaobaoAlipayBill::query(); + $repository = app(TmallTaobaoSpreadBillRepository::class); + $cache = $this->upstreamCaches->forSource('jiansun'); + $filters = [ + 'store_name' => $storeName !== '' ? $storeName : null, + 'from' => $fromDate !== '' ? $fromDate : null, + 'to' => $toDate !== '' ? $toDate : null, + 'flow_type' => $flowType !== '' ? $flowType : null, + ]; - if ($storeName !== '') { - $query->where('store_name', $storeName); - } - if ($fromDate !== '') { - $query->where('transaction_date', '>=', $fromDate); - } - if ($toDate !== '') { - $query->where('transaction_date', '<=', $toDate); - } + $result = $cache->remember( + 'taobao-alipay-bills.list', + ['filters' => $filters, 'page' => $page, 'limit' => $pageSize], + fn () => $repository->paginate( + $filters['store_name'], + $filters['from'], + $filters['to'], + $filters['flow_type'], + $page, + $pageSize + ) + ); + $summary = $cache->remember( + 'taobao-alipay-bills.summary', + ['filters' => $filters], + fn () => $repository->summary( + $filters['store_name'], + $filters['from'], + $filters['to'], + $filters['flow_type'] + ) + ); + $storeOptions = $cache->remember( + 'taobao-alipay-bills.options', + [], + fn () => $repository->storeOptions() + ); - // 筛选项下拉用的店铺列表(去重) - $storeOptions = MarketingTaobaoAlipayBill::select('store_name') - ->whereNotNull('store_name') - ->distinct() - ->orderBy('store_name') - ->pluck('store_name') - ->toArray(); + $bills = new LengthAwarePaginator( + Collection::make($result['rows']), + $result['total'], + $pageSize, + $page, + [ + 'path' => $request->url(), + 'query' => $request->query(), + ] + ); return [ 'view' => 'banking.marketing_taobao_alipay_bills.partials.embedded-table', - 'bills' => $query->orderBy('transaction_date', 'desc') - ->orderBy('id', 'desc') - ->paginate(setting('default.list_limit', 25)) - ->appends([ - 'store_name' => $storeName, - 'from' => $fromDate, - 'to' => $toDate, - ]), + 'bills' => $bills, 'filters' => [ 'store_name' => $storeName, 'from' => $fromDate, 'to' => $toDate, + 'flow_type' => $flowType, 'store_options' => $storeOptions, ], + 'summary' => $summary, ]; } diff --git a/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php b/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php new file mode 100644 index 00000000..0cd5761b --- /dev/null +++ b/modules/EcommerceFinance/Repositories/TmallTaobaoSpreadBillRepository.php @@ -0,0 +1,244 @@ + 表1 列): + * shopName -> 店铺名称 + * uniTransTime -> 记账日期(datetime) + * transTime -> 交易日期(date) + * amount(分) -> 金额(元)—— 上游以「分」存储,返回时除以 100 + * balance(分) -> 余额(元)—— 上游以「分」存储,返回时除以 100 + * comment -> 备注 + * finType -> finType(CREDIT/DEBIT 原值) + * finType 派生 -> 收支类型(CREDIT=支出, DEBIT=收入) + * bizType/tradeType -> 交易类型(CASH_DEDUCT=扣款, INTEL_CHARGE_POST=充值 等) + * + * 金额单位换算:上游 amount/balance 均以「分」存储,前端期望「元」, + * 因此 normalize() 中统一除以 100,summary() 也基于「元」单位汇总。 + */ +class TmallTaobaoSpreadBillRepository +{ + private const TABLE = '[dbo].[tmall_spread_bills]'; + private const SOURCE = 'jiansun'; + + /** finType -> 中文收支类型(CREDIT=支出, DEBIT=收入,与天猫平台口径一致)*/ + private const FIN_TYPE_MAP = [ + 'CREDIT' => '支出', + 'DEBIT' => '收入', + ]; + + /** bizType -> 中文交易类型(天猫平台 bizType 枚举)*/ + private const BIZ_TYPE_MAP = [ + 'CASH_DEDUCT' => '扣款', + 'INTEL_CHARGE_POST' => '充值', + 'TRANSFER_IN' => '转入', + 'TRANSFER_OUT' => '转出', + 'REFUND' => '退款', + 'WITHDRAW' => '提现', + ]; + + /** tradeType 兜底映射 */ + private const TRADE_TYPE_MAP = [ + 'DEDUCT' => '扣款', + 'CHARGE' => '充值', + 'TRANSFER_IN' => '转入', + 'TRANSFER_OUT' => '转出', + 'REFUND' => '退款', + 'WITHDRAW' => '提现', + ]; + + private UpstreamReadOnlyQuery $query; + + public function __construct(UpstreamReadOnlyQueryFactory $factory) + { + $this->query = $factory->forSource(self::SOURCE); + } + + /** + * 店铺下拉选项(全部去重) + * + * @return string[] + */ + public function storeOptions(): array + { + $rows = $this->query->select( + 'SELECT DISTINCT shopName FROM ' . self::TABLE . ' WHERE shopName IS NOT NULL ORDER BY shopName' + ); + + return array_map(static fn (array $r) => (string) $r['shopName'], $rows); + } + + /** + * 分页查询淘系支付宝账户流水,返回表1 9 列结构。 + * + * SQL Server 2012 兼容:用 ROW_NUMBER() OVER 做分页,避免 OFFSET/FETCH。 + * + * @param string|null $flowType 收支类型筛选:income=收入,expense=支出 + * @return array{rows: array>, total: int} + */ + public function paginate( + ?string $storeName = null, + ?string $fromDate = null, // Y-m-d(按交易日期 transTime 过滤) + ?string $toDate = null, // Y-m-d + ?string $flowType = null, // 'income' | 'expense' | null + int $page = 1, + int $pageSize = 25 + ): array { + $page = max(1, $page); + $pageSize = max(1, min(200, $pageSize)); + + [$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType); + + // 总数 + $total = (int) $this->query->scalar( + 'SELECT COUNT(*) FROM ' . self::TABLE . $whereSql, + $params + ); + + if ($total === 0) { + return ['rows' => [], 'total' => 0]; + } + + // 分页(按记账日期 uniTransTime DESC, id DESC 排序,与列表展示方向一致) + $offsetStart = ($page - 1) * $pageSize + 1; + $offsetEnd = $page * $pageSize; + + $pagedSql = 'SELECT * FROM (' + . 'SELECT src.id, src.shopName, src.uniTransTime, src.transTime, ' + . 'src.amount, src.balance, src.comment, src.finType, ' + . 'src.bizType, src.tradeType, ' + . 'ROW_NUMBER() OVER (ORDER BY src.uniTransTime DESC, src.id DESC) AS rn ' + . 'FROM ' . self::TABLE . ' src' . $whereSql + . ') AS t WHERE t.rn BETWEEN ? AND ? ORDER BY t.rn'; + + $rawRows = $this->query->select( + $pagedSql, + array_merge($params, [$offsetStart, $offsetEnd]) + ); + + return [ + 'rows' => array_map(fn (array $r) => $this->normalize($r), $rawRows), + 'total' => $total, + ]; + } + + /** + * 当前筛选条件下的汇总(金额单位:元)。 + * + * @param string|null $flowType 'income'/'expense'/null + * @return array{income_total: float, expense_total: float, net: float, count: int} + */ + public function summary( + ?string $storeName = null, + ?string $fromDate = null, + ?string $toDate = null, + ?string $flowType = null + ): array { + [$whereSql, $params] = $this->buildWhere($storeName, $fromDate, $toDate, $flowType); + + // amount 单位为「分」,汇总时除以 100 转为「元」 + $row = $this->query->first( + 'SELECT ' + . 'COALESCE(SUM(CASE WHEN finType = \'DEBIT\' THEN amount ELSE 0 END), 0) AS income_total_raw, ' + . 'COALESCE(SUM(CASE WHEN finType = \'CREDIT\' THEN amount ELSE 0 END), 0) AS expense_total_raw, ' + . 'COUNT(*) AS cnt ' + . 'FROM ' . self::TABLE . $whereSql, + $params + ); + + // 分 -> 元 + $income = ((float) ($row['income_total_raw'] ?? 0)) / 100; + $expense = ((float) ($row['expense_total_raw'] ?? 0)) / 100; + + return [ + 'income_total' => $income, + 'expense_total' => $expense, + 'net' => $income - $expense, + 'count' => (int) ($row['cnt'] ?? 0), + ]; + } + + /** + * 构造 WHERE 子句。 + * + * @param string|null $flowType 'income'/'expense'/null + * @return array{0:string,1:array} + */ + private function buildWhere( + ?string $storeName, + ?string $fromDate, + ?string $toDate, + ?string $flowType = null + ): array { + $conditions = []; + $params = []; + + if ($storeName !== null && $storeName !== '') { + $conditions[] = 'shopName = ?'; + $params[] = $storeName; + } + + // 日期按交易日期 transTime 过滤;Y-m-d 校验后字符串比较 date 即可 + if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) { + $conditions[] = 'transTime >= ?'; + $params[] = $fromDate; + } + + if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) { + $conditions[] = 'transTime <= ?'; + $params[] = $toDate; + } + + // 收支类型筛选:income -> finType='DEBIT'(收入),expense -> finType='CREDIT'(支出) + if ($flowType === 'income') { + $conditions[] = 'finType = \'DEBIT\''; + } elseif ($flowType === 'expense') { + $conditions[] = 'finType = \'CREDIT\''; + } + + $whereSql = $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions); + + return [$whereSql, $params]; + } + + /** + * 把上游行归一化为表1 9 列结构(前端可直接消费)。 + * 金额单位从「分」转为「元」。 + */ + private function normalize(array $row): array + { + $finType = (string) ($row['finType'] ?? ''); + $bizType = (string) ($row['bizType'] ?? ''); + $tradeType = (string) ($row['tradeType'] ?? ''); + + // 交易类型:优先 bizType,其次 tradeType,最后返回原始 bizType + $transactionType = self::BIZ_TYPE_MAP[$bizType] + ?? self::TRADE_TYPE_MAP[$tradeType] + ?? $bizType; + + return [ + // 表1 9 列 + 'store_name' => (string) ($row['shopName'] ?? ''), + 'bookkeeping_time' => (string) ($row['uniTransTime'] ?? ''), + 'transaction_date' => (string) ($row['transTime'] ?? ''), + 'amount' => ((float) ($row['amount'] ?? 0)) / 100, + 'balance' => ((float) ($row['balance'] ?? 0)) / 100, + 'comment' => (string) ($row['comment'] ?? ''), + 'fin_type' => $finType, + 'income_expense_type' => self::FIN_TYPE_MAP[$finType] ?? '', + 'transaction_type' => $transactionType, + // 原始字段保留 + 'biz_type_raw' => $bizType, + 'trade_type_raw' => $tradeType, + ]; + } +} diff --git a/modules/EcommerceFinance/Services/JianSunSnapshotRefresher.php b/modules/EcommerceFinance/Services/JianSunSnapshotRefresher.php index b4d64108..3245d29a 100644 --- a/modules/EcommerceFinance/Services/JianSunSnapshotRefresher.php +++ b/modules/EcommerceFinance/Services/JianSunSnapshotRefresher.php @@ -8,6 +8,7 @@ use Modules\EcommerceFinance\Repositories\EdgeBandingRepository; use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository; use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository; use Modules\EcommerceFinance\Repositories\SupplierPayableRepository; +use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository; /** * 将活跃查询登记项还原成仓库调用,供定时任务刷新共享快照。 @@ -19,6 +20,7 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher private readonly EdgeBandingRepository $edgeBanding, private readonly GoodsReconciliationRepository $goods, private readonly PinduoduoSpreadBillRepository $pinduoduo, + private readonly TmallTaobaoSpreadBillRepository $taobaoAlipay, private readonly SupplierPayableRepository $supplier, ) { } @@ -76,6 +78,22 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher ), 'pinduoduo-bills.options' => $this->pinduoduo->storeOptions(), + 'taobao-alipay-bills.list' => $this->taobaoAlipay->paginate( + $storeName, + $fromDate, + $toDate, + $flowType, + $page, + $limit + ), + 'taobao-alipay-bills.summary' => $this->taobaoAlipay->summary( + $storeName, + $fromDate, + $toDate, + $flowType + ), + 'taobao-alipay-bills.options' => $this->taobaoAlipay->storeOptions(), + 'return-reconciliations.list' => $this->supplier->returnReconciliations($filters, $page, $limit), 'return-reconciliations.summary' => $this->supplier->returnReconciliationSummary($filters), 'return-reconciliations.options' => $this->supplier->filterOptionsForReturn(), diff --git a/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php b/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php index fd67038e..ab6da197 100644 --- a/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php +++ b/resources/views/banking/marketing_taobao_alipay_bills/partials/embedded-table.blade.php @@ -1,4 +1,5 @@ - + +{{-- 列顺序按表1:店铺名称 | 记账日期 | 交易日期 | 金额 | 余额 | 备注 | finType | 收支类型 | 交易类型 --}} -@php $pageBillIds = $bills->pluck('id')->map(fn($id) => (string) $id)->values(); @endphp +@php + // 汇总数据(来自 Pages.php 的 $summary 变量,金额单位为元) + $summary = $summary ?? ['income_total' => 0, 'expense_total' => 0, 'net' => 0, 'count' => 0]; + $hasSummary = ($summary['count'] ?? 0) > 0; + $incomeWan = ((float) ($summary['income_total'] ?? 0)) / 10000; + $expenseWan = ((float) ($summary['expense_total'] ?? 0)) / 10000; + $netWan = ((float) ($summary['net'] ?? 0)) / 10000; +@endphp -
-
- - {{-- 操作工具栏 --}} -
-
-
- @can('update-banking-accounts') - - @endcan -
-
- - @if ($errors->has('import')) -
- @foreach ($errors->get('import') as $message) -

{{ $message }}

- @endforeach +
+ {{-- 汇总卡:当前筛选条件下的收入/支出/净额/笔数,金额单位为万元 --}} + @if ($hasSummary) +
+
+
收入合计(万元)
+
+ + {{ number_format($incomeWan, 2) }} +
+
+
+
支出合计(万元)
+
+ - {{ number_format($expenseWan, 2) }} +
+
+
+
净额(收入 - 支出,万元)
+
+ {{ $netWan >= 0 ? '+' : '' }} {{ number_format($netWan, 2) }} +
+
+
+
流水笔数
+
+ {{ number_format((int) ($summary['count'] ?? 0)) }} +
+
@endif - {{-- 筛选器:按店铺 + 日期范围 --}} + {{-- 筛选器:按店铺 + 收支类型 + 交易日期范围 --}} @php - $filters = $filters ?? ['store_name' => '', 'from' => '', 'to' => '', 'store_options' => []]; + $filters = $filters ?? ['store_name' => '', 'from' => '', 'to' => '', 'flow_type' => '', 'store_options' => []]; $storeOptions = is_array($filters['store_options'] ?? null) ? $filters['store_options'] : []; $currentStore = (string) ($filters['store_name'] ?? ''); $currentFrom = (string) ($filters['from'] ?? ''); $currentTo = (string) ($filters['to'] ?? ''); + $currentFlowType = (string) ($filters['flow_type'] ?? ''); @endphp
- - @foreach ($storeOptions as $opt) @@ -67,28 +92,35 @@
- - 收支类型 + +
+
+ +
- - 交易日期 止 +
- - + 清除
- @if ($currentStore !== '' || $currentFrom !== '' || $currentTo !== '') + @if ($currentStore !== '' || $currentFrom !== '' || $currentTo !== '' || $currentFlowType !== '')
共匹配 {{ $bills->total() }} 条
@@ -96,62 +128,71 @@
- {{-- 8列数据表格 --}} + {{-- 9列数据表格(按表1顺序)--}}
- - - - - - - - - + + + + + + + + + @if ($bills->isEmpty()) @else @foreach ($bills as $bill) + @php + // 兼容数组(上游 Repository)和对象(本地 Model)两种结构 + $getStore = is_array($bill) ? ($bill['store_name'] ?? '') : $bill->store_name; + $getBookTime = is_array($bill) ? ($bill['bookkeeping_time'] ?? '') : ($bill->bookkeeping_time ? $bill->bookkeeping_time->format('Y-m-d H:i:s') : ''); + $getTransDate = is_array($bill) ? ($bill['transaction_date'] ?? '') : ($bill->transaction_date ? $bill->transaction_date->format('Y-m-d') : ''); + $getAmount = is_array($bill) ? ($bill['amount'] ?? 0) : $bill->operation_amount; + $getBalance = is_array($bill) ? ($bill['balance'] ?? 0) : $bill->balance_after; + $getComment = is_array($bill) ? ($bill['comment'] ?? '') : $bill->notes; + $getFinType = is_array($bill) ? ($bill['fin_type'] ?? '') : ''; + $getType = is_array($bill) ? ($bill['income_expense_type'] ?? '') : $bill->income_expense_type; + $getTradeType = is_array($bill) ? ($bill['transaction_type'] ?? '') : $bill->transaction_type; + @endphp - + + + + + - - - - - - - + @endforeach @endif @@ -160,6 +201,8 @@ @unless ($bills->isEmpty()) -
{{ $bills->links() }}
+
+ +
@endunless diff --git a/tests/Unit/EcommerceFinance/JianSunSnapshotRefresherTest.php b/tests/Unit/EcommerceFinance/JianSunSnapshotRefresherTest.php index d771eb25..39ecb1a7 100644 --- a/tests/Unit/EcommerceFinance/JianSunSnapshotRefresherTest.php +++ b/tests/Unit/EcommerceFinance/JianSunSnapshotRefresherTest.php @@ -7,6 +7,7 @@ use Modules\EcommerceFinance\Repositories\EdgeBandingRepository; use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository; use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository; use Modules\EcommerceFinance\Repositories\SupplierPayableRepository; +use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository; use Modules\EcommerceFinance\Services\JianSunQueryCache; use Modules\EcommerceFinance\Services\JianSunSnapshotRefresher; use PHPUnit\Framework\TestCase; @@ -54,8 +55,16 @@ class JianSunSnapshotRefresherTest extends TestCase $edgeBanding = (new ReflectionClass(EdgeBandingRepository::class))->newInstanceWithoutConstructor(); $goods = (new ReflectionClass(GoodsReconciliationRepository::class))->newInstanceWithoutConstructor(); + $taobaoAlipay = (new ReflectionClass(TmallTaobaoSpreadBillRepository::class))->newInstanceWithoutConstructor(); $supplier = (new ReflectionClass(SupplierPayableRepository::class))->newInstanceWithoutConstructor(); - $refresher = new JianSunSnapshotRefresher($cache, $edgeBanding, $goods, $pinduoduo, $supplier); + $refresher = new JianSunSnapshotRefresher( + $cache, + $edgeBanding, + $goods, + $pinduoduo, + $taobaoAlipay, + $supplier + ); $status = $refresher->refresh([ 'scope' => 'pinduoduo-bills.list', @@ -77,4 +86,76 @@ class JianSunSnapshotRefresherTest extends TestCase $pinduoduo->arguments ); } + + public function test_taobao_alipay_filters_are_preserved_during_background_refresh(): void + { + $cache = new class extends JianSunQueryCache { + public mixed $value = null; + + public function __construct() + { + } + + public function refresh(string $scope, array $parameters, Closure $resolver): string + { + $this->value = $resolver(); + + return 'fresh'; + } + }; + + $taobaoAlipay = new class extends TmallTaobaoSpreadBillRepository { + public array $arguments = []; + + public function __construct() + { + } + + public function paginate( + ?string $storeName = null, + ?string $fromDate = null, + ?string $toDate = null, + ?string $flowType = null, + int $page = 1, + int $pageSize = 25 + ): array { + $this->arguments = func_get_args(); + + return ['rows' => [], 'total' => 0]; + } + }; + + $edgeBanding = (new ReflectionClass(EdgeBandingRepository::class))->newInstanceWithoutConstructor(); + $goods = (new ReflectionClass(GoodsReconciliationRepository::class))->newInstanceWithoutConstructor(); + $pinduoduo = (new ReflectionClass(PinduoduoSpreadBillRepository::class))->newInstanceWithoutConstructor(); + $supplier = (new ReflectionClass(SupplierPayableRepository::class))->newInstanceWithoutConstructor(); + $refresher = new JianSunSnapshotRefresher( + $cache, + $edgeBanding, + $goods, + $pinduoduo, + $taobaoAlipay, + $supplier + ); + + $status = $refresher->refresh([ + 'scope' => 'taobao-alipay-bills.list', + 'parameters' => [ + 'filters' => [ + 'store_name' => '测试店铺', + 'from' => '2026-07-01', + 'to' => '2026-07-23', + 'flow_type' => 'income', + ], + 'page' => 2, + 'limit' => 25, + ], + ]); + + $this->assertSame('fresh', $status); + $this->assertSame( + ['测试店铺', '2026-07-01', '2026-07-23', 'income', 2, 25], + $taobaoAlipay->arguments + ); + } } diff --git a/tests/Unit/EcommerceFinance/TmallTaobaoSpreadBillRepositoryTest.php b/tests/Unit/EcommerceFinance/TmallTaobaoSpreadBillRepositoryTest.php new file mode 100644 index 00000000..1cbaaabb --- /dev/null +++ b/tests/Unit/EcommerceFinance/TmallTaobaoSpreadBillRepositoryTest.php @@ -0,0 +1,87 @@ + */ + public array $calls = []; + + public function scalar(string $sql, array $bindings = []): mixed + { + $this->calls[] = compact('sql', 'bindings') + ['method' => 'scalar']; + + return 1; + } + + public function select(string $sql, array $bindings = []): array + { + $this->calls[] = compact('sql', 'bindings') + ['method' => 'select']; + + return [[ + 'shopName' => '测试店铺', + 'uniTransTime' => '2026-07-23 12:00:00', + 'transTime' => '2026-07-23', + 'amount' => 15050, + 'balance' => 990050, + 'comment' => '测试流水', + 'finType' => 'DEBIT', + 'bizType' => 'INTEL_CHARGE_POST', + 'tradeType' => '', + ]]; + } + + public function first(string $sql, array $bindings = []): ?array + { + $this->calls[] = compact('sql', 'bindings') + ['method' => 'first']; + + return [ + 'income_total_raw' => 15050, + 'expense_total_raw' => 0, + 'cnt' => 1, + ]; + } + }; + + $factory = new class($query) extends UpstreamReadOnlyQueryFactory { + public function __construct(private readonly UpstreamReadOnlyQuery $query) + { + } + + public function forSource(string $source): UpstreamReadOnlyQuery + { + TestCase::assertSame('jiansun', $source); + + return $this->query; + } + }; + + $repository = new TmallTaobaoSpreadBillRepository($factory); + + $page = $repository->paginate('测试店铺', '2026-07-01', '2026-07-23', 'income', 1, 25); + $summary = $repository->summary('测试店铺', '2026-07-01', '2026-07-23', 'income'); + + $this->assertSame(1, $page['total']); + $this->assertSame(150.5, $page['rows'][0]['amount']); + $this->assertSame(9900.5, $page['rows'][0]['balance']); + $this->assertSame('收入', $page['rows'][0]['income_expense_type']); + $this->assertSame('充值', $page['rows'][0]['transaction_type']); + $this->assertSame(150.5, $summary['income_total']); + $this->assertSame(150.5, $summary['net']); + $this->assertStringContainsString('finType = \'DEBIT\'', $query->calls[0]['sql']); + $this->assertSame( + ['测试店铺', '2026-07-01', '2026-07-23'], + $query->calls[0]['bindings'] + ); + $this->assertStringContainsString('finType = \'DEBIT\'', $query->calls[1]['sql']); + $this->assertStringContainsString('finType = \'DEBIT\'', $query->calls[2]['sql']); + } +}
- isEmpty()) disabled @endif - x-on:change="selected = $event.target.checked ? @js($pageBillIds) : []" - x-bind:checked="selected.length === {{ $pageBillIds->count() }} && selected.length > 0" - class="h-4 w-4 rounded border-gray-300 text-green" /> - {{ trans('marketing_taobao_alipay_bills.store_name') }}{{ trans('marketing_taobao_alipay_bills.bookkeeping_time') }}{{ trans('marketing_taobao_alipay_bills.transaction_date') }}{{ trans('marketing_taobao_alipay_bills.income_expense_type') }}{{ trans('marketing_taobao_alipay_bills.transaction_type') }}{{ trans('marketing_taobao_alipay_bills.operation_amount') }}{{ trans('marketing_taobao_alipay_bills.balance_after') }}{{ trans('marketing_taobao_alipay_bills.notes') }}店铺名称记账日期交易日期金额(元)余额(元)备注finType收支类型交易类型
- {{ trans('marketing_taobao_alipay_bills.no_records') }} + 暂无账单数据
- + {{ $getStore }}{{ $getBookTime }}{{ $getTransDate }}{{ number_format((float) $getAmount, 2) }}{{ number_format((float) $getBalance, 2) }} + {{ \Illuminate\Support\Str::limit($getComment, 20) }} {{ $bill->store_name }}{{ $bill->bookkeeping_time?->format('Y-m-d H:i:s') }}{{ $bill->transaction_date->format('Y-m-d') }} - @if ($bill->income_expense_type === '收入' || $bill->income_expense_type === 'income') - {{ $bill->income_expense_type }} + @if ($getFinType === 'DEBIT') + {{ $getFinType }} + @elseif ($getFinType === 'CREDIT') + {{ $getFinType }} @else - {{ $bill->income_expense_type }} + {{ $getFinType }} @endif {{ $bill->transaction_type }}{{ number_format($bill->operation_amount, 2) }}{{ number_format($bill->balance_after, 2) }} - {{ \Illuminate\Support\Str::limit($bill->notes, 20) }} + + @if ($getType === '收入' || $getType === 'income') + {{ $getType }} + @elseif ($getType === '支出' || $getType === 'expense') + {{ $getType }} + @else + {{ $getType }} + @endif {{ $getTradeType }}