feat: 提现管理增加提现金额专用导入模板 (#528)
Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
This commit is contained in:
parent
0f8ab6e5ae
commit
46b9acae2e
@ -126,7 +126,7 @@ class WithdrawalManagementReport
|
||||
)->all();
|
||||
$compliance = $rows->mapWithKeys(
|
||||
fn (StoreReceivingAccount $row): array => [
|
||||
(int) $row->id => $this->complianceFor(
|
||||
(int) $row->id => self::complianceFor(
|
||||
$balances[(int) $row->id] ?? ['status' => 'not_ready', 'balance' => null],
|
||||
$reserveStandards[(int) $row->id] ?? null,
|
||||
$withdrawalAmounts[(int) $row->id] ?? null,
|
||||
@ -350,7 +350,7 @@ class WithdrawalManagementReport
|
||||
}
|
||||
|
||||
/** @param array{status:string,balance:?float} $balance */
|
||||
private function complianceFor(array $balance, ?int $reserve, ?float $withdrawalAmount): ?string
|
||||
public static function complianceFor(array $balance, ?int $reserve, ?float $withdrawalAmount): ?string
|
||||
{
|
||||
if (($balance['status'] ?? '') !== 'matched' || $balance['balance'] === null || $reserve === null || $withdrawalAmount === null) {
|
||||
return null;
|
||||
|
||||
@ -12,6 +12,7 @@ use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataValidation;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
@ -30,6 +31,17 @@ class WithdrawalManagementSpreadsheet
|
||||
];
|
||||
|
||||
private const CLEAR_VALUE = '[清空]';
|
||||
private const WITHDRAWAL_TEMPLATE_HEADERS = [
|
||||
'店铺平台',
|
||||
'ERP店铺名称',
|
||||
'平台店铺ID',
|
||||
'余额日期',
|
||||
'店铺余额',
|
||||
'预留标准',
|
||||
'提现金额',
|
||||
'提现后余额',
|
||||
'是否达标',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly JushuitanShopBalanceRepository $shopBalances,
|
||||
@ -85,10 +97,113 @@ class WithdrawalManagementSpreadsheet
|
||||
return $this->download($spreadsheet, 'withdrawal_management_import_template.xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* 按当前筛选预填店铺和余额参考值,同事只需填写提现金额。
|
||||
*
|
||||
* @param array<string,mixed> $report
|
||||
*/
|
||||
public function withdrawalTemplate(array $report): StreamedResponse
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('提现金额');
|
||||
$this->writeHeader($sheet, self::WITHDRAWAL_TEMPLATE_HEADERS);
|
||||
$this->sizeColumns($sheet, [14, 30, 20, 14, 18, 18, 18, 18, 14]);
|
||||
|
||||
$rowNumber = 2;
|
||||
foreach ($report['rows'] as $row) {
|
||||
$storeId = (int) $row->id;
|
||||
$balanceMatch = $report['balances'][$storeId] ?? [
|
||||
'status' => 'not_ready',
|
||||
'balance' => null,
|
||||
];
|
||||
$hasBalance = ($balanceMatch['status'] ?? '') === 'matched'
|
||||
&& ($balanceMatch['balance'] ?? null) !== null;
|
||||
$reserve = $report['reserveStandards'][$storeId] ?? null;
|
||||
$amount = $report['withdrawalAmounts'][$storeId] ?? null;
|
||||
|
||||
foreach ([
|
||||
'A' => (string) ($row->store_platform ?? ''),
|
||||
'B' => (string) ($row->erp_store_name ?? ''),
|
||||
'C' => (string) ($row->store_id ?? ''),
|
||||
'D' => (string) $report['balanceDate'],
|
||||
] as $column => $value) {
|
||||
$sheet->setCellValueExplicit("{$column}{$rowNumber}", $value, DataType::TYPE_STRING);
|
||||
}
|
||||
|
||||
if ($hasBalance) {
|
||||
$sheet->setCellValue("E{$rowNumber}", (float) $balanceMatch['balance']);
|
||||
} else {
|
||||
$sheet->setCellValueExplicit("E{$rowNumber}", $this->balanceStatus((string) ($balanceMatch['status'] ?? 'not_ready')), DataType::TYPE_STRING);
|
||||
}
|
||||
if ($reserve !== null) {
|
||||
$sheet->setCellValue("F{$rowNumber}", $reserve);
|
||||
} else {
|
||||
$sheet->setCellValueExplicit("F{$rowNumber}", '待配置', DataType::TYPE_STRING);
|
||||
}
|
||||
if ($amount !== null) {
|
||||
$sheet->setCellValue("G{$rowNumber}", $amount);
|
||||
}
|
||||
|
||||
$sheet->setCellValue(
|
||||
"H{$rowNumber}",
|
||||
"=IF(OR(NOT(ISNUMBER(E{$rowNumber})),G{$rowNumber}=\"\"),\"\",E{$rowNumber}-G{$rowNumber})",
|
||||
);
|
||||
$sheet->setCellValue(
|
||||
"I{$rowNumber}",
|
||||
"=IF(OR(H{$rowNumber}=\"\",NOT(ISNUMBER(F{$rowNumber}))),\"\",IF(H{$rowNumber}<=F{$rowNumber},\"是\",\"否\"))",
|
||||
);
|
||||
$rowNumber++;
|
||||
}
|
||||
|
||||
$lastDataRow = max(2, $rowNumber - 1);
|
||||
$sheet->getStyle("E2:I{$lastDataRow}")->getNumberFormat()->setFormatCode('#,##0.00');
|
||||
$sheet->getStyle("E2:I{$lastDataRow}")->getAlignment()->setHorizontal(Alignment::HORIZONTAL_RIGHT);
|
||||
$sheet->getStyle("G2:G{$lastDataRow}")->getFill()
|
||||
->setFillType(Fill::FILL_SOLID)
|
||||
->getStartColor()->setARGB('FFF7F0DE');
|
||||
$sheet->getStyle('G1')->getFill()
|
||||
->setFillType(Fill::FILL_SOLID)
|
||||
->getStartColor()->setARGB('FFB9954F');
|
||||
|
||||
$validation = new DataValidation();
|
||||
$validation->setType(DataValidation::TYPE_DECIMAL)
|
||||
->setOperator(DataValidation::OPERATOR_GREATERTHANOREQUAL)
|
||||
->setFormula1('0')
|
||||
->setAllowBlank(true)
|
||||
->setShowErrorMessage(true)
|
||||
->setErrorTitle('提现金额格式错误')
|
||||
->setError('请填写非负金额,最多两位小数。');
|
||||
$sheet->setDataValidation("G2:G{$lastDataRow}", $validation);
|
||||
|
||||
$instructions = $spreadsheet->createSheet();
|
||||
$instructions->setTitle('填写说明');
|
||||
$instructions->fromArray([
|
||||
['提现金额导入说明'],
|
||||
['1. 本模板按下载时的页面筛选生成,已预填店铺标识、余额日期、店铺余额和预留标准。'],
|
||||
['2. 只需填写黄色“提现金额”列;未填写金额的店铺不会提交。填写“' . self::CLEAR_VALUE . '”可清空该店铺提现金额。'],
|
||||
['3. 店铺余额、预留标准、提现后余额和是否达标仅用于下载时预览,系统会在上传后按最新余额日期重新计算。'],
|
||||
['4. 判定规则:店铺余额 - 提现金额 ≤ 预留标准时,“是否达标”为“是”,即该笔提现按大笔提现处理。'],
|
||||
['5. 模板余额日期为 ' . $report['balanceDate'] . ';日期滚动后请重新下载模板,避免上传过期金额。'],
|
||||
], null, 'A1');
|
||||
$instructions->getStyle('A1')->getFont()->setBold(true)->setSize(14);
|
||||
$instructions->getColumnDimension('A')->setWidth(96);
|
||||
$instructions->getStyle('A1:A6')->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
|
||||
foreach (range(1, 6) as $row) {
|
||||
$instructions->getRowDimension($row)->setRowHeight($row === 1 ? 26 : 36);
|
||||
}
|
||||
$spreadsheet->setActiveSheetIndex(0);
|
||||
|
||||
return $this->download(
|
||||
$spreadsheet,
|
||||
'withdrawal_amount_import_' . str_replace('-', '', (string) $report['balanceDate']) . '.xlsx',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* total:int,changed:int,unchanged:int,
|
||||
* preview:list<array{platform:string,erp_store_name:string,store_id:string,changes:string}>,
|
||||
* preview:list<array{platform:string,erp_store_name:string,store_id:string,changes:string,withdrawal_amount:?float,post_withdrawal_balance:?float,reserve_standard:?int,compliance:?string}>,
|
||||
* plan:list<array{id:int,updated_at:string,changes:array<string,string|null>}>
|
||||
* }
|
||||
*/
|
||||
@ -104,7 +219,7 @@ class WithdrawalManagementSpreadsheet
|
||||
'import' => 'Excel 文件无法读取,请确认文件未损坏并使用最新模板。',
|
||||
]);
|
||||
}
|
||||
[$headerIndex, $headers] = $this->headers($matrix);
|
||||
[$headerIndex, $headers, $headerMode] = $this->headers($matrix);
|
||||
$indexes = array_flip($headers);
|
||||
$balanceDate = $this->currentBalanceDate();
|
||||
$accounts = StoreReceivingAccount::withoutGlobalScopes()
|
||||
@ -112,6 +227,8 @@ class WithdrawalManagementSpreadsheet
|
||||
->whereNull('deleted_at')
|
||||
->get();
|
||||
[$byId, $byName] = $this->indexes($accounts);
|
||||
$balances = $this->shopBalances->matchesForStores($accounts, $balanceDate);
|
||||
$withdrawalOnlyMode = $headerMode === 'withdrawal';
|
||||
$errors = [];
|
||||
$plan = [];
|
||||
$preview = [];
|
||||
@ -121,12 +238,15 @@ class WithdrawalManagementSpreadsheet
|
||||
|
||||
foreach (array_slice($matrix, $headerIndex + 1, null, true) as $matrixIndex => $row) {
|
||||
$values = [];
|
||||
foreach (self::CONFIG_HEADERS as $header) {
|
||||
$values[$header] = trim((string) ($row[$indexes[$header]] ?? ''));
|
||||
foreach ([...self::CONFIG_HEADERS, '提现金额', '余额日期'] as $header) {
|
||||
$values[$header] = isset($indexes[$header])
|
||||
? trim((string) ($row[$indexes[$header]] ?? ''))
|
||||
: '';
|
||||
}
|
||||
// 专用模板会预填所有店铺,只把填写了提现金额的行视为提交数据。
|
||||
if ($withdrawalOnlyMode && $values['提现金额'] === '') {
|
||||
continue;
|
||||
}
|
||||
$values['提现金额'] = isset($indexes['提现金额'])
|
||||
? trim((string) ($row[$indexes['提现金额']] ?? ''))
|
||||
: '';
|
||||
if (count(array_filter($values, static fn (string $value): bool => $value !== '')) === 0) {
|
||||
continue;
|
||||
}
|
||||
@ -140,8 +260,13 @@ class WithdrawalManagementSpreadsheet
|
||||
$erpStoreName = mb_substr($values['ERP店铺名称'], 0, 191);
|
||||
$storeId = mb_substr(ltrim($values['平台店铺ID'], "'"), 0, 191);
|
||||
|
||||
if ($platform === '' || $erpStoreName === '') {
|
||||
$errors[] = "第 {$line} 行缺少店铺平台或 ERP 店铺名称";
|
||||
if ($platform === '' || ($storeId === '' && $erpStoreName === '')) {
|
||||
$errors[] = "第 {$line} 行缺少店铺平台或店铺标识";
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($withdrawalOnlyMode && $values['余额日期'] !== '' && $values['余额日期'] !== $balanceDate) {
|
||||
$errors[] = "第 {$line} 行余额日期 {$values['余额日期']} 已过期,请按最新余额日期 {$balanceDate} 重新下载模板";
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -196,11 +321,40 @@ class WithdrawalManagementSpreadsheet
|
||||
'changes' => $changes,
|
||||
];
|
||||
if (count($preview) < 20) {
|
||||
$amount = array_key_exists('withdrawal_amount', $changes)
|
||||
? ($changes['withdrawal_amount'] === null ? null : (float) $changes['withdrawal_amount'])
|
||||
: (
|
||||
$account->withdrawal_balance_date === $balanceDate
|
||||
&& $account->withdrawal_amount !== null
|
||||
&& is_numeric($account->withdrawal_amount)
|
||||
? (float) $account->withdrawal_amount
|
||||
: null
|
||||
);
|
||||
$balance = ($balances[(int) $account->id]['status'] ?? '') === 'matched'
|
||||
? (float) $balances[(int) $account->id]['balance']
|
||||
: null;
|
||||
$reserve = WithdrawalReserveStandard::amountFor(
|
||||
$account->store_platform,
|
||||
$account->erp_store_name,
|
||||
);
|
||||
$preview[] = [
|
||||
'platform' => $platform,
|
||||
'erp_store_name' => $erpStoreName,
|
||||
'store_id' => $storeId,
|
||||
'changes' => implode('、', $changedLabels),
|
||||
'withdrawal_amount' => $amount,
|
||||
'post_withdrawal_balance' => $balance !== null && $amount !== null
|
||||
? round($balance - $amount, 2)
|
||||
: null,
|
||||
'reserve_standard' => $reserve,
|
||||
'compliance' => WithdrawalManagementReport::complianceFor(
|
||||
[
|
||||
'status' => $balance === null ? 'not_ready' : 'matched',
|
||||
'balance' => $balance,
|
||||
],
|
||||
$reserve,
|
||||
$amount,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -356,25 +510,47 @@ class WithdrawalManagementSpreadsheet
|
||||
return $spreadsheet;
|
||||
}
|
||||
|
||||
/** @return array{0:int,1:list<string>} */
|
||||
/** @return array{0:int,1:list<string>,2:'config'|'withdrawal'} */
|
||||
private function headers(array $matrix): array
|
||||
{
|
||||
foreach (array_slice($matrix, 0, 10, true) as $index => $row) {
|
||||
$headers = array_map(static fn (mixed $value): string => trim((string) $value), $row);
|
||||
if (in_array('店铺平台', $headers, true) && in_array('ERP店铺名称', $headers, true)) {
|
||||
if (! in_array('店铺平台', $headers, true) || ! in_array('提现金额', $headers, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->requireUniqueHeader($headers, '店铺平台');
|
||||
$this->requireUniqueHeader($headers, '提现金额');
|
||||
$fullConfigurationTemplate = collect(self::CONFIG_HEADERS)->every(
|
||||
static fn (string $header): bool => in_array($header, $headers, true),
|
||||
);
|
||||
|
||||
if ($fullConfigurationTemplate) {
|
||||
foreach (self::CONFIG_HEADERS as $required) {
|
||||
if (count(array_keys($headers, $required, true)) !== 1) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "导入表头必须且只能包含一个“{$required}”列,请下载最新模板。",
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (count(array_keys($headers, '提现金额', true)) > 1) {
|
||||
throw ValidationException::withMessages(['import' => '导入表头只能包含一个“提现金额”列。']);
|
||||
$this->requireUniqueHeader($headers, $required);
|
||||
}
|
||||
|
||||
return [(int) $index, $headers];
|
||||
return [(int) $index, $headers, 'config'];
|
||||
}
|
||||
|
||||
if (! in_array('ERP店铺名称', $headers, true) && ! in_array('平台店铺ID', $headers, true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '提现金额模板必须包含“ERP店铺名称”或“平台店铺ID”用于定位店铺,请下载最新模板。',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ([
|
||||
'ERP店铺名称', '平台店铺ID', '余额日期', '店铺余额', '预留标准',
|
||||
'提现后余额', '是否达标',
|
||||
] as $optional) {
|
||||
if (count(array_keys($headers, $optional, true)) > 1) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "导入表头只能包含一个“{$optional}”列。",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return [(int) $index, $headers, 'withdrawal'];
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
@ -382,6 +558,16 @@ class WithdrawalManagementSpreadsheet
|
||||
]);
|
||||
}
|
||||
|
||||
/** @param list<string> $headers */
|
||||
private function requireUniqueHeader(array $headers, string $header): void
|
||||
{
|
||||
if (count(array_keys($headers, $header, true)) !== 1) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => "导入表头必须且只能包含一个“{$header}”列,请下载最新模板。",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{0:array<string,list<StoreReceivingAccount>>,1:array<string,list<StoreReceivingAccount>>} */
|
||||
private function indexes($accounts): array
|
||||
{
|
||||
|
||||
@ -34,6 +34,11 @@ class WithdrawalManagement extends Controller
|
||||
return $this->spreadsheet->template();
|
||||
}
|
||||
|
||||
public function withdrawalTemplate(Request $request): StreamedResponse
|
||||
{
|
||||
return $this->spreadsheet->withdrawalTemplate($this->report->results($request));
|
||||
}
|
||||
|
||||
public function importPreview(Request $request): RedirectResponse
|
||||
{
|
||||
$file = $request->validate([
|
||||
@ -93,6 +98,7 @@ class WithdrawalManagement extends Controller
|
||||
'storeEntity' => 'store_entity',
|
||||
'keyword' => 'keyword',
|
||||
'balanceStatus' => 'balance_status',
|
||||
'balanceDate' => 'balance_date',
|
||||
];
|
||||
|
||||
foreach ($queryNames as $filterName => $queryName) {
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
['key' => 'balance_alert', 'label' => '余额预警', 'numeric' => false, 'title' => '连续3个自然日余额一致且高于预留标准'],
|
||||
['key' => 'reserve_standard', 'label' => '预留标准', 'numeric' => true, 'title' => null],
|
||||
['key' => 'withdrawal_amount', 'label' => '提现金额', 'numeric' => true, 'title' => null],
|
||||
['key' => 'compliance', 'label' => '是否达标', 'numeric' => false, 'title' => null],
|
||||
['key' => 'compliance', 'label' => '是否达标', 'numeric' => false, 'title' => '店铺余额 - 提现金额 ≤ 预留标准时判定为“是”,即按大笔提现处理'],
|
||||
];
|
||||
$withdrawalAlertCount = count($withdrawalAlerts);
|
||||
@endphp
|
||||
@ -321,7 +321,7 @@
|
||||
>
|
||||
<div class="biz-modal-card">
|
||||
<div class="withdrawal-import__head">
|
||||
<div class="biz-modal-title" id="withdrawal-import-title">导入提现配置</div>
|
||||
<div class="biz-modal-title" id="withdrawal-import-title">导入提现金额或配置</div>
|
||||
<button type="button" class="withdrawal-import__close" title="关闭" aria-label="关闭" data-withdrawal-import-close>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
@ -337,7 +337,18 @@
|
||||
@if ($withdrawalImportPreview['preview'] !== [])
|
||||
<div class="withdrawal-import__preview">
|
||||
<table class="biz-table">
|
||||
<thead><tr><th>店铺平台</th><th>ERP店铺名称</th><th>平台店铺ID</th><th>变更字段</th></tr></thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>店铺平台</th>
|
||||
<th>ERP店铺名称</th>
|
||||
<th>平台店铺ID</th>
|
||||
<th>变更字段</th>
|
||||
<th class="num">提现金额</th>
|
||||
<th class="num">提现后余额</th>
|
||||
<th class="num">预留标准</th>
|
||||
<th>是否达标(大笔)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($withdrawalImportPreview['preview'] as $previewRow)
|
||||
<tr>
|
||||
@ -345,6 +356,18 @@
|
||||
<td>{{ $previewRow['erp_store_name'] }}</td>
|
||||
<td>{{ $previewRow['store_id'] ?: '-' }}</td>
|
||||
<td>{{ $previewRow['changes'] }}</td>
|
||||
<td class="num">{{ isset($previewRow['withdrawal_amount']) ? number_format($previewRow['withdrawal_amount'], 2) : '-' }}</td>
|
||||
<td class="num">{{ isset($previewRow['post_withdrawal_balance']) ? number_format($previewRow['post_withdrawal_balance'], 2) : '-' }}</td>
|
||||
<td class="num">{{ isset($previewRow['reserve_standard']) ? number_format($previewRow['reserve_standard'], 2) : '待配置' }}</td>
|
||||
<td>
|
||||
@if (($previewRow['compliance'] ?? null) === 'yes')
|
||||
<span class="biz-badge biz-badge-yes">是(大笔)</span>
|
||||
@elseif (($previewRow['compliance'] ?? null) === 'no')
|
||||
<span class="biz-badge biz-badge-no">否</span>
|
||||
@else
|
||||
<span class="biz-badge biz-badge-no">待核验</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
@ -377,10 +400,21 @@
|
||||
<label class="biz-field-label" for="withdrawal-import-file">Excel 文件</label>
|
||||
<input id="withdrawal-import-file" class="biz-field-input" type="file" name="import" accept=".xlsx,.xls" required>
|
||||
<div class="withdrawal-import__template">
|
||||
<a class="biz-btn biz-btn-ghost" href="{{ route('business.withdrawal-management.template') }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>下载导入模板</span>
|
||||
</a>
|
||||
<div class="withdrawal-import__template-copy">
|
||||
<strong>提现金额专用模板</strong>
|
||||
<span>按当前筛选预填店铺、余额日期、店铺余额和预留标准,只填写“提现金额”即可。</span>
|
||||
<span>系统按“店铺余额 - 提现金额 ≤ 预留标准”计算是否达标;结果为“是”时按大笔提现处理。</span>
|
||||
</div>
|
||||
<div class="withdrawal-import__template-actions">
|
||||
<a class="biz-btn biz-btn-primary" href="{{ route('business.withdrawal-management.template.withdrawals', $withdrawalFilterQuery) }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>下载提现金额模板</span>
|
||||
</a>
|
||||
<a class="biz-btn biz-btn-ghost" href="{{ route('business.withdrawal-management.template') }}">
|
||||
<span class="material-icons-outlined" aria-hidden="true">settings</span>
|
||||
<span>下载完整配置模板</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="biz-modal-foot">
|
||||
<button type="button" class="biz-btn biz-btn-ghost" data-withdrawal-import-close>取消</button>
|
||||
@ -443,9 +477,12 @@
|
||||
.withdrawal-import__stats span { color:#6b665e; font-size:.8125rem; }
|
||||
.withdrawal-import__stats strong { color:#171717; font-variant-numeric:tabular-nums; }
|
||||
.withdrawal-import__preview { max-height:360px; overflow:auto; border:1px solid #e5e0d7; }
|
||||
.withdrawal-import__preview .biz-table { min-width:700px; }
|
||||
.withdrawal-import__preview .biz-table { min-width:980px; }
|
||||
.withdrawal-import__status { margin:.75rem 0 0; color:#6b665e; font-size:.8125rem; }
|
||||
.withdrawal-import__template { margin-top:1rem; }
|
||||
.withdrawal-import__template { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:1rem; align-items:center; margin-top:1rem; padding:.85rem 1rem; border:1px solid #d8d2c7; border-radius:8px; background:#f8f6f1; }
|
||||
.withdrawal-import__template-copy { display:flex; min-width:0; flex-direction:column; gap:.25rem; color:#595959; font-size:.75rem; line-height:1.55; }
|
||||
.withdrawal-import__template-copy strong { color:#171717; font-size:.8125rem; }
|
||||
.withdrawal-import__template-actions { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:.5rem; }
|
||||
.withdrawal-import__empty, .withdrawal-import__error { padding:.85rem 1rem; border:1px solid #d8d2c7; color:#595959; }
|
||||
.withdrawal-import__error { margin-bottom:1rem; border-color:#d6a4a4; background:#fff7f7; color:#8a1f1f; }
|
||||
@media (max-width: 640px) {
|
||||
@ -456,6 +493,9 @@
|
||||
.withdrawal-import-modal { padding:1rem; }
|
||||
.withdrawal-import__stats { grid-template-columns:1fr; }
|
||||
.withdrawal-import__stats > div + div { border-left:0; border-top:1px solid #d8d2c7; }
|
||||
.withdrawal-import__template { grid-template-columns:1fr; }
|
||||
.withdrawal-import__template-actions { justify-content:stretch; }
|
||||
.withdrawal-import__template-actions .biz-btn { flex:1 1 100%; justify-content:center; }
|
||||
.withdrawal-import-modal .biz-modal-foot { align-items:stretch; flex-direction:column-reverse; }
|
||||
.withdrawal-import-modal .biz-btn { justify-content:center; }
|
||||
}
|
||||
|
||||
@ -625,6 +625,7 @@ Route::prefix('workspace/fund-management/withdrawal-management')
|
||||
->group(function () {
|
||||
Route::get('export', [WithdrawalManagement::class, 'export'])->name('export');
|
||||
Route::get('template', [WithdrawalManagement::class, 'template'])->name('template');
|
||||
Route::get('template/withdrawals', [WithdrawalManagement::class, 'withdrawalTemplate'])->name('template.withdrawals');
|
||||
Route::post('import/preview', [WithdrawalManagement::class, 'importPreview'])
|
||||
->middleware('import')
|
||||
->name('import.preview');
|
||||
|
||||
@ -31,6 +31,18 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase
|
||||
|
||||
private const IMPORT_HEADERS = [...self::HEADERS, '提现金额'];
|
||||
|
||||
private const WITHDRAWAL_TEMPLATE_HEADERS = [
|
||||
'店铺平台',
|
||||
'ERP店铺名称',
|
||||
'平台店铺ID',
|
||||
'余额日期',
|
||||
'店铺余额',
|
||||
'预留标准',
|
||||
'提现金额',
|
||||
'提现后余额',
|
||||
'是否达标',
|
||||
];
|
||||
|
||||
/** @var list<string> */
|
||||
private array $temporaryFiles = [];
|
||||
|
||||
@ -68,7 +80,10 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase
|
||||
->assertSee('data-withdrawal-sort-asc', false)
|
||||
->assertSee('data-withdrawal-sort-desc', false)
|
||||
->assertSee(route('business.withdrawal-management.export', ['platform' => '天猫']), false)
|
||||
->assertSee(route('business.withdrawal-management.template'), false);
|
||||
->assertSee(route('business.withdrawal-management.template'), false)
|
||||
->assertSee(route('business.withdrawal-management.template.withdrawals', ['platform' => '天猫']), false)
|
||||
->assertSeeText('下载提现金额模板')
|
||||
->assertSeeText('按大笔提现处理');
|
||||
}
|
||||
|
||||
public function test_page_sorts_all_filtered_rows_before_pagination(): void
|
||||
@ -180,6 +195,142 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_withdrawal_amount_template_prefills_current_filtered_store_and_balance(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-24 09:00:00');
|
||||
|
||||
try {
|
||||
StoreReceivingAccount::query()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'store_platform' => '天猫',
|
||||
'erp_store_name' => '金额模板目标店',
|
||||
'store_id' => 'TM-AMOUNT-TEMPLATE',
|
||||
'store_entity_name' => '模板测试主体',
|
||||
]);
|
||||
$this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:00:00', '金额模板目标店', 25000);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.withdrawal-management.template.withdrawals', [
|
||||
'platform' => '天猫',
|
||||
'keyword' => '金额模板目标店',
|
||||
]));
|
||||
$response->assertOk()->assertDownload();
|
||||
$path = $this->temporaryPath('withdrawal-amount-template-');
|
||||
file_put_contents($path, $response->streamedContent());
|
||||
|
||||
$spreadsheet = IOFactory::load($path);
|
||||
try {
|
||||
$sheet = $spreadsheet->getSheet(0);
|
||||
$this->assertSame('提现金额', $sheet->getTitle());
|
||||
$this->assertSame(self::WITHDRAWAL_TEMPLATE_HEADERS, $sheet->rangeToArray('A1:I1')[0]);
|
||||
$this->assertSame('天猫', $sheet->getCell('A2')->getValue());
|
||||
$this->assertSame('金额模板目标店', $sheet->getCell('B2')->getValue());
|
||||
$this->assertSame('TM-AMOUNT-TEMPLATE', $sheet->getCell('C2')->getValue());
|
||||
$this->assertSame('2026-09-23', $sheet->getCell('D2')->getValue());
|
||||
$this->assertSame(25000.0, (float) $sheet->getCell('E2')->getValue());
|
||||
$this->assertSame(20000.0, (float) $sheet->getCell('F2')->getValue());
|
||||
$this->assertNull($sheet->getCell('G2')->getValue());
|
||||
$this->assertStringContainsString('E2-G2', (string) $sheet->getCell('H2')->getValue());
|
||||
$this->assertStringContainsString('H2<=F2', (string) $sheet->getCell('I2')->getValue());
|
||||
$this->assertSame(2, $sheet->getHighestDataRow());
|
||||
$this->assertStringContainsString('大笔提现', (string) $spreadsheet->getSheetByName('填写说明')?->getCell('A5')->getValue());
|
||||
} finally {
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
} finally {
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_withdrawal_amount_template_imports_only_filled_amounts_and_previews_compliance(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-24 09:00:00');
|
||||
|
||||
try {
|
||||
$qualified = StoreReceivingAccount::query()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'store_platform' => '天猫',
|
||||
'erp_store_name' => '大笔达标店',
|
||||
'store_id' => 'TM-LARGE-YES',
|
||||
'store_entity_name' => '模板测试主体',
|
||||
]);
|
||||
$unqualified = StoreReceivingAccount::query()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'store_platform' => '天猫',
|
||||
'erp_store_name' => '大笔未达标店',
|
||||
'store_id' => 'TM-LARGE-NO',
|
||||
'store_entity_name' => '模板测试主体',
|
||||
]);
|
||||
$this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:00:00', '大笔达标店', 25000);
|
||||
$this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:01:00', '大笔未达标店', 25000);
|
||||
|
||||
$this->loginAs();
|
||||
$previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [
|
||||
'import' => $this->withdrawalWorkbook([
|
||||
['天猫', '大笔达标店', 'TM-LARGE-YES', '2026-09-23', 25000, 20000, '5000.00'],
|
||||
['天猫', '大笔未达标店', 'TM-LARGE-NO', '2026-09-23', 25000, 20000, '4999.99'],
|
||||
]),
|
||||
]);
|
||||
$previewResponse->assertSessionHas('withdrawalImportPreview', function (array $preview): bool {
|
||||
return $preview['total'] === 2
|
||||
&& $preview['changed'] === 2
|
||||
&& $preview['preview'][0]['withdrawal_amount'] === 5000.0
|
||||
&& $preview['preview'][0]['post_withdrawal_balance'] === 20000.0
|
||||
&& $preview['preview'][0]['compliance'] === 'yes'
|
||||
&& $preview['preview'][1]['withdrawal_amount'] === 4999.99
|
||||
&& $preview['preview'][1]['post_withdrawal_balance'] === 20000.01
|
||||
&& $preview['preview'][1]['compliance'] === 'no';
|
||||
});
|
||||
$preview = $previewResponse->getSession()->get('withdrawalImportPreview');
|
||||
$this->get(route('business.pages.show', [
|
||||
'section' => 'fund-management',
|
||||
'page' => 'withdrawal-management',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSeeText('提现后余额')
|
||||
->assertSeeText('是否达标(大笔)')
|
||||
->assertSeeText('是(大笔)');
|
||||
|
||||
$this->post(route('business.withdrawal-management.import.commit'), [
|
||||
'preview_token' => $preview['token'],
|
||||
])->assertSessionHasNoErrors();
|
||||
|
||||
$this->assertSame('5000.00', $qualified->fresh()->withdrawal_amount);
|
||||
$this->assertSame('2026-09-23', $qualified->fresh()->withdrawal_balance_date);
|
||||
$this->assertSame('4999.99', $unqualified->fresh()->withdrawal_amount);
|
||||
$this->assertSame('2026-09-23', $unqualified->fresh()->withdrawal_balance_date);
|
||||
} finally {
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_withdrawal_amount_template_rejects_a_stale_balance_date(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-24 09:00:00');
|
||||
|
||||
try {
|
||||
$store = StoreReceivingAccount::query()->create([
|
||||
'company_id' => $this->company->id,
|
||||
'store_platform' => '京东',
|
||||
'erp_store_name' => '过期金额模板店',
|
||||
'store_id' => 'JD-STALE-AMOUNT',
|
||||
'store_entity_name' => '模板测试主体',
|
||||
]);
|
||||
|
||||
$this->loginAs();
|
||||
$this->withExceptionHandling()
|
||||
->post(route('business.withdrawal-management.import.preview'), [
|
||||
'import' => $this->withdrawalWorkbook([
|
||||
['京东', '过期金额模板店', 'JD-STALE-AMOUNT', '2026-09-22', 10000, 5000, '5000.00'],
|
||||
]),
|
||||
])
|
||||
->assertSessionHasErrors('import');
|
||||
|
||||
$this->assertNull($store->fresh()->withdrawal_amount);
|
||||
} finally {
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_valid_import_is_previewed_before_allowed_fields_are_committed(): void
|
||||
{
|
||||
$store = StoreReceivingAccount::query()->create([
|
||||
@ -554,6 +705,29 @@ class WithdrawalManagementImportExportTest extends FeatureTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/** @param list<list<mixed>> $rows */
|
||||
private function withdrawalWorkbook(array $rows): UploadedFile
|
||||
{
|
||||
$path = $this->temporaryPath('withdrawal-amount-import-');
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$sheet->setTitle('提现金额');
|
||||
$sheet->fromArray(self::WITHDRAWAL_TEMPLATE_HEADERS, null, 'A1');
|
||||
if ($rows !== []) {
|
||||
$sheet->fromArray($rows, null, 'A2');
|
||||
}
|
||||
(new Xlsx($spreadsheet))->save($path);
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
|
||||
return new UploadedFile(
|
||||
$path,
|
||||
'提现金额.xlsx',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
null,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
private function temporaryPath(string $prefix): string
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), $prefix);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user