Merge remote-tracking branch 'origin/feature/chenxi-jd-wallet-upstream' into fix/WuYongJie-jd-wallet-integration
This commit is contained in:
commit
8912ae7a83
@ -16,6 +16,7 @@ use Illuminate\Support\Collection;
|
||||
use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Repositories\InventoryRepository;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
|
||||
use Modules\EcommerceFinance\Repositories\RawBoardPayablesRepository;
|
||||
@ -939,6 +940,89 @@ class Pages extends Controller
|
||||
];
|
||||
}
|
||||
|
||||
// 京东钱包 - 按附件表1分别展示充值账单和消耗账单,数据来自 JianSun 上游只读表。
|
||||
if ($tab === 'jd-wallet') {
|
||||
$request = request();
|
||||
$ledger = $request->query('jd_ledger') === 'consumption' ? 'consumption' : 'recharge';
|
||||
$storeName = mb_substr(trim((string) $request->query('store_name')), 0, 191);
|
||||
$fromDate = $this->dateFilter($request->query('from'));
|
||||
$toDate = $this->dateFilter($request->query('to'));
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$pageSize = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||||
$filters = [
|
||||
'store_name' => $storeName !== '' ? $storeName : null,
|
||||
'from' => $fromDate !== '' ? $fromDate : null,
|
||||
'to' => $toDate !== '' ? $toDate : null,
|
||||
];
|
||||
$repository = app(JdWalletBillRepository::class);
|
||||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||||
$scopePrefix = 'jd-wallet-bills.' . $ledger;
|
||||
|
||||
try {
|
||||
$result = $cache->remember(
|
||||
$scopePrefix . '.list',
|
||||
['filters' => $filters, 'page' => $page, 'limit' => $pageSize],
|
||||
fn () => $ledger === 'consumption'
|
||||
? $repository->paginateConsumption($filters['store_name'], $filters['from'], $filters['to'], $page, $pageSize)
|
||||
: $repository->paginateRecharge($filters['store_name'], $filters['from'], $filters['to'], $page, $pageSize),
|
||||
);
|
||||
$summary = $cache->remember(
|
||||
$scopePrefix . '.summary',
|
||||
['filters' => $filters],
|
||||
fn () => $ledger === 'consumption'
|
||||
? $repository->summaryConsumption($filters['store_name'], $filters['from'], $filters['to'])
|
||||
: $repository->summaryRecharge($filters['store_name'], $filters['from'], $filters['to']),
|
||||
);
|
||||
$shopOptions = $cache->remember(
|
||||
'jd-wallet-bills.options',
|
||||
[],
|
||||
fn () => $repository->shopOptions(),
|
||||
);
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
return [
|
||||
'view' => 'banking.marketing_jd_wallet_bills.partials.embedded-table',
|
||||
'ledger' => $ledger,
|
||||
'bills' => new LengthAwarePaginator([], 0, $pageSize, $page, [
|
||||
'path' => $request->url(), 'query' => $request->query(),
|
||||
]),
|
||||
'filters' => $filters,
|
||||
'summary' => ['amount_total' => 0, 'balance_total' => 0, 'count' => 0],
|
||||
'shopOptions' => [],
|
||||
'error' => $exception->getMessage(),
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return [
|
||||
'view' => 'banking.marketing_jd_wallet_bills.partials.embedded-table',
|
||||
'ledger' => $ledger,
|
||||
'bills' => new LengthAwarePaginator([], 0, $pageSize, $page, [
|
||||
'path' => $request->url(), 'query' => $request->query(),
|
||||
]),
|
||||
'filters' => $filters,
|
||||
'summary' => ['amount_total' => 0, 'balance_total' => 0, 'count' => 0],
|
||||
'shopOptions' => [],
|
||||
'error' => '京东账单数据暂时无法读取,请稍后重试。',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'view' => 'banking.marketing_jd_wallet_bills.partials.embedded-table',
|
||||
'ledger' => $ledger,
|
||||
'bills' => new LengthAwarePaginator(
|
||||
Collection::make($result['rows'] ?? []),
|
||||
(int) ($result['total'] ?? 0),
|
||||
$pageSize,
|
||||
$page,
|
||||
['path' => $request->url(), 'query' => $request->query()],
|
||||
),
|
||||
'filters' => $filters,
|
||||
'summary' => $summary,
|
||||
'shopOptions' => $shopOptions,
|
||||
'error' => null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'view' => 'banking.marketing_account_bills.partials.embedded-table',
|
||||
'platform' => $tab,
|
||||
|
||||
187
modules/EcommerceFinance/Repositories/JdWalletBillRepository.php
Normal file
187
modules/EcommerceFinance/Repositories/JdWalletBillRepository.php
Normal file
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
|
||||
/**
|
||||
* 京东钱包充值和消耗账单的只读上游适配器。
|
||||
*
|
||||
* 数据来源:JianSun.dbo.jd_spreadBills、JianSun.dbo.jd_costBills。
|
||||
* 金额字段已按元存储,查询只返回京东表格需要的字段。
|
||||
*/
|
||||
class JdWalletBillRepository
|
||||
{
|
||||
private const RECHARGE_TABLE = '[dbo].[jd_spreadBills]';
|
||||
private const CONSUMPTION_TABLE = '[dbo].[jd_costBills]';
|
||||
private const SOURCE = 'jiansun';
|
||||
|
||||
private UpstreamReadOnlyQuery $query;
|
||||
|
||||
public function __construct(UpstreamReadOnlyQueryFactory $factory)
|
||||
{
|
||||
$this->query = $factory->forSource(self::SOURCE);
|
||||
}
|
||||
|
||||
/** @return string[] */
|
||||
public function shopOptions(): array
|
||||
{
|
||||
$rows = $this->query->select(
|
||||
"SELECT TOP (500) shopName FROM (SELECT shopName FROM " . self::RECHARGE_TABLE
|
||||
. " UNION SELECT shopName FROM " . self::CONSUMPTION_TABLE
|
||||
. ") shops WHERE shopName IS NOT NULL AND LTRIM(RTRIM(shopName)) <> '' ORDER BY shopName"
|
||||
);
|
||||
|
||||
return array_values(array_map(
|
||||
static fn (array $row): string => (string) ($row['shopName'] ?? ''),
|
||||
$rows,
|
||||
));
|
||||
}
|
||||
|
||||
/** @return array{rows: array<int,array<string,mixed>>, total: int} */
|
||||
public function paginateRecharge(
|
||||
?string $shopName = null,
|
||||
?string $fromDate = null,
|
||||
?string $toDate = null,
|
||||
int $page = 1,
|
||||
int $pageSize = 25,
|
||||
): array {
|
||||
return $this->paginate(self::RECHARGE_TABLE, $this->rechargeWhere($shopName, $fromDate, $toDate), $page, $pageSize, [
|
||||
'id', 'shopName', 'amount', 'balanceAmount', 'orderTypeName', 'pk',
|
||||
'createDate', 'updateTime', 'statusName', 'remark',
|
||||
], 'createDate', 'id', fn (array $row): array => $this->normalizeRecharge($row));
|
||||
}
|
||||
|
||||
/** @return array{rows: array<int,array<string,mixed>>, total: int} */
|
||||
public function paginateConsumption(
|
||||
?string $shopName = null,
|
||||
?string $fromDate = null,
|
||||
?string $toDate = null,
|
||||
int $page = 1,
|
||||
int $pageSize = 25,
|
||||
): array {
|
||||
return $this->paginate(self::CONSUMPTION_TABLE, $this->consumptionWhere($shopName, $fromDate, $toDate), $page, $pageSize, [
|
||||
'id', 'shopName', 'amount', 'balanceAmount', 'orderTypeName', 'creatTime', 'settleTime', 'pk',
|
||||
'invoiceApplyTypeName',
|
||||
], 'creatTime', 'id', fn (array $row): array => $this->normalizeConsumption($row));
|
||||
}
|
||||
|
||||
/** @return array{amount_total: float, balance_total: float, count: int} */
|
||||
public function summaryRecharge(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array
|
||||
{
|
||||
return $this->summary(self::RECHARGE_TABLE, $this->rechargeWhere($shopName, $fromDate, $toDate));
|
||||
}
|
||||
|
||||
/** @return array{amount_total: float, balance_total: float, count: int} */
|
||||
public function summaryConsumption(?string $shopName = null, ?string $fromDate = null, ?string $toDate = null): array
|
||||
{
|
||||
return $this->summary(self::CONSUMPTION_TABLE, $this->consumptionWhere($shopName, $fromDate, $toDate));
|
||||
}
|
||||
|
||||
/** @param array{sql:string,bindings:array<int,mixed>} $where */
|
||||
private function paginate(string $table, array $where, int $page, int $pageSize, array $fields, string $dateField, string $idField, callable $normalize): array
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$pageSize = min(max(1, $pageSize), 100);
|
||||
$total = (int) $this->query->scalar('SELECT COUNT(*) FROM ' . $table . ' src' . $where['sql'], $where['bindings']);
|
||||
|
||||
if ($total === 0) {
|
||||
return ['rows' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
$start = (($page - 1) * $pageSize) + 1;
|
||||
$end = $page * $pageSize;
|
||||
$select = implode(', ', array_map(static fn (string $field): string => 'src.' . $field, $fields));
|
||||
$sql = 'SELECT * FROM ('
|
||||
. 'SELECT ' . $select . ', ROW_NUMBER() OVER (ORDER BY src.' . $dateField . ' DESC, src.' . $idField . ' DESC) AS rn '
|
||||
. 'FROM ' . $table . ' src' . $where['sql']
|
||||
. ') AS t WHERE t.rn BETWEEN ? AND ? ORDER BY t.rn';
|
||||
$rows = $this->query->select($sql, array_merge($where['bindings'], [$start, $end]));
|
||||
|
||||
return ['rows' => array_map($normalize, $rows), 'total' => $total];
|
||||
}
|
||||
|
||||
/** @param array{sql:string,bindings:array<int,mixed>} $where */
|
||||
private function summary(string $table, array $where): array
|
||||
{
|
||||
$row = $this->query->first(
|
||||
'SELECT COALESCE(SUM(ABS(amount)), 0) AS amount_total, COALESCE(SUM(balanceAmount), 0) AS balance_total, COUNT(*) AS cnt FROM '
|
||||
. $table . ' src' . $where['sql'],
|
||||
$where['bindings'],
|
||||
) ?? [];
|
||||
|
||||
return [
|
||||
'amount_total' => round((float) ($row['amount_total'] ?? 0), 2),
|
||||
'balance_total' => round((float) ($row['balance_total'] ?? 0), 2),
|
||||
'count' => (int) ($row['cnt'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array{sql:string,bindings:array<int,mixed>} */
|
||||
private function rechargeWhere(?string $shopName, ?string $fromDate, ?string $toDate): array
|
||||
{
|
||||
return $this->dateWhere('src.createDate', $shopName, $fromDate, $toDate);
|
||||
}
|
||||
|
||||
/** @return array{sql:string,bindings:array<int,mixed>} */
|
||||
private function consumptionWhere(?string $shopName, ?string $fromDate, ?string $toDate): array
|
||||
{
|
||||
return $this->dateWhere('src.creatTime', $shopName, $fromDate, $toDate);
|
||||
}
|
||||
|
||||
/** @return array{sql:string,bindings:array<int,mixed>} */
|
||||
private function dateWhere(string $dateField, ?string $shopName, ?string $fromDate, ?string $toDate): array
|
||||
{
|
||||
$conditions = [];
|
||||
$bindings = [];
|
||||
if ($shopName !== null && $shopName !== '') {
|
||||
$conditions[] = 'src.shopName = ?';
|
||||
$bindings[] = $shopName;
|
||||
}
|
||||
if ($fromDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) === 1) {
|
||||
$conditions[] = $dateField . ' >= ?';
|
||||
$bindings[] = $fromDate . ' 00:00:00';
|
||||
}
|
||||
if ($toDate !== null && preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) === 1) {
|
||||
$conditions[] = $dateField . ' <= ?';
|
||||
$bindings[] = $toDate . ' 23:59:59';
|
||||
}
|
||||
|
||||
return ['sql' => $conditions === [] ? '' : ' WHERE ' . implode(' AND ', $conditions), 'bindings' => $bindings];
|
||||
}
|
||||
|
||||
private function normalizeRecharge(array $row): array
|
||||
{
|
||||
return [
|
||||
'id' => (string) ($row['id'] ?? $row['pk'] ?? ''),
|
||||
'store_name' => (string) ($row['shopName'] ?? ''),
|
||||
'amount' => round((float) ($row['amount'] ?? 0), 2),
|
||||
'balance' => round((float) ($row['balanceAmount'] ?? 0), 2),
|
||||
'transaction_type' => (string) ($row['orderTypeName'] ?? ''),
|
||||
'order_number' => (string) ($row['pk'] ?? ''),
|
||||
'recharge_time' => (string) ($row['createDate'] ?? ''),
|
||||
'arrival_time' => (string) ($row['updateTime'] ?? ''),
|
||||
'serial_number' => (string) ($row['id'] ?? $row['pk'] ?? ''),
|
||||
'status' => (string) ($row['statusName'] ?? ''),
|
||||
'notes' => (string) ($row['remark'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeConsumption(array $row): array
|
||||
{
|
||||
$amount = abs((float) ($row['amount'] ?? 0));
|
||||
|
||||
return [
|
||||
'id' => (string) ($row['id'] ?? $row['pk'] ?? ''),
|
||||
'store_name' => (string) ($row['shopName'] ?? ''),
|
||||
'amount' => round($amount, 2),
|
||||
'balance' => round((float) ($row['balanceAmount'] ?? 0), 2),
|
||||
'transaction_type' => (string) ($row['orderTypeName'] ?? ''),
|
||||
'investment_date' => (string) ($row['creatTime'] ?? ''),
|
||||
'settlement_date' => (string) ($row['settleTime'] ?? ''),
|
||||
'serial_number' => (string) ($row['pk'] ?? $row['id'] ?? ''),
|
||||
'notes' => (string) ($row['invoiceApplyTypeName'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ use Modules\EcommerceFinance\Repositories\AlibabaBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
|
||||
use Modules\EcommerceFinance\Repositories\InventoryRepository;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductPriceRepository;
|
||||
use Modules\EcommerceFinance\Repositories\RawBoardPayablesRepository;
|
||||
@ -32,6 +33,7 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
private readonly TmallTaobaoSpreadBillRepository $taobaoAlipay,
|
||||
private readonly SupplierPayableRepository $supplier,
|
||||
private readonly SupplierPayableReportRepository $supplierPayableReports,
|
||||
private readonly ?JdWalletBillRepository $jdWallet = null,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -112,6 +114,32 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
),
|
||||
'pinduoduo-bills.options' => $this->pinduoduo->storeOptions(),
|
||||
|
||||
'jd-wallet-bills.recharge.list' => $this->jdWallet?->paginateRecharge(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$limit,
|
||||
),
|
||||
'jd-wallet-bills.recharge.summary' => $this->jdWallet?->summaryRecharge(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
),
|
||||
'jd-wallet-bills.consumption.list' => $this->jdWallet?->paginateConsumption(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
$page,
|
||||
$limit,
|
||||
),
|
||||
'jd-wallet-bills.consumption.summary' => $this->jdWallet?->summaryConsumption(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
$toDate,
|
||||
),
|
||||
'jd-wallet-bills.options' => $this->jdWallet?->shopOptions(),
|
||||
|
||||
'taobao-alipay-bills.list' => $this->taobaoAlipay->paginate(
|
||||
$storeName,
|
||||
$fromDate,
|
||||
|
||||
@ -0,0 +1,131 @@
|
||||
<!-- 京东钱包账单:仅展示 JianSun 上游只读数据 -->
|
||||
<style>
|
||||
.marketing-bills-subtabs { display: flex; gap: 8px; margin: 0 0 16px; padding-bottom: 16px; border-bottom: 1px solid var(--vip-border, #D8D2C7); }
|
||||
.marketing-bills-subtab { display: inline-flex; min-height: 34px; align-items: center; gap: 6px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; background: #fff; padding: 0 12px; color: #595959; font-size: 0.8125rem; font-weight: 500; }
|
||||
.marketing-bills-subtab:hover, .marketing-bills-subtab.is-active { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-soft, #F7F0DE); color: var(--vip-content-accent-dark, #856632); }
|
||||
.jd-wallet-table-wrap { overflow-x: auto; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; }
|
||||
.jd-wallet-table { min-width: 100%; white-space: nowrap; font-size: 0.78125rem; }
|
||||
.jd-wallet-table th { position: sticky; top: 0; padding: 0.65rem 0.75rem; border-bottom: 1px solid var(--vip-border, #D8D2C7); background: var(--vip-content-accent-soft, #F7F0DE); color: #171717; font-size: 0.75rem; font-weight: 600; text-align: left; }
|
||||
.jd-wallet-table td { padding: 0.65rem 0.75rem; border-bottom: 1px solid var(--vip-border, #D8D2C7); color: #171717; }
|
||||
.jd-wallet-table tr:last-child td { border-bottom: 0; }
|
||||
.jd-wallet-table tbody tr:hover { background: var(--vip-content-accent-soft, #F7F0DE); }
|
||||
.jd-wallet-number { text-align: right; font-variant-numeric: tabular-nums; }
|
||||
.jd-wallet-control { height: 34px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; background: #fff; padding: 0 10px; color: #171717; font-size: 0.8125rem; }
|
||||
.jd-wallet-control:focus { border-color: var(--vip-content-accent-dark, #856632); outline: none; box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2); }
|
||||
.jd-wallet-button { display: inline-flex; height: 34px; align-items: center; gap: 0.5rem; border-radius: 8px; padding: 0 14px; font-size: 0.8125rem; font-weight: 500; }
|
||||
.jd-wallet-button-primary { background: var(--vip-content-accent-dark, #856632); color: #fff; }
|
||||
.jd-wallet-button-primary:hover { background: #6A5129; }
|
||||
.jd-wallet-button-secondary { border: 1px solid var(--vip-border, #D8D2C7); background: #fff; color: #171717; }
|
||||
.jd-wallet-button-secondary:hover { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-soft, #F7F0DE); }
|
||||
.jd-wallet-pagination ul.pagination { display: flex; flex-direction: row; align-items: center; gap: 4px; margin: 0; padding: 0; list-style: none; }
|
||||
.jd-wallet-pagination .page-item { margin: 0; }
|
||||
.jd-wallet-pagination .page-link { display: inline-flex; min-width: 34px; height: 34px; align-items: center; justify-content: center; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; background: #fff; padding: 0 10px; color: #171717; font-size: 0.8125rem; text-decoration: none; }
|
||||
.jd-wallet-pagination a.page-link:hover { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-soft, #F7F0DE); color: var(--vip-content-accent-dark, #856632); }
|
||||
.jd-wallet-pagination .page-item.active .page-link { border-color: var(--vip-content-accent-dark, #856632); background: var(--vip-content-accent-dark, #856632); color: #fff; }
|
||||
.jd-wallet-pagination .page-item.disabled .page-link { color: #595959; cursor: not-allowed; }
|
||||
.jd-wallet-pagination div.lg\:flex { flex-flow: row wrap; align-items: center; justify-content: center; gap: 12px; }
|
||||
.jd-wallet-pagination div.lg\:flex > div:first-child { display: none !important; }
|
||||
.jd-wallet-pagination .pagination-xs { display: flex; width: 100%; justify-content: center; margin-top: 0; }
|
||||
.jd-wallet-pagination .pagination-xs > nav,
|
||||
.jd-wallet-pagination .pagination-xs > nav > ul { margin-right: auto; margin-left: auto; }
|
||||
</style>
|
||||
|
||||
@php
|
||||
$ledger = $ledger ?? 'recharge';
|
||||
$filters = $filters ?? ['store_name' => '', 'from' => '', 'to' => ''];
|
||||
$summary = $summary ?? ['amount_total' => 0, 'balance_total' => 0, 'count' => 0];
|
||||
$shopOptions = is_array($shopOptions ?? null) ? $shopOptions : [];
|
||||
$amountWan = (float) ($summary['amount_total'] ?? 0) / 10000;
|
||||
$balanceWan = (float) ($summary['balance_total'] ?? 0) / 10000;
|
||||
$isConsumption = $ledger === 'consumption';
|
||||
$currentUrl = request()->url();
|
||||
@endphp
|
||||
|
||||
<div class="w-full">
|
||||
<nav class="marketing-bills-subtabs" aria-label="京东账单类型">
|
||||
<a href="{{ route('business.pages.show', ['section' => request()->route('section') ?? 'billing-transactions', 'page' => request()->route('page') ?? 'marketing-account-bills', 'tab' => 'jd-wallet', 'jd_ledger' => 'recharge']) }}"
|
||||
@class(['marketing-bills-subtab', 'is-active' => ! $isConsumption])>
|
||||
<span class="material-icons-outlined text-base" aria-hidden="true">account_balance</span><span>充值账单</span>
|
||||
</a>
|
||||
<a href="{{ route('business.pages.show', ['section' => request()->route('section') ?? 'billing-transactions', 'page' => request()->route('page') ?? 'marketing-account-bills', 'tab' => 'jd-wallet', 'jd_ledger' => 'consumption']) }}"
|
||||
@class(['marketing-bills-subtab', 'is-active' => $isConsumption])>
|
||||
<span class="material-icons-outlined text-base" aria-hidden="true">receipt_long</span><span>消耗账单</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="mb-4 grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3">
|
||||
<div class="text-xs font-medium text-gray-500">{{ $isConsumption ? '消耗金额合计' : '充值金额合计' }}(万元)</div>
|
||||
<div class="mt-1 text-lg font-semibold tabular-nums">{{ number_format($amountWan, 2) }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3">
|
||||
<div class="text-xs font-medium text-gray-500">余额合计(万元)</div>
|
||||
<div class="mt-1 text-lg font-semibold tabular-nums">{{ number_format($balanceWan, 2) }}</div>
|
||||
</div>
|
||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3">
|
||||
<div class="text-xs font-medium text-gray-500">账单笔数</div>
|
||||
<div class="mt-1 text-lg font-semibold tabular-nums">{{ number_format((int) ($summary['count'] ?? 0)) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (! empty($error))
|
||||
<div class="mb-4 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700" role="alert">{{ $error }}</div>
|
||||
@endif
|
||||
|
||||
<form method="GET" action="{{ $currentUrl }}" class="mb-4 rounded-lg border border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<input type="hidden" name="jd_ledger" value="{{ $ledger }}" />
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
<div>
|
||||
<label for="jd-filter-store" class="mb-1 block text-xs font-medium text-gray-600">店铺名称</label>
|
||||
<select id="jd-filter-store" name="store_name" class="jd-wallet-control min-w-[200px]">
|
||||
<option value="">全部店铺</option>
|
||||
@foreach ($shopOptions as $shop)
|
||||
<option value="{{ $shop }}" @selected(($filters['store_name'] ?? '') === $shop)>{{ $shop }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="jd-filter-from" class="mb-1 block text-xs font-medium text-gray-600">账单日期 起</label>
|
||||
<input id="jd-filter-from" type="date" name="from" value="{{ $filters['from'] ?? '' }}" class="jd-wallet-control" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="jd-filter-to" class="mb-1 block text-xs font-medium text-gray-600">账单日期 止</label>
|
||||
<input id="jd-filter-to" type="date" name="to" value="{{ $filters['to'] ?? '' }}" class="jd-wallet-control" />
|
||||
</div>
|
||||
<div class="flex items-end gap-2">
|
||||
<button type="submit" class="jd-wallet-button jd-wallet-button-primary"><span class="material-icons text-base" aria-hidden="true">search</span>查询</button>
|
||||
<a href="{{ $currentUrl }}?jd_ledger={{ $ledger }}" class="jd-wallet-button jd-wallet-button-secondary"><span class="material-icons text-base" aria-hidden="true">clear</span>清除</a>
|
||||
</div>
|
||||
<div class="ml-auto self-center text-xs text-gray-500">共匹配 {{ $bills->total() }} 条</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="jd-wallet-table-wrap">
|
||||
<table class="jd-wallet-table">
|
||||
<thead><tr>
|
||||
@if ($isConsumption)
|
||||
<th>店铺名称</th><th class="jd-wallet-number">金额(元)</th><th class="jd-wallet-number">余额(元)</th><th>交易类型</th><th>投放日期</th><th>结算日期</th><th>流水号</th>
|
||||
@else
|
||||
<th>店铺名称</th><th class="jd-wallet-number">金额(元)</th><th class="jd-wallet-number">余额(元)</th><th>交易类型</th><th>商户订单号</th><th>充值时间</th><th>到账时间</th><th>流水号</th><th>状态</th><th>备注</th>
|
||||
@endif
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@forelse ($bills as $bill)
|
||||
<tr>
|
||||
@if ($isConsumption)
|
||||
<td>{{ $bill['store_name'] }}</td><td class="jd-wallet-number">{{ number_format($bill['amount'], 2) }}</td><td class="jd-wallet-number">{{ number_format($bill['balance'], 2) }}</td><td>{{ $bill['transaction_type'] }}</td><td>{{ $bill['investment_date'] }}</td><td>{{ $bill['settlement_date'] }}</td><td>{{ $bill['serial_number'] }}</td>
|
||||
@else
|
||||
<td>{{ $bill['store_name'] }}</td><td class="jd-wallet-number">{{ number_format($bill['amount'], 2) }}</td><td class="jd-wallet-number">{{ number_format($bill['balance'], 2) }}</td><td>{{ $bill['transaction_type'] }}</td><td>{{ $bill['order_number'] }}</td><td>{{ $bill['recharge_time'] }}</td><td>{{ $bill['arrival_time'] }}</td><td>{{ $bill['serial_number'] }}</td><td>{{ $bill['status'] }}</td><td class="max-w-xs truncate" title="{{ $bill['notes'] }}">{{ \Illuminate\Support\Str::limit($bill['notes'], 20) }}</td>
|
||||
@endif
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="{{ $isConsumption ? 7 : 10 }}" class="px-5 py-10 text-center text-gray-400">暂无符合条件的账单</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@unless ($bills->isEmpty())
|
||||
<div class="jd-wallet-pagination mt-5"><x-pagination :items="$bills" /></div>
|
||||
@endunless
|
||||
</div>
|
||||
@ -1,23 +1,24 @@
|
||||
@php
|
||||
$isMarketingAccountBillsPage = ($section['slug'] ?? null) === 'billing-transactions'
|
||||
&& ($page['slug'] ?? null) === 'marketing-account-bills';
|
||||
$marketingAccountTabTitles = [
|
||||
'taobao-alipay' => '淘系账户',
|
||||
'taobao-wechat' => '阿里巴巴账户',
|
||||
'jd-wallet' => '京东账户',
|
||||
];
|
||||
$activeContentTitle = $activeTab['title'] ?? ($page['title'] ?? $section['title']);
|
||||
|
||||
if ($isMarketingAccountBillsPage && isset($marketingAccountTabTitles[$activeTab['slug'] ?? null])) {
|
||||
$activeContentTitle = $marketingAccountTabTitles[$activeTab['slug']];
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">
|
||||
{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}
|
||||
{{ $activeContentTitle }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@php
|
||||
$isMarketingAccountBillsPage = ($section['slug'] ?? null) === 'billing-transactions'
|
||||
&& ($page['slug'] ?? null) === 'marketing-account-bills';
|
||||
$activeContentTitle = $activeTab['title'] ?? ($page['title'] ?? $section['title']);
|
||||
|
||||
if ($isMarketingAccountBillsPage && ($activeTab['slug'] ?? null) === 'taobao-wechat') {
|
||||
$activeContentTitle = 'Alibaba';
|
||||
}
|
||||
|
||||
if ($isMarketingAccountBillsPage && ($activeTab['slug'] ?? null) === 'jd-wallet') {
|
||||
$activeContentTitle = '京东';
|
||||
}
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
#main-body > .container {
|
||||
max-width: none;
|
||||
@ -410,15 +411,9 @@
|
||||
<nav class="business-tab-bar" aria-label="{{ $page['title'] }}">
|
||||
@foreach ($tabs as $tab)
|
||||
@php
|
||||
$visibleTabTitle = $tab['title'];
|
||||
|
||||
if ($isMarketingAccountBillsPage && $tab['slug'] === 'taobao-wechat') {
|
||||
$visibleTabTitle = 'Alibaba';
|
||||
}
|
||||
|
||||
if ($isMarketingAccountBillsPage && $tab['slug'] === 'jd-wallet') {
|
||||
$visibleTabTitle = '京东';
|
||||
}
|
||||
$visibleTabTitle = $isMarketingAccountBillsPage
|
||||
? ($marketingAccountTabTitles[$tab['slug']] ?? $tab['title'])
|
||||
: $tab['title'];
|
||||
@endphp
|
||||
|
||||
<a
|
||||
|
||||
@ -18,8 +18,11 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
'tab' => 'taobao-wechat',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('Alibaba')
|
||||
->assertSeeText('京东')
|
||||
->assertSeeText('淘系账户')
|
||||
->assertSeeText('阿里巴巴账户')
|
||||
->assertSeeText('京东账户')
|
||||
->assertDontSeeText('淘系支付宝账户')
|
||||
->assertDontSeeText('京东网银钱包账户')
|
||||
->assertSee('class="my-6 business-page--marketing-account-bills"', false);
|
||||
}
|
||||
|
||||
@ -38,16 +41,19 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
->assertOk()
|
||||
->assertSeeText('充值账单')
|
||||
->assertSeeText('消耗账单')
|
||||
->assertSeeText('总存入金额(元)')
|
||||
->assertDontSeeText('非赠款消耗金额(元)');
|
||||
->assertSeeText('金额(元)')
|
||||
->assertSeeText('充值时间')
|
||||
->assertDontSeeText('投放日期');
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', array_merge($base, ['jd_ledger' => 'consumption'])))
|
||||
->assertOk()
|
||||
->assertSeeText('充值账单')
|
||||
->assertSeeText('消耗账单')
|
||||
->assertSeeText('非赠款消耗金额(元)')
|
||||
->assertDontSeeText('总存入金额(元)');
|
||||
->assertSeeText('金额(元)')
|
||||
->assertSeeText('投放日期')
|
||||
->assertSeeText('结算日期')
|
||||
->assertDontSeeText('充值时间');
|
||||
}
|
||||
|
||||
private function allowMarketingAccountBillsPage(): void
|
||||
|
||||
79
tests/Unit/EcommerceFinance/JdWalletBillRepositoryTest.php
Normal file
79
tests/Unit/EcommerceFinance/JdWalletBillRepositoryTest.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class JdWalletBillRepositoryTest extends TestCase
|
||||
{
|
||||
public function test_recharge_and_consumption_rows_are_normalized_in_yuan(): void
|
||||
{
|
||||
$query = new class('jiansun') extends UpstreamReadOnlyQuery {
|
||||
public array $calls = [];
|
||||
|
||||
public function scalar(string $sql, array $bindings = []): mixed
|
||||
{
|
||||
$this->calls[] = ['method' => 'scalar', 'sql' => $sql, 'bindings' => $bindings];
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function select(string $sql, array $bindings = []): array
|
||||
{
|
||||
$this->calls[] = ['method' => 'select', 'sql' => $sql, 'bindings' => $bindings];
|
||||
|
||||
return [[
|
||||
'id' => 42,
|
||||
'shopName' => '测试京东店铺',
|
||||
'amount' => -12.345,
|
||||
'balanceAmount' => 98.765,
|
||||
'orderTypeName' => '快车扣费',
|
||||
'pk' => '流水-1',
|
||||
'createDate' => '2026-08-03 12:00:00',
|
||||
'updateTime' => '2026-08-03 12:01:00',
|
||||
'statusName' => '成功',
|
||||
'remark' => '备注',
|
||||
'creatTime' => '2026-08-03 12:00:00',
|
||||
'settleTime' => '2026-08-04 00:00:00',
|
||||
'invoiceApplyTypeName' => '推广费开票',
|
||||
]];
|
||||
}
|
||||
|
||||
public function first(string $sql, array $bindings = []): ?array
|
||||
{
|
||||
$this->calls[] = ['method' => 'first', 'sql' => $sql, 'bindings' => $bindings];
|
||||
|
||||
return ['amount_total' => 12.345, 'balance_total' => 98.765, '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 JdWalletBillRepository($factory);
|
||||
$recharge = $repository->paginateRecharge(null, '2026-08-03', '2026-08-03', 1, 25);
|
||||
$consumption = $repository->paginateConsumption(null, '2026-08-03', '2026-08-03', 1, 25);
|
||||
$summary = $repository->summaryConsumption(null, '2026-08-03', '2026-08-03');
|
||||
|
||||
$this->assertSame(1, $recharge['total']);
|
||||
$this->assertSame('2026-08-03 12:00:00', $recharge['rows'][0]['recharge_time']);
|
||||
$this->assertSame('成功', $recharge['rows'][0]['status']);
|
||||
$this->assertSame(12.35, $consumption['rows'][0]['amount']);
|
||||
$this->assertSame('推广费开票', $consumption['rows'][0]['notes']);
|
||||
$this->assertSame(12.35, $summary['amount_total']);
|
||||
$this->assertStringContainsString('ABS(amount)', $query->calls[4]['sql']);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user