feat(admin-frontend): 补齐活跃筛选与支付快照能力

新增用户管理“活跃状态”高级筛选,并在后端支持
activity_status 复合规则,支持按活跃与非活跃筛选用户。

补齐订单支付成功快照落库与后台展示,保存支付渠道、
支付方法、实付金额和支付 IP,并在订单详情中优先展示。

同时增强节点页在线/离线筛选与批量删除、仪表盘快捷入口,
并修复已关闭工单再次回复后自动重开的统一语义。

附带同步测试、迁移、CI 工作流命名及知识库记录
This commit is contained in:
yinjianm
2026-04-25 00:59:08 +08:00
parent 2218457237
commit c64badfc23
55 changed files with 2023 additions and 71 deletions
@@ -0,0 +1,92 @@
<?php
namespace Tests\Unit\Orders;
use App\Models\Order;
use App\Models\Payment;
use App\Services\OrderService;
use PHPUnit\Framework\TestCase;
use ReflectionMethod;
class OrderServicePaymentSnapshotTest extends TestCase
{
public function test_build_payment_snapshot_prefers_callback_metadata_over_payment_defaults(): void
{
$payment = new Payment([
'name' => 'TokenPay 收银台',
'payment' => 'TokenPay',
'config' => ['token_pay_currency' => 'USDT_TRC20'],
]);
$order = new Order();
$order->setRelation('payment', $payment);
$payload = $this->invokeBuildPaymentSnapshot($order, [
'payment_channel' => '链上收银台',
'payment_method' => 'TRC20',
'payment_amount' => '19.29',
'payment_ip' => '117.132.7.238',
]);
$this->assertSame('链上收银台', $payload['payment_channel']);
$this->assertSame('TRC20', $payload['payment_method']);
$this->assertSame(1929, $payload['payment_amount']);
$this->assertSame('117.132.7.238', $payload['payment_ip']);
}
public function test_build_payment_snapshot_falls_back_to_payment_config_when_callback_method_missing(): void
{
$payment = new Payment([
'name' => 'TokenPay 收银台',
'payment' => 'TokenPay',
'config' => ['token_pay_currency' => 'USDT_TRC20'],
]);
$order = new Order();
$order->setRelation('payment', $payment);
$payload = $this->invokeBuildPaymentSnapshot($order, []);
$this->assertSame('TokenPay 收银台', $payload['payment_channel']);
$this->assertSame('USDT_TRC20', $payload['payment_method']);
$this->assertArrayNotHasKey('payment_amount', $payload);
$this->assertArrayNotHasKey('payment_ip', $payload);
}
public function test_build_payment_snapshot_ignores_invalid_amount_and_blank_ip(): void
{
$payment = new Payment([
'payment' => 'TokenPay',
'config' => [],
]);
$order = new Order();
$order->setRelation('payment', $payment);
$payload = $this->invokeBuildPaymentSnapshot($order, [
'payment_amount' => 'invalid',
'payment_ip' => ' ',
]);
$this->assertSame('TokenPay', $payload['payment_channel']);
$this->assertSame('TokenPay', $payload['payment_method']);
$this->assertArrayNotHasKey('payment_amount', $payload);
$this->assertArrayNotHasKey('payment_ip', $payload);
}
/**
* @param array<string, mixed> $paymentSnapshot
* @return array<string, mixed>
*/
private function invokeBuildPaymentSnapshot(Order $order, array $paymentSnapshot): array
{
$service = new OrderService($order);
$method = new ReflectionMethod(OrderService::class, 'buildPaymentSnapshot');
$method->setAccessible(true);
/** @var array<string, mixed> $result */
$result = $method->invoke($service, $paymentSnapshot);
return $result;
}
}