财务台账:补齐账户后4位并统一支付宝占位 #442
@ -386,11 +386,10 @@ class FinanceLedger extends Controller
|
||||
abort_if($accountShort === '', 422, '账户简称不能为空。');
|
||||
|
||||
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
||||
abort_unless(
|
||||
$connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
|
||||
422,
|
||||
'账户简称不存在,请刷新账户总表后重试。',
|
||||
);
|
||||
$account = $connection->table('fin_accounts')
|
||||
->where('account_short', $accountShort)
|
||||
->first(['account_no', 'account_last4']);
|
||||
abort_if($account === null, 422, '账户简称不存在,请刷新账户总表后重试。');
|
||||
|
||||
if ($accountType !== '') {
|
||||
$allowedTypes = FinanceLedgerAccountType::options(
|
||||
@ -412,21 +411,36 @@ class FinanceLedger extends Controller
|
||||
'updated_by' => (string) user()->name,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
$accountLast4 = FinanceLedgerAccountType::last4(
|
||||
$accountType,
|
||||
(string) ($account->account_no ?? ''),
|
||||
(string) ($account->account_last4 ?? ''),
|
||||
);
|
||||
|
||||
if ($existing === null) {
|
||||
$overrides->insert($values + [
|
||||
'account_short' => $accountShort,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$overrides->where('account_short', $accountShort)->update($values);
|
||||
}
|
||||
$connection->transaction(function () use ($connection, $overrides, $existing, $values, $accountShort, $now, $accountLast4): void {
|
||||
if ($existing === null) {
|
||||
$overrides->insert($values + [
|
||||
'account_short' => $accountShort,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
} else {
|
||||
$overrides->where('account_short', $accountShort)->update($values);
|
||||
}
|
||||
|
||||
$connection->table('fin_accounts')
|
||||
->where('account_short', $accountShort)
|
||||
->update([
|
||||
'account_last4' => $accountLast4,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
});
|
||||
|
||||
return response()->json([
|
||||
'ok' => true,
|
||||
'account_short' => $accountShort,
|
||||
'account_type' => $accountType,
|
||||
'label' => $accountType !== '' ? $accountType : '未填写',
|
||||
'account_last4' => $accountLast4,
|
||||
], 200, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
|
||||
@ -4,10 +4,14 @@ namespace App\Utilities;
|
||||
|
||||
final class FinanceLedgerAccountType
|
||||
{
|
||||
public const ALIPAY = '支付宝';
|
||||
|
||||
public const BANK_ACCOUNT_TYPES = ['公户', '个人卡', '网商银行'];
|
||||
|
||||
public const DEFAULT_OPTIONS = [
|
||||
'个人卡',
|
||||
'公户',
|
||||
'支付宝',
|
||||
self::ALIPAY,
|
||||
'网商银行',
|
||||
];
|
||||
|
||||
@ -28,4 +32,23 @@ final class FinanceLedgerAccountType
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
public static function last4(?string $accountType, ?string $accountNo, ?string $fallback = null): string
|
||||
{
|
||||
$type = trim((string) $accountType);
|
||||
if ($type === self::ALIPAY) {
|
||||
return '/';
|
||||
}
|
||||
|
||||
if (in_array($type, self::BANK_ACCOUNT_TYPES, true)) {
|
||||
$digits = preg_replace('/\D+/', '', (string) $accountNo);
|
||||
if (strlen((string) $digits) >= 4) {
|
||||
return substr((string) $digits, -4);
|
||||
}
|
||||
}
|
||||
|
||||
$fallback = trim((string) $fallback);
|
||||
|
||||
return $fallback !== '' ? $fallback : '/';
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use App\Utilities\FinanceLedgerAccountShouldKeep;
|
||||
use App\Utilities\FinanceLedgerAccountType;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
@ -12,8 +13,8 @@ use RuntimeException;
|
||||
/**
|
||||
* 导入财务台账的基础数据:账户、店铺、公司和期初余额。
|
||||
*
|
||||
* 源文件是财务维护的《银行当前余额-*.xlsx》。这里只做结构校验和全量
|
||||
* 覆盖,不推断业务口径;导入完成后由 FinanceLedgerCalculator 重算流水。
|
||||
* 源文件是财务维护的《银行当前余额-*.xlsx》。这里只做结构校验、后4位
|
||||
* 规范化和全量覆盖,不推断其他业务口径;导入完成后由计算器重算流水。
|
||||
*/
|
||||
class FinanceLedgerBaseImporter
|
||||
{
|
||||
@ -121,19 +122,25 @@ class FinanceLedgerBaseImporter
|
||||
}
|
||||
|
||||
$keepIndex = $headerIndexes['账户是否保留'] ?? null;
|
||||
$accountNo = $this->text($value('账号', 2));
|
||||
$accountType = $this->text($value('账户类型', 5));
|
||||
$rows[] = [
|
||||
'company_name' => $company,
|
||||
'account_short' => $short,
|
||||
'region' => null,
|
||||
'account_no' => $this->text($value('账号', 2)),
|
||||
'account_no' => $accountNo,
|
||||
'bank_name' => $this->text($value('开户行', 3)),
|
||||
'bank_city_branch' => $this->text($value('开户城市/网点', 4)),
|
||||
'account_type' => $this->text($value('账户类型', 5)),
|
||||
'account_type' => $accountType,
|
||||
'company_plan' => $this->text($value('公司规划', 6)),
|
||||
'account_should_keep' => FinanceLedgerAccountShouldKeep::normalize(
|
||||
$keepIndex === null ? null : $this->text($row[$keepIndex] ?? null),
|
||||
),
|
||||
'account_last4' => $this->text($value('后4位', 7)),
|
||||
'account_last4' => FinanceLedgerAccountType::last4(
|
||||
$accountType,
|
||||
$accountNo,
|
||||
$this->text($value('后4位', 7)),
|
||||
),
|
||||
'single_limit' => $this->text($value('单笔限额', 8)),
|
||||
'daily_limit' => $this->text($value('日限额', 9)),
|
||||
'monthly_limit' => $this->text($value('月限额', 10)),
|
||||
|
||||
@ -323,7 +323,7 @@
|
||||
</select>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ $row->account_last4 }}</td><td class="center" title="{{ $moneyYuanTitle($row->single_limit) }}">{{ $moneyWan($row->single_limit) }}</td><td class="center" title="{{ $moneyYuanTitle($row->daily_limit) }}">{{ $moneyWan($row->daily_limit) }}</td><td class="center" title="{{ $moneyYuanTitle($row->monthly_limit) }}">{{ $moneyWan($row->monthly_limit) }}</td><td class="center" title="{{ $moneyYuanTitle($row->yearly_limit) }}">{{ $moneyWan($row->yearly_limit) }}</td><td class="num" data-account-current-balance="{{ (float) $row->current_balance }}" title="{{ $moneyYuanTitle($row->current_balance) }}">{{ $moneyWan($row->current_balance) }}</td><td class="num" data-account-available-amount title="{{ $moneyYuanTitle($row->daily_available) }}">{{ $moneyWan($row->daily_available) }}</td><td class="num" data-account-blocked-amount title="{{ $moneyYuanTitle($row->blocked_amount) }}">{{ $moneyWan($row->blocked_amount) }}</td><td class="num" data-account-daily-available-amount title="{{ $moneyYuanTitle($row->daily_available) }}">{{ $moneyWan($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td title="{{ $moneyYuanTitle($row->raise_target) }}">{{ $moneyWan($row->raise_target) }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
|
||||
<td class="center" data-account-last4>{{ $row->account_last4 }}</td><td class="center" title="{{ $moneyYuanTitle($row->single_limit) }}">{{ $moneyWan($row->single_limit) }}</td><td class="center" title="{{ $moneyYuanTitle($row->daily_limit) }}">{{ $moneyWan($row->daily_limit) }}</td><td class="center" title="{{ $moneyYuanTitle($row->monthly_limit) }}">{{ $moneyWan($row->monthly_limit) }}</td><td class="center" title="{{ $moneyYuanTitle($row->yearly_limit) }}">{{ $moneyWan($row->yearly_limit) }}</td><td class="num" data-account-current-balance="{{ (float) $row->current_balance }}" title="{{ $moneyYuanTitle($row->current_balance) }}">{{ $moneyWan($row->current_balance) }}</td><td class="num" data-account-available-amount title="{{ $moneyYuanTitle($row->daily_available) }}">{{ $moneyWan($row->daily_available) }}</td><td class="num" data-account-blocked-amount title="{{ $moneyYuanTitle($row->blocked_amount) }}">{{ $moneyWan($row->blocked_amount) }}</td><td class="num" data-account-daily-available-amount title="{{ $moneyYuanTitle($row->daily_available) }}">{{ $moneyWan($row->daily_available) }}</td><td class="num">{{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }}</td><td title="{{ $moneyYuanTitle($row->raise_target) }}">{{ $moneyWan($row->raise_target) }}</td><td>{{ $row->raise_status }}</td><td>{{ $row->raise_actions }}</td><td>{{ $row->done_actions }}</td><td>{{ $row->next_action }}</td><td>{{ $row->remark }}</td><td>{{ $row->owner }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="27"><div class="ledger-empty">没有匹配的账户数据。</div></td></tr>
|
||||
@ -1001,6 +1001,10 @@
|
||||
item.closest('td')?.setAttribute('data-account-should-keep', value);
|
||||
}
|
||||
});
|
||||
if (field === 'account_type') {
|
||||
const last4Cell = select.closest('tr')?.querySelector('[data-account-last4]');
|
||||
if (last4Cell) last4Cell.textContent = payload.account_last4 || '/';
|
||||
}
|
||||
if (field === 'status') {
|
||||
const row = select.closest('tr');
|
||||
const currentCell = row?.querySelector('[data-account-current-balance]');
|
||||
|
||||
@ -610,6 +610,42 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
|
||||
])->assertUnprocessable()->assertJsonPath('message', '请选择现有账户类型。');
|
||||
}
|
||||
|
||||
public function test_account_type_change_derives_last4_and_alipay_uses_slash(): void
|
||||
{
|
||||
DB::connection('business_mirror')->table('fin_accounts')
|
||||
->where('account_short', '测试账户')
|
||||
->update([
|
||||
'account_no' => '6222-1234 5678',
|
||||
'account_last4' => '0000',
|
||||
]);
|
||||
|
||||
$this->loginAs()->postJson($this->typeUrl(), [
|
||||
'account_short' => '测试账户',
|
||||
'account_type' => '网商银行',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('account_last4', '5678');
|
||||
$this->assertDatabaseHas('fin_accounts', [
|
||||
'account_short' => '测试账户',
|
||||
'account_last4' => '5678',
|
||||
], 'business_mirror');
|
||||
|
||||
$this->loginAs()->postJson($this->typeUrl(), [
|
||||
'account_short' => '测试账户',
|
||||
'account_type' => '支付宝',
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('account_last4', '/');
|
||||
$this->assertDatabaseHas('fin_accounts', [
|
||||
'account_short' => '测试账户',
|
||||
'account_last4' => '/',
|
||||
], 'business_mirror');
|
||||
|
||||
$this->loginAs()->get($this->pageUrl())
|
||||
->assertOk()
|
||||
->assertSee('<td class="center" data-account-last4>/</td>', false);
|
||||
}
|
||||
|
||||
public function test_account_status_can_be_saved_and_updated_without_duplicate_rows(): void
|
||||
{
|
||||
$payload = [
|
||||
|
||||
27
tests/Unit/FinanceLedgerAccountTypeTest.php
Normal file
27
tests/Unit/FinanceLedgerAccountTypeTest.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Utilities\FinanceLedgerAccountType;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class FinanceLedgerAccountTypeTest extends TestCase
|
||||
{
|
||||
public function test_bank_account_types_use_the_last_four_digits_of_the_account_number(): void
|
||||
{
|
||||
foreach (FinanceLedgerAccountType::BANK_ACCOUNT_TYPES as $type) {
|
||||
$this->assertSame('5678', FinanceLedgerAccountType::last4($type, '6222-1234 5678', '0000'));
|
||||
}
|
||||
}
|
||||
|
||||
public function test_alipay_always_uses_a_slash(): void
|
||||
{
|
||||
$this->assertSame('/', FinanceLedgerAccountType::last4('支付宝', 'alipay@example.com', '1234'));
|
||||
}
|
||||
|
||||
public function test_other_types_keep_the_fallback_when_no_bank_last_four_can_be_derived(): void
|
||||
{
|
||||
$this->assertSame('1234', FinanceLedgerAccountType::last4('其他', 'alipay@example.com', '1234'));
|
||||
$this->assertSame('/', FinanceLedgerAccountType::last4('公户', '123', ''));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user