parent
873c59d774
commit
28a62434d2
@ -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,81 @@ 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;
|
||||
$errors = [];
|
||||
$remember = function (string $scope, array $parameters, callable $resolver, mixed $fallback) use ($cache, &$errors): mixed {
|
||||
try {
|
||||
return $cache->remember($scope, $parameters, $resolver);
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
$errors[] = $exception->getMessage();
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
$errors[] = '京东账单数据暂时无法读取,请稍后重试。';
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
};
|
||||
|
||||
// 三项分别读取,确保生产冷启动时一次请求就能登记列表、汇总和选项,
|
||||
// 同时保留已经命中的共享快照,不因另一项暂缺而清空整页。
|
||||
$result = $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),
|
||||
['rows' => [], 'total' => 0],
|
||||
);
|
||||
$summary = $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']),
|
||||
['amount_total' => 0, 'balance_total' => 0, 'count' => 0],
|
||||
);
|
||||
$shopOptions = $remember(
|
||||
'jd-wallet-bills.options',
|
||||
[],
|
||||
fn () => $repository->shopOptions(),
|
||||
[],
|
||||
);
|
||||
|
||||
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' => $errors === []
|
||||
? null
|
||||
: implode(' ', array_values(array_unique(array_filter($errors)))),
|
||||
];
|
||||
}
|
||||
|
||||
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 . ' < DATEADD(day, 1, CAST(? AS date))';
|
||||
$bindings[] = $toDate;
|
||||
}
|
||||
|
||||
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,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
206
resources/assets/js/views/business/marketing-account-bills.js
vendored
Normal file
206
resources/assets/js/views/business/marketing-account-bills.js
vendored
Normal file
@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 营销账户账单页面的服务端局部导航。
|
||||
*
|
||||
* 链接始终保留完整 href 作为渐进增强回退;普通左键操作只替换当前
|
||||
* 工作区并同步 History,避免平台标签、京东账单类型、筛选和分页触发整页刷新。
|
||||
*/
|
||||
(function () {
|
||||
const workspaceSelector = '[data-marketing-account-bills-workspace]';
|
||||
const pagePathMarker = '/workspace/billing-transactions/marketing-account-bills';
|
||||
let activeRequest = null;
|
||||
|
||||
function currentWorkspace() {
|
||||
return document.querySelector(workspaceSelector);
|
||||
}
|
||||
|
||||
function isSupportedUrl(url) {
|
||||
const target = new URL(url, window.location.origin);
|
||||
|
||||
return target.origin === window.location.origin
|
||||
&& target.pathname.indexOf(pagePathMarker) !== -1;
|
||||
}
|
||||
|
||||
function updateHeader(nextDocument) {
|
||||
const currentHeader = document.querySelector('[data-page-title-first]');
|
||||
const nextHeader = nextDocument.querySelector('[data-page-title-first]');
|
||||
|
||||
if (currentHeader && nextHeader) {
|
||||
currentHeader.innerHTML = nextHeader.innerHTML;
|
||||
}
|
||||
|
||||
if (nextDocument.title) {
|
||||
document.title = nextDocument.title;
|
||||
}
|
||||
}
|
||||
|
||||
function setLoading(workspace, loading) {
|
||||
const status = workspace.querySelector('[data-marketing-account-bills-status]');
|
||||
|
||||
if (loading) {
|
||||
workspace.setAttribute('aria-busy', 'true');
|
||||
if (status) {
|
||||
status.textContent = '正在加载账单数据';
|
||||
}
|
||||
} else {
|
||||
workspace.removeAttribute('aria-busy');
|
||||
}
|
||||
}
|
||||
|
||||
function showError(workspace, url) {
|
||||
const error = workspace.querySelector('[data-marketing-account-bills-navigation-error]');
|
||||
const status = workspace.querySelector('[data-marketing-account-bills-status]');
|
||||
|
||||
setLoading(workspace, false);
|
||||
|
||||
if (status) {
|
||||
status.textContent = '账单内容加载失败';
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const retry = error.querySelector('[data-marketing-account-bills-retry]');
|
||||
error.hidden = false;
|
||||
if (retry) {
|
||||
retry.href = url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initialiseInsertedContent(workspace) {
|
||||
if (window.Alpine && typeof window.Alpine.initTree === 'function') {
|
||||
window.Alpine.initTree(workspace);
|
||||
}
|
||||
}
|
||||
|
||||
async function navigate(url, options) {
|
||||
const settings = Object.assign({ pushState: true, focus: null }, options || {});
|
||||
const workspace = currentWorkspace();
|
||||
|
||||
if (!workspace || !isSupportedUrl(url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (activeRequest) {
|
||||
activeRequest.abort();
|
||||
}
|
||||
|
||||
const request = new AbortController();
|
||||
activeRequest = request;
|
||||
setLoading(workspace, true);
|
||||
|
||||
try {
|
||||
const response = await window.fetch(url, {
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
Accept: 'text/html',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
signal: request.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const nextDocument = new DOMParser().parseFromString(html, 'text/html');
|
||||
const nextWorkspace = nextDocument.querySelector(workspaceSelector);
|
||||
|
||||
if (!nextWorkspace) {
|
||||
throw new Error('目标页面缺少营销账单工作区');
|
||||
}
|
||||
|
||||
const importedWorkspace = document.importNode(nextWorkspace, true);
|
||||
workspace.replaceWith(importedWorkspace);
|
||||
initialiseInsertedContent(importedWorkspace);
|
||||
updateHeader(nextDocument);
|
||||
|
||||
const target = new URL(url, window.location.origin);
|
||||
const relativeUrl = `${target.pathname}${target.search}${target.hash}`;
|
||||
|
||||
if (settings.pushState && relativeUrl !== `${window.location.pathname}${window.location.search}${window.location.hash}`) {
|
||||
window.history.pushState({ marketingAccountBills: true }, '', relativeUrl);
|
||||
}
|
||||
|
||||
if (settings.focus) {
|
||||
const focusTarget = importedWorkspace.querySelector(settings.focus);
|
||||
if (focusTarget) {
|
||||
window.requestAnimationFrame(() => focusTarget.focus({ preventScroll: true }));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
showError(workspace, url);
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
if (activeRequest === request) {
|
||||
activeRequest = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', (event) => {
|
||||
const link = event.target.closest(
|
||||
`${workspaceSelector} .business-tab-link, `
|
||||
+ `${workspaceSelector} [data-jd-wallet-navigation], `
|
||||
+ `${workspaceSelector} .jd-wallet-pagination a`
|
||||
);
|
||||
|
||||
if (!link
|
||||
|| link.hasAttribute('data-marketing-account-bills-full-page')
|
||||
|| event.button !== 0
|
||||
|| event.metaKey
|
||||
|| event.ctrlKey
|
||||
|| event.shiftKey
|
||||
|| event.altKey
|
||||
|| link.target === '_blank'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSupportedUrl(link.href)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const focusSelector = link.classList.contains('marketing-bills-subtab')
|
||||
? '.marketing-bills-subtab.is-active'
|
||||
: '.business-tab-link.is-active';
|
||||
|
||||
navigate(link.href, { focus: focusSelector });
|
||||
}, true);
|
||||
|
||||
document.addEventListener('submit', (event) => {
|
||||
const form = event.target.closest(`${workspaceSelector} [data-jd-wallet-filter]`);
|
||||
|
||||
if (!form) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
const target = new URL(form.action, window.location.origin);
|
||||
|
||||
new FormData(form).forEach((value, key) => {
|
||||
const normalized = String(value).trim();
|
||||
if (normalized === '') {
|
||||
target.searchParams.delete(key);
|
||||
} else {
|
||||
target.searchParams.set(key, normalized);
|
||||
}
|
||||
});
|
||||
|
||||
target.searchParams.delete('page');
|
||||
navigate(target.href, { focus: '#jd-filter-store' });
|
||||
}, true);
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
if (currentWorkspace() && isSupportedUrl(window.location.href)) {
|
||||
navigate(window.location.href, { pushState: false });
|
||||
}
|
||||
});
|
||||
}());
|
||||
@ -0,0 +1,148 @@
|
||||
<!-- 京东钱包账单:仅展示 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); }
|
||||
.marketing-bills-subtab:focus-visible, .jd-wallet-button:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2); }
|
||||
[data-marketing-account-bills-workspace][aria-busy="true"] { cursor: progress; opacity: 0.72; }
|
||||
.jd-wallet-summary-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-bottom: 16px; }
|
||||
.jd-wallet-summary-card { border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; background: #fff; padding: 12px 16px; }
|
||||
.jd-wallet-summary-label { color: #595959; font-size: 0.75rem; font-weight: 500; }
|
||||
.jd-wallet-summary-value { margin-top: 4px; color: #171717; font-size: 0.875rem; font-weight: 700; font-variant-numeric: tabular-nums; }
|
||||
.jd-wallet-filter { margin-bottom: 16px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; background: var(--vip-content-bg, #F8F6F1); padding: 12px 16px; }
|
||||
.jd-wallet-error { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 16px; border: 1px solid #B80000; border-radius: 8px; background: #FFF1F1; padding: 12px 16px; color: #B80000; font-size: 0.8125rem; }
|
||||
.jd-wallet-empty { padding: 40px 20px; color: #595959; text-align: center; }
|
||||
.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; }
|
||||
@media (min-width: 640px) { .jd-wallet-summary-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
</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" data-jd-wallet-panel>
|
||||
<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']) }}"
|
||||
data-jd-wallet-navigation
|
||||
@class(['marketing-bills-subtab', 'is-active' => ! $isConsumption])
|
||||
@if (! $isConsumption) aria-current="page" @endif>
|
||||
<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']) }}"
|
||||
data-jd-wallet-navigation
|
||||
@class(['marketing-bills-subtab', 'is-active' => $isConsumption])
|
||||
@if ($isConsumption) aria-current="page" @endif>
|
||||
<span class="material-icons-outlined text-base" aria-hidden="true">receipt_long</span><span>消耗账单</span>
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<div class="jd-wallet-summary-grid">
|
||||
<div class="jd-wallet-summary-card">
|
||||
<div class="jd-wallet-summary-label">{{ $isConsumption ? '消耗金额合计' : '充值金额合计' }}(万元)</div>
|
||||
<div class="jd-wallet-summary-value">{{ number_format($amountWan, 2) }}</div>
|
||||
</div>
|
||||
<div class="jd-wallet-summary-card">
|
||||
<div class="jd-wallet-summary-label">余额合计(万元)</div>
|
||||
<div class="jd-wallet-summary-value">{{ number_format($balanceWan, 2) }}</div>
|
||||
</div>
|
||||
<div class="jd-wallet-summary-card">
|
||||
<div class="jd-wallet-summary-label">账单笔数</div>
|
||||
<div class="jd-wallet-summary-value">{{ number_format((int) ($summary['count'] ?? 0)) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (! empty($error))
|
||||
<div class="jd-wallet-error" role="alert">
|
||||
<span>{{ $error }}</span>
|
||||
<a href="{{ request()->fullUrl() }}" data-jd-wallet-navigation>重新加载</a>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="GET" action="{{ $currentUrl }}" class="jd-wallet-filter" data-jd-wallet-filter>
|
||||
<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 }}" data-jd-wallet-navigation 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="jd-wallet-empty">当前筛选条件下没有账单,可调整筛选条件后重新查询。</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,23 @@
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">
|
||||
{{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
@php
|
||||
$isMarketingAccountBillsPage = ($section['slug'] ?? null) === 'billing-transactions'
|
||||
&& ($page['slug'] ?? null) === 'marketing-account-bills';
|
||||
$marketingAccountTabTitles = [
|
||||
'taobao-wechat' => 'Alibaba',
|
||||
'jd-wallet' => '京东',
|
||||
];
|
||||
$activeContentTitle = $activeTab['title'] ?? ($page['title'] ?? $section['title']);
|
||||
|
||||
if ($isMarketingAccountBillsPage && ($activeTab['slug'] ?? null) === 'taobao-wechat') {
|
||||
$activeContentTitle = 'Alibaba';
|
||||
}
|
||||
|
||||
if ($isMarketingAccountBillsPage && ($activeTab['slug'] ?? null) === 'jd-wallet') {
|
||||
$activeContentTitle = '京东';
|
||||
if ($isMarketingAccountBillsPage && isset($marketingAccountTabTitles[$activeTab['slug'] ?? null])) {
|
||||
$activeContentTitle = $marketingAccountTabTitles[$activeTab['slug']];
|
||||
}
|
||||
@endphp
|
||||
|
||||
<x-layouts.admin>
|
||||
<x-slot name="title">
|
||||
{{ $activeContentTitle }}
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<style>
|
||||
#main-body > .container {
|
||||
max-width: none;
|
||||
@ -390,10 +390,20 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div @class([
|
||||
<div
|
||||
@class([
|
||||
'my-6',
|
||||
'business-page--marketing-account-bills' => $isMarketingAccountBillsPage,
|
||||
])>
|
||||
])
|
||||
@if ($isMarketingAccountBillsPage) data-marketing-account-bills-workspace @endif
|
||||
>
|
||||
@if ($isMarketingAccountBillsPage)
|
||||
<p class="sr-only" role="status" aria-live="polite" data-marketing-account-bills-status></p>
|
||||
<div class="biz-alert biz-alert-error" role="alert" data-marketing-account-bills-navigation-error hidden>
|
||||
<span>账单内容局部加载失败,当前页面内容已保留。</span>
|
||||
<a href="{{ request()->fullUrl() }}" data-marketing-account-bills-retry data-marketing-account-bills-full-page>重新加载</a>
|
||||
</div>
|
||||
@endif
|
||||
@if (is_string(session('success')))
|
||||
<div class="biz-alert" role="status">{{ session('success') }}</div>
|
||||
@endif
|
||||
@ -410,15 +420,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
|
||||
@ -719,7 +723,11 @@
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (($page['slug'] ?? null) === 'store-receiving-bills')
|
||||
@if ($isMarketingAccountBillsPage)
|
||||
@push('scripts_start')
|
||||
<script src="{{ asset('public/js/business/marketing-account-bills.min.js') }}?id={{ md5_file(public_path('js/business/marketing-account-bills.min.js')) }}"></script>
|
||||
@endpush
|
||||
@elseif (($page['slug'] ?? null) === 'store-receiving-bills')
|
||||
@push('scripts_start')
|
||||
<script src="{{ asset('public/js/business/pages.min.js') }}?id={{ md5_file(public_path('js/business/pages.min.js')) }}"></script>
|
||||
@endpush
|
||||
|
||||
@ -153,6 +153,7 @@ $jsCacheValid = Test-CacheFingerprint `
|
||||
-Expected $jsFingerprint `
|
||||
-RequiredOutputs @(
|
||||
(Join-Path $frontendOutputRoot 'js/auth/common.min.js'),
|
||||
(Join-Path $frontendOutputRoot 'js/business/marketing-account-bills.min.js'),
|
||||
(Join-Path $frontendOutputRoot 'js/manifest.js'),
|
||||
(Join-Path $frontendOutputRoot 'js/vendor.js'),
|
||||
(Join-Path $cachedVendorPath 'apexcharts/apexcharts.min.js'),
|
||||
@ -261,6 +262,7 @@ try {
|
||||
|
||||
foreach ($output in @(
|
||||
'public/js/auth/common.min.js',
|
||||
'public/js/business/marketing-account-bills.min.js',
|
||||
'public/js/manifest.js',
|
||||
'public/js/vendor.js',
|
||||
'public/mix-manifest.json'
|
||||
|
||||
@ -3,6 +3,11 @@
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\JdWalletBillRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
@ -18,8 +23,11 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
'tab' => 'taobao-wechat',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('淘系支付宝账户')
|
||||
->assertSeeText('Alibaba')
|
||||
->assertSeeText('京东')
|
||||
->assertDontSeeText('淘系微信账户')
|
||||
->assertDontSeeText('京东网银钱包账户')
|
||||
->assertSee('class="my-6 business-page--marketing-account-bills"', false);
|
||||
}
|
||||
|
||||
@ -38,16 +46,71 @@ class MarketingAccountBillsTabsTest extends FeatureTestCase
|
||||
->assertOk()
|
||||
->assertSeeText('充值账单')
|
||||
->assertSeeText('消耗账单')
|
||||
->assertSeeText('总存入金额(元)')
|
||||
->assertDontSeeText('非赠款消耗金额(元)');
|
||||
->assertSeeText('金额(元)')
|
||||
->assertSeeText('充值时间')
|
||||
->assertDontSeeText('投放日期')
|
||||
->assertSee('data-jd-wallet-panel', false)
|
||||
->assertSee('data-jd-wallet-navigation', false);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', array_merge($base, ['jd_ledger' => 'consumption'])))
|
||||
->assertOk()
|
||||
->assertSeeText('充值账单')
|
||||
->assertSeeText('消耗账单')
|
||||
->assertSeeText('非赠款消耗金额(元)')
|
||||
->assertDontSeeText('总存入金额(元)');
|
||||
->assertSeeText('金额(元)')
|
||||
->assertSeeText('投放日期')
|
||||
->assertSeeText('结算日期')
|
||||
->assertDontSeeText('充值时间');
|
||||
}
|
||||
|
||||
public function testJdWalletColdStartRegistersEveryRequiredSnapshotScope(): void
|
||||
{
|
||||
$this->allowMarketingAccountBillsPage();
|
||||
$scopes = [];
|
||||
$cache = Mockery::mock(UpstreamQueryCache::class);
|
||||
$cache->shouldReceive('remember')
|
||||
->times(3)
|
||||
->andReturnUsing(function (string $scope, array $parameters, callable $resolver) use (&$scopes): array {
|
||||
$scopes[] = $scope;
|
||||
|
||||
if (count($scopes) === 1) {
|
||||
throw new ServiceUnavailableHttpException(30, '首份共享快照正在准备。');
|
||||
}
|
||||
|
||||
return match ($scope) {
|
||||
'jd-wallet-bills.recharge.summary' => [
|
||||
'amount_total' => 0,
|
||||
'balance_total' => 0,
|
||||
'count' => 0,
|
||||
],
|
||||
'jd-wallet-bills.options' => [],
|
||||
default => ['rows' => [], 'total' => 0],
|
||||
};
|
||||
});
|
||||
|
||||
$factory = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$factory->shouldReceive('forSource')
|
||||
->once()
|
||||
->with('jiansun')
|
||||
->andReturn($cache);
|
||||
|
||||
$this->app->instance(UpstreamQueryCacheFactory::class, $factory);
|
||||
$this->app->instance(JdWalletBillRepository::class, Mockery::mock(JdWalletBillRepository::class));
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'billing-transactions',
|
||||
'page' => 'marketing-account-bills',
|
||||
'tab' => 'jd-wallet',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('首份共享快照正在准备。');
|
||||
|
||||
$this->assertSame([
|
||||
'jd-wallet-bills.recharge.list',
|
||||
'jd-wallet-bills.recharge.summary',
|
||||
'jd-wallet-bills.options',
|
||||
], $scopes);
|
||||
}
|
||||
|
||||
private function allowMarketingAccountBillsPage(): void
|
||||
|
||||
81
tests/Unit/EcommerceFinance/JdWalletBillRepositoryTest.php
Normal file
81
tests/Unit/EcommerceFinance/JdWalletBillRepositoryTest.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?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']);
|
||||
$this->assertStringContainsString('< DATEADD(day, 1, CAST(? AS date))', $query->calls[0]['sql']);
|
||||
$this->assertSame(['2026-08-03 00:00:00', '2026-08-03'], $query->calls[0]['bindings']);
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,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;
|
||||
@ -217,6 +218,141 @@ class JianSunSnapshotRefresherTest extends TestCase
|
||||
$this->assertSame([['supplier' => '测试厂'], 2, 50], $rawBoardPayables->arguments);
|
||||
}
|
||||
|
||||
public function test_jd_wallet_scopes_are_available_to_background_refresh(): void
|
||||
{
|
||||
$jdWallet = new class extends JdWalletBillRepository {
|
||||
public array $calls = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function paginateConsumption(
|
||||
?string $shopName = null,
|
||||
?string $fromDate = null,
|
||||
?string $toDate = null,
|
||||
int $page = 1,
|
||||
int $pageSize = 25,
|
||||
): array {
|
||||
$this->calls['consumption.list'] = func_get_args();
|
||||
|
||||
return ['rows' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
public function paginateRecharge(
|
||||
?string $shopName = null,
|
||||
?string $fromDate = null,
|
||||
?string $toDate = null,
|
||||
int $page = 1,
|
||||
int $pageSize = 25,
|
||||
): array {
|
||||
$this->calls['recharge.list'] = func_get_args();
|
||||
|
||||
return ['rows' => [], 'total' => 0];
|
||||
}
|
||||
|
||||
public function summaryConsumption(
|
||||
?string $shopName = null,
|
||||
?string $fromDate = null,
|
||||
?string $toDate = null,
|
||||
): array {
|
||||
$this->calls['consumption.summary'] = func_get_args();
|
||||
|
||||
return ['amount_total' => 0, 'balance_total' => 0, 'count' => 0];
|
||||
}
|
||||
|
||||
public function summaryRecharge(
|
||||
?string $shopName = null,
|
||||
?string $fromDate = null,
|
||||
?string $toDate = null,
|
||||
): array {
|
||||
$this->calls['recharge.summary'] = func_get_args();
|
||||
|
||||
return ['amount_total' => 0, 'balance_total' => 0, 'count' => 0];
|
||||
}
|
||||
|
||||
public function shopOptions(): array
|
||||
{
|
||||
$this->calls['options'] = [];
|
||||
|
||||
return ['测试京东店铺'];
|
||||
}
|
||||
};
|
||||
|
||||
$refresher = $this->refresher(jdWallet: $jdWallet);
|
||||
$rechargeStatus = $refresher->refresh([
|
||||
'scope' => 'jd-wallet-bills.recharge.list',
|
||||
'parameters' => [
|
||||
'filters' => [
|
||||
'store_name' => '测试京东店铺',
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-10',
|
||||
],
|
||||
'page' => 2,
|
||||
'limit' => 50,
|
||||
],
|
||||
]);
|
||||
$consumptionSummaryStatus = $refresher->refresh([
|
||||
'scope' => 'jd-wallet-bills.consumption.summary',
|
||||
'parameters' => [
|
||||
'filters' => [
|
||||
'store_name' => '测试京东店铺',
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-10',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$rechargeSummaryStatus = $refresher->refresh([
|
||||
'scope' => 'jd-wallet-bills.recharge.summary',
|
||||
'parameters' => [
|
||||
'filters' => [
|
||||
'store_name' => '测试京东店铺',
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-10',
|
||||
],
|
||||
],
|
||||
]);
|
||||
$consumptionListStatus = $refresher->refresh([
|
||||
'scope' => 'jd-wallet-bills.consumption.list',
|
||||
'parameters' => [
|
||||
'filters' => [
|
||||
'store_name' => '测试京东店铺',
|
||||
'from' => '2026-08-01',
|
||||
'to' => '2026-08-10',
|
||||
],
|
||||
'page' => 3,
|
||||
'limit' => 25,
|
||||
],
|
||||
]);
|
||||
$optionsStatus = $refresher->refresh([
|
||||
'scope' => 'jd-wallet-bills.options',
|
||||
'parameters' => [],
|
||||
]);
|
||||
|
||||
$this->assertSame('fresh', $rechargeStatus);
|
||||
$this->assertSame('fresh', $consumptionSummaryStatus);
|
||||
$this->assertSame('fresh', $rechargeSummaryStatus);
|
||||
$this->assertSame('fresh', $consumptionListStatus);
|
||||
$this->assertSame('fresh', $optionsStatus);
|
||||
$this->assertSame(
|
||||
['测试京东店铺', '2026-08-01', '2026-08-10', 2, 50],
|
||||
$jdWallet->calls['recharge.list'],
|
||||
);
|
||||
$this->assertSame(
|
||||
['测试京东店铺', '2026-08-01', '2026-08-10'],
|
||||
$jdWallet->calls['consumption.summary'],
|
||||
);
|
||||
$this->assertSame(
|
||||
['测试京东店铺', '2026-08-01', '2026-08-10'],
|
||||
$jdWallet->calls['recharge.summary'],
|
||||
);
|
||||
$this->assertSame(
|
||||
['测试京东店铺', '2026-08-01', '2026-08-10', 3, 25],
|
||||
$jdWallet->calls['consumption.list'],
|
||||
);
|
||||
$this->assertSame([], $jdWallet->calls['options']);
|
||||
}
|
||||
|
||||
private function refresher(
|
||||
?InventoryRepository $inventory = null,
|
||||
?AlibabaBillRepository $alibaba = null,
|
||||
@ -225,6 +361,7 @@ class JianSunSnapshotRefresherTest extends TestCase
|
||||
?RawBoardPayablesRepository $rawBoardPayables = null,
|
||||
?TmallTaobaoSpreadBillRepository $taobaoAlipay = null,
|
||||
?SupplierPayableReportRepository $supplierPayableReports = null,
|
||||
?JdWalletBillRepository $jdWallet = null,
|
||||
): JianSunSnapshotRefresher {
|
||||
return new JianSunSnapshotRefresher(
|
||||
new class extends JianSunQueryCache {
|
||||
@ -251,6 +388,7 @@ class JianSunSnapshotRefresherTest extends TestCase
|
||||
$taobaoAlipay ?? $this->placeholder(TmallTaobaoSpreadBillRepository::class),
|
||||
$this->placeholder(SupplierPayableRepository::class),
|
||||
$supplierPayableReports ?? $this->placeholder(SupplierPayableReportRepository::class),
|
||||
$jdWallet ?? $this->placeholder(JdWalletBillRepository::class),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
1
webpack.mix.js
vendored
1
webpack.mix.js
vendored
@ -72,6 +72,7 @@ if (buildJavaScript) {
|
||||
|
||||
// Business workspace(店铺管理 › 各子功能)
|
||||
.js('resources/assets/js/views/business/pages.js', 'public/js/business/pages.min.js')
|
||||
.js('resources/assets/js/views/business/marketing-account-bills.js', 'public/js/business/marketing-account-bills.min.js')
|
||||
|
||||
// Ecommerce Finance
|
||||
.js('resources/assets/js/views/ecommerce-finance/goods-reconciliations.js', 'public/js/ecommerce-finance/goods-reconciliations.min.js')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user