ThinkAdmin/plugin/think-plugs-payment/tests/PaymentLedgerControllerTest.php
Anyon e7a8c05556 chore(repo): 统一 v8 仓库品牌名称
将 v8 重构分支中残留的 ThinkAdminDeveloper 文本统一调整为 ThinkAdmin,避免迁移到主仓库后继续暴露旧开发仓库名称。

主要内容:

- 更新 README 标题与项目描述。

- 统一 PHP 文件头注释中的项目标识。

- 同步调整测试、配置、插件与文档中的旧仓库名称文本。

- 保持旧包删除说明与架构边界测试语义不变,只清理品牌名称残留。
2026-05-08 16:15:24 +08:00

581 lines
25 KiB
PHP

<?php
declare(strict_types=1);
/**
* +----------------------------------------------------------------------
* | ThinkAdmin Plugin
* +----------------------------------------------------------------------
* | Copyright (c) 2014~2026 ThinkAdmin [ thinkadmin.top ]
* +----------------------------------------------------------------------
* | Official Website: https://thinkadmin.top
* +----------------------------------------------------------------------
* | Licensed: https://mit-license.org
* | Disclaimer: https://thinkadmin.top/disclaimer
* | Vip Rights: https://thinkadmin.top/vip-introduce
* +----------------------------------------------------------------------
* | Gitee Repository: https://gitee.com/zoujingli/ThinkAdmin
* | Github Repository: https://github.com/zoujingli/ThinkAdmin
* +----------------------------------------------------------------------
*/
namespace think\admin\tests;
use plugin\payment\controller\Balance as BalanceController;
use plugin\payment\controller\Config as ConfigController;
use plugin\payment\controller\Integral as IntegralController;
use plugin\payment\controller\api\auth\Balance as AuthBalanceController;
use plugin\payment\controller\api\auth\Integral as AuthIntegralController;
use plugin\payment\model\PluginPaymentBalance;
use plugin\payment\model\PluginPaymentIntegral;
use plugin\payment\service\Balance;
use plugin\payment\service\Integral;
use think\admin\runtime\RequestContext;
use think\admin\tests\Support\SqliteIntegrationTestCase;
use think\exception\HttpResponseException;
use think\Request;
/**
* @internal
* @coversNothing
*/
class PaymentLedgerControllerTest extends SqliteIntegrationTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->configureAccountAccess([
'headimg' => 'https://example.com/payment-ledger-controller.png',
'userPrefix' => '台账控制器账号',
]);
}
protected function afterSchemaCreated(): void
{
$this->app->setAppPath(TEST_PROJECT_ROOT . '/plugin/think-plugs-payment/src/');
$this->configureView([
'view_path' => TEST_PROJECT_ROOT . '/plugin/think-plugs-payment/src/view' . DIRECTORY_SEPARATOR,
]);
}
public function testBalanceControllerUnlockCancelAndRemoveChain(): void
{
$user = $this->createAccountUser();
Balance::create(intval($user->getAttr('id')), 'ledger-balance-001', '余额发放', '30.00', '后台台账测试');
$unlock = $this->callController(BalanceController::class, 'unlock', [
'code' => 'ledger-balance-001',
'unlock' => 1,
]);
$cancel = $this->callController(BalanceController::class, 'cancel', [
'code' => 'ledger-balance-001',
'cancel' => 1,
]);
$remove = $this->callController(BalanceController::class, 'remove', [
'code' => 'ledger-balance-001',
]);
$model = PluginPaymentBalance::mk()->withTrashed()->where(['code' => 'ledger-balance-001'])->findOrEmpty();
$user = $user->refresh();
$extra = $user->getAttr('extra');
$this->assertSame(200, intval($unlock['code'] ?? 0));
$this->assertSame(200, intval($cancel['code'] ?? 0));
$this->assertSame(200, intval($remove['code'] ?? 0));
$this->assertSame('交易操作成功!', $remove['info'] ?? '');
$this->assertSame(1, intval($model->getAttr('unlock')));
$this->assertSame(1, intval($model->getAttr('cancel')));
$this->assertNotEmpty($model->getAttr('unlock_time'));
$this->assertNotEmpty($model->getAttr('cancel_time'));
$this->assertNotEmpty($model->getAttr('delete_time'));
$this->assertSame('0.00', $this->decimal($extra['balance_lock'] ?? 0));
$this->assertSame('0.00', $this->decimal($extra['balance_total'] ?? 0));
$this->assertSame('0.00', $this->decimal($extra['balance_usable'] ?? 0));
}
public function testIntegralControllerUnlockCancelAndRemoveChain(): void
{
$user = $this->createAccountUser();
Integral::create(intval($user->getAttr('id')), 'ledger-integral-001', '积分发放', '18.00', '后台台账测试');
$unlock = $this->callController(IntegralController::class, 'unlock', [
'code' => 'ledger-integral-001',
'unlock' => 1,
]);
$cancel = $this->callController(IntegralController::class, 'cancel', [
'code' => 'ledger-integral-001',
'cancel' => 1,
]);
$remove = $this->callController(IntegralController::class, 'remove', [
'code' => 'ledger-integral-001',
]);
$model = PluginPaymentIntegral::mk()->withTrashed()->where(['code' => 'ledger-integral-001'])->findOrEmpty();
$user = $user->refresh();
$extra = $user->getAttr('extra');
$this->assertSame(200, intval($unlock['code'] ?? 0));
$this->assertSame(200, intval($cancel['code'] ?? 0));
$this->assertSame(200, intval($remove['code'] ?? 0));
$this->assertSame('交易操作成功!', $remove['info'] ?? '');
$this->assertSame(1, intval($model->getAttr('unlock')));
$this->assertSame(1, intval($model->getAttr('cancel')));
$this->assertNotEmpty($model->getAttr('unlock_time'));
$this->assertNotEmpty($model->getAttr('cancel_time'));
$this->assertNotEmpty($model->getAttr('delete_time'));
$this->assertSame('0.00', $this->decimal($extra['integral_lock'] ?? 0));
$this->assertSame('0.00', $this->decimal($extra['integral_total'] ?? 0));
$this->assertSame('0.00', $this->decimal($extra['integral_usable'] ?? 0));
}
public function testBalanceIndexControllerFiltersActiveLedgersByUserKeyword(): void
{
$user = $this->createAccountUser([
'username' => 'balance-search-user',
'nickname' => '余额检索用户',
]);
$other = $this->createAccountUser([
'username' => 'balance-other-user',
'nickname' => '余额其他用户',
]);
Balance::create(intval($user->getAttr('id')), 'ledger-balance-active', '有效余额', '30.00', '有效余额记录');
Balance::create(intval($user->getAttr('id')), 'ledger-balance-cancelled', '作废余额', '12.00', '作废余额记录');
Balance::cancel('ledger-balance-cancelled', 1);
Balance::create(intval($user->getAttr('id')), 'ledger-balance-deleted', '删除余额', '8.00', '删除余额记录');
Balance::remove('ledger-balance-deleted');
Balance::create(intval($other->getAttr('id')), 'ledger-balance-other', '其他余额', '6.00', '其他用户余额');
$result = $this->callIndexController(BalanceController::class, [
'output' => 'json',
'user' => 'balance-search-user',
'page' => 1,
'limit' => 20,
]);
$this->assertSame(200, intval($result['code'] ?? 0));
$this->assertSame('JSON-DATA', $result['info'] ?? '');
$this->assertSame(1, intval($result['data']['page']['total'] ?? 0));
$this->assertCount(1, $result['data']['list'] ?? []);
$this->assertSame('ledger-balance-active', $result['data']['list'][0]['code'] ?? '');
$this->assertSame('balance-search-user', $result['data']['list'][0]['user']['username'] ?? '');
}
public function testIntegralIndexControllerShowsCancelledLedgersForHistoryType(): void
{
$user = $this->createAccountUser([
'username' => 'integral-history-user',
'nickname' => '积分检索用户',
]);
$other = $this->createAccountUser([
'username' => 'integral-other-user',
'nickname' => '积分其他用户',
]);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-active', '有效积分', '18.00', '有效积分记录');
Integral::create(intval($user->getAttr('id')), 'ledger-integral-cancelled', '作废积分', '9.00', '作废积分记录');
Integral::cancel('ledger-integral-cancelled', 1);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-deleted', '删除积分', '5.00', '删除积分记录');
Integral::cancel('ledger-integral-deleted', 1);
Integral::remove('ledger-integral-deleted');
Integral::create(intval($other->getAttr('id')), 'ledger-integral-other', '其他积分', '7.00', '其他用户积分');
Integral::cancel('ledger-integral-other', 1);
$result = $this->callIndexController(IntegralController::class, [
'output' => 'json',
'type' => 'history',
'user' => 'integral-history-user',
'page' => 1,
'limit' => 20,
]);
$this->assertSame(200, intval($result['code'] ?? 0));
$this->assertSame('JSON-DATA', $result['info'] ?? '');
$this->assertSame(1, intval($result['data']['page']['total'] ?? 0));
$this->assertCount(1, $result['data']['list'] ?? []);
$this->assertSame('ledger-integral-cancelled', $result['data']['list'][0]['code'] ?? '');
$this->assertSame('integral-history-user', $result['data']['list'][0]['user']['username'] ?? '');
}
public function testBalanceIndexControllerAppliesDateRangeAndDescendingOrder(): void
{
$user = $this->createAccountUser([
'username' => 'balance-range-user',
'nickname' => '余额时间用户',
]);
Balance::create(intval($user->getAttr('id')), 'ledger-balance-older', '旧余额', '10.00', '旧余额记录');
Balance::create(intval($user->getAttr('id')), 'ledger-balance-middle', '中余额', '20.00', '中余额记录');
Balance::create(intval($user->getAttr('id')), 'ledger-balance-newer', '新余额', '30.00', '新余额记录');
PluginPaymentBalance::mk()->where(['code' => 'ledger-balance-older'])->update([
'create_time' => '2026-03-09 08:00:00',
'update_time' => '2026-03-09 08:00:00',
]);
PluginPaymentBalance::mk()->where(['code' => 'ledger-balance-middle'])->update([
'create_time' => '2026-03-10 09:00:00',
'update_time' => '2026-03-10 09:00:00',
]);
PluginPaymentBalance::mk()->where(['code' => 'ledger-balance-newer'])->update([
'create_time' => '2026-03-10 18:30:00',
'update_time' => '2026-03-10 18:30:00',
]);
$result = $this->callIndexController(BalanceController::class, [
'output' => 'json',
'user' => 'balance-range-user',
'create_time' => '2026-03-10 - 2026-03-10',
'_field_' => 'create_time',
'_order_' => 'desc',
'page' => 1,
'limit' => 20,
]);
$this->assertSame(200, intval($result['code'] ?? 0));
$this->assertSame('JSON-DATA', $result['info'] ?? '');
$this->assertSame(2, intval($result['data']['page']['total'] ?? 0));
$this->assertSame([
'ledger-balance-newer',
'ledger-balance-middle',
], array_column($result['data']['list'] ?? [], 'code'));
}
public function testIntegralIndexControllerAppliesAmountSortForHistoryView(): void
{
$user = $this->createAccountUser([
'username' => 'integral-sort-user',
'nickname' => '积分排序用户',
]);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-low', '低积分', '6.00', '低积分记录');
Integral::cancel('ledger-integral-low', 1);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-mid', '中积分', '12.00', '中积分记录');
Integral::cancel('ledger-integral-mid', 1);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-high', '高积分', '18.00', '高积分记录');
Integral::cancel('ledger-integral-high', 1);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-deleted-history', '删积分', '24.00', '删除积分记录');
Integral::cancel('ledger-integral-deleted-history', 1);
Integral::remove('ledger-integral-deleted-history');
$result = $this->callIndexController(IntegralController::class, [
'output' => 'json',
'type' => 'history',
'user' => 'integral-sort-user',
'_field_' => 'amount',
'_order_' => 'desc',
'page' => 1,
'limit' => 20,
]);
$this->assertSame(200, intval($result['code'] ?? 0));
$this->assertSame('JSON-DATA', $result['info'] ?? '');
$this->assertSame(3, intval($result['data']['page']['total'] ?? 0));
$this->assertSame([
'ledger-integral-high',
'ledger-integral-mid',
'ledger-integral-low',
], array_column($result['data']['list'] ?? [], 'code'));
}
public function testBalanceIndexControllerReturnsSecondPageWithConfiguredLimit(): void
{
$user = $this->createAccountUser([
'username' => 'balance-page-user',
'nickname' => '余额分页用户',
]);
for ($i = 1; $i <= 11; ++$i) {
$code = sprintf('ledger-balance-page-%02d', $i);
Balance::create(intval($user->getAttr('id')), $code, "分页余额{$i}", number_format((float)$i, 2, '.', ''), '余额分页测试');
}
$result = $this->callIndexController(BalanceController::class, [
'output' => 'json',
'user' => 'balance-page-user',
'_field_' => 'amount',
'_order_' => 'asc',
'page' => 2,
'limit' => 10,
]);
$this->assertSame(200, intval($result['code'] ?? 0));
$this->assertSame('JSON-DATA', $result['info'] ?? '');
$this->assertSame(11, intval($result['data']['page']['total'] ?? 0));
$this->assertSame(2, intval($result['data']['page']['pages'] ?? 0));
$this->assertSame(2, intval($result['data']['page']['current'] ?? 0));
$this->assertSame(10, intval($result['data']['page']['limit'] ?? 0));
$this->assertCount(1, $result['data']['list'] ?? []);
$this->assertSame('ledger-balance-page-11', $result['data']['list'][0]['code'] ?? '');
$this->assertSame('11.00', $this->decimal($result['data']['list'][0]['amount'] ?? 0));
}
public function testIntegralIndexControllerFallsBackToDefaultLimitWhenRequestedLimitIsInvalid(): void
{
$user = $this->createAccountUser([
'username' => 'integral-page-user',
'nickname' => '积分分页用户',
]);
for ($i = 1; $i <= 21; ++$i) {
$code = sprintf('ledger-integral-page-%02d', $i);
Integral::create(intval($user->getAttr('id')), $code, "分页积分{$i}", number_format((float)$i, 2, '.', ''), '积分分页测试');
Integral::cancel($code, 1);
}
$result = $this->callIndexController(IntegralController::class, [
'output' => 'json',
'type' => 'history',
'user' => 'integral-page-user',
'_field_' => 'amount',
'_order_' => 'asc',
'page' => 2,
'limit' => 999,
]);
$this->assertSame(200, intval($result['code'] ?? 0));
$this->assertSame('JSON-DATA', $result['info'] ?? '');
$this->assertSame(21, intval($result['data']['page']['total'] ?? 0));
$this->assertSame(2, intval($result['data']['page']['pages'] ?? 0));
$this->assertSame(2, intval($result['data']['page']['current'] ?? 0));
$this->assertSame(20, intval($result['data']['page']['limit'] ?? 0));
$this->assertCount(1, $result['data']['list'] ?? []);
$this->assertSame('ledger-integral-page-21', $result['data']['list'][0]['code'] ?? '');
$this->assertSame('21.00', $this->decimal($result['data']['list'][0]['amount'] ?? 0));
}
public function testConfigIndexRendersEnglishTextsWhenLangSetIsEnUs(): void
{
$this->switchPaymentLang('en-us');
$html = $this->callActionHtml(ConfigController::class, 'index');
$this->assertStringContainsString('Payment Management', $html);
$this->assertStringContainsString('Recycle Bin', $html);
$this->assertStringContainsString('Payment Code', $html);
$this->assertStringContainsString('Payment Name', $html);
$this->assertStringContainsString('Payment Type', $html);
$this->assertStringContainsString('Payment Configuration Management', $html);
$this->assertStringContainsString('Search', $html);
$this->assertStringNotContainsString('支付编号', $html);
}
public function testConfigAddRendersEnglishTextsWhenLangSetIsEnUs(): void
{
$this->switchPaymentLang('en-us');
$html = $this->callActionHtml(ConfigController::class, 'add');
$this->assertStringContainsString('Payment Type', $html);
$this->assertStringContainsString('Payment Name', $html);
$this->assertStringContainsString('Payment Icon', $html);
$this->assertStringContainsString('Payment Description', $html);
$this->assertStringContainsString('Save Data', $html);
$this->assertStringContainsString('Offline Payment QR Code', $html);
$this->assertStringNotContainsString('支付方式', $html);
}
public function testIntegralIndexRendersEnglishTextsWhenLangSetIsEnUs(): void
{
$user = $this->createAccountUser([
'username' => 'integral-english-user',
'nickname' => '积分英文用户',
]);
Integral::create(intval($user->getAttr('id')), 'ledger-integral-en', '英文积分', '16.00', '英文积分记录');
$this->switchPaymentLang('en-us');
$html = $this->callActionHtml(IntegralController::class, 'index');
$this->assertStringContainsString('Integral Statistics', $html);
$this->assertStringContainsString('Total Issued', $html);
$this->assertStringContainsString('User Account', $html);
$this->assertStringContainsString('Transaction Status', $html);
$this->assertStringContainsString('Operation Remark', $html);
$this->assertStringContainsString('Actions', $html);
$this->assertStringNotContainsString('积分统计', $html);
}
public function testApiBalanceGetReturnsEnglishInfoWhenLangSetIsEnUs(): void
{
$account = $this->createBoundAccountFixture();
$login = $account->token()->get(true);
Balance::create($account->getUnid(), 'api-balance-001', 'API余额', '18.00', 'API余额记录', true);
$this->switchPaymentLang('en-us');
$response = $this->callAuthApiController(AuthBalanceController::class, 'get', ['page' => 1], strval($login['token'] ?? ''));
$this->assertSame(200, intval($response['code'] ?? 0));
$this->assertSame('Balance records loaded successfully', $response['info'] ?? '');
$this->assertNotEmpty($response['data'] ?? []);
}
public function testApiIntegralGetReturnsEnglishInfoWhenLangSetIsEnUs(): void
{
$account = $this->createBoundAccountFixture();
$login = $account->token()->get(true);
Integral::create($account->getUnid(), 'api-integral-001', 'API积分', '16.00', 'API积分记录', true);
$this->switchPaymentLang('en-us');
$response = $this->callAuthApiController(AuthIntegralController::class, 'get', ['page' => 1], strval($login['token'] ?? ''));
$this->assertSame(200, intval($response['code'] ?? 0));
$this->assertSame('Integral records loaded successfully', $response['info'] ?? '');
$this->assertNotEmpty($response['data'] ?? []);
}
protected function defineSchema(): void
{
$this->createAccountTables();
$this->createPaymentConfigTable();
$this->createPaymentBalanceTable();
$this->createPaymentIntegralTable();
}
/**
* @param class-string $controllerClass
*/
private function callController(string $controllerClass, string $action, array $data): array
{
$parts = explode('\\', $controllerClass);
$request = (new Request())
->withGet($data)
->withPost($data)
->setMethod('POST')
->setController(strtolower(strval(end($parts))))
->setAction($action);
RequestContext::clear();
$this->app->instance('request', $request);
try {
$controller = new $controllerClass($this->app);
$controller->{$action}();
self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException.");
} catch (HttpResponseException $exception) {
return json_decode($exception->getResponse()->getContent(), true) ?: [];
}
}
/**
* @param class-string $controllerClass
*/
private function callIndexController(string $controllerClass, array $query): array
{
$parts = explode('\\', $controllerClass);
$request = (new Request())
->withGet($query)
->setMethod('GET')
->setController(strtolower(strval(end($parts))))
->setAction('index');
$this->setRequestPayload($request, $query);
RequestContext::clear();
$this->app->instance('request', $request);
try {
$controller = new $controllerClass($this->app);
$controller->index();
self::fail("Expected {$controllerClass}::index to throw HttpResponseException.");
} catch (HttpResponseException $exception) {
return json_decode($exception->getResponse()->getContent(), true) ?: [];
}
}
/**
* @param class-string $controllerClass
*/
private function callAuthApiController(string $controllerClass, string $action, array $query, string $token): array
{
$parts = explode('\\', $controllerClass);
$request = (new Request())
->withGet($query)
->withHeader(['authorization' => "Bearer {$token}"])
->setMethod('GET')
->setController('api.auth.' . strtolower(strval(end($parts))))
->setAction($action);
$this->setRequestPayload($request, $query);
RequestContext::clear();
$this->app->instance('request', $request);
try {
$controller = new $controllerClass($this->app);
$controller->{$action}();
self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException.");
} catch (HttpResponseException $exception) {
return json_decode($exception->getResponse()->getContent(), true) ?: [];
}
}
/**
* @param class-string $controllerClass
*/
private function callActionHtml(string $controllerClass, string $action, array $query = []): string
{
$parts = explode('\\', $controllerClass);
$request = (new Request())
->withGet($query)
->setMethod('GET')
->setController(strtolower(strval(end($parts))))
->setAction($action);
$this->setRequestPayload($request, $query);
RequestContext::clear();
RequestContext::instance()->setAuth([
'id' => 9001,
'username' => 'admin',
'password' => 'test-admin-password',
], '', true);
$this->activateApplicationContext($request);
try {
$controller = new $controllerClass($this->app);
$controller->{$action}();
self::fail("Expected {$controllerClass}::{$action} to throw HttpResponseException.");
} catch (HttpResponseException $exception) {
return $exception->getResponse()->getContent();
}
}
private function setRequestPayload(Request $request, array $data): void
{
$property = new \ReflectionProperty(Request::class, 'request');
$property->setAccessible(true);
$property->setValue($request, $data);
}
private function switchPaymentLang(string $langSet): void
{
$this->app->lang->switchLangSet($langSet);
foreach ([
TEST_PROJECT_ROOT . "/plugin/think-plugs-payment/src/lang/{$langSet}.php",
TEST_PROJECT_ROOT . "/plugin/think-plugs-account/src/lang/{$langSet}.php",
] as $file) {
if (is_file($file)) {
$this->app->lang->load($file, $langSet);
}
}
}
private function createPaymentConfigTable(): void
{
$this->executeStatements([
<<<'SQL'
CREATE TABLE plugin_payment_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT DEFAULT '',
code TEXT DEFAULT '',
name TEXT DEFAULT '',
cover TEXT DEFAULT '',
remark TEXT DEFAULT '',
content TEXT DEFAULT '',
sort INTEGER DEFAULT 0,
status INTEGER DEFAULT 1,
delete_time TEXT DEFAULT NULL,
create_time TEXT DEFAULT NULL,
update_time TEXT DEFAULT NULL
)
SQL,
]);
}
}