feat(admin-frontend): 补齐活跃筛选与支付快照能力
新增用户管理“活跃状态”高级筛选,并在后端支持 activity_status 复合规则,支持按活跃与非活跃筛选用户。 补齐订单支付成功快照落库与后台展示,保存支付渠道、 支付方法、实付金额和支付 IP,并在订单详情中优先展示。 同时增强节点页在线/离线筛选与批量删除、仪表盘快捷入口, 并修复已关闭工单再次回复后自动重开的统一语义。 附带同步测试、迁移、CI 工作流命名及知识库记录
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Jobs\OrderHandleJob;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use App\Models\Plan;
|
||||
use App\Models\TrafficResetLog;
|
||||
use App\Models\User;
|
||||
@@ -283,7 +284,7 @@ class OrderService
|
||||
}
|
||||
}
|
||||
|
||||
public function paid(string $callbackNo)
|
||||
public function paid(string $callbackNo, array $paymentSnapshot = [])
|
||||
{
|
||||
$order = $this->order;
|
||||
if ($order->status !== Order::STATUS_PENDING)
|
||||
@@ -291,6 +292,7 @@ class OrderService
|
||||
$order->status = Order::STATUS_PROCESSING;
|
||||
$order->paid_at = time();
|
||||
$order->callback_no = $callbackNo;
|
||||
$order->fill($this->buildPaymentSnapshot($paymentSnapshot));
|
||||
if (!$order->save())
|
||||
return false;
|
||||
try {
|
||||
@@ -302,6 +304,82 @@ class OrderService
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $paymentSnapshot
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildPaymentSnapshot(array $paymentSnapshot): array
|
||||
{
|
||||
/** @var Payment|null $payment */
|
||||
$payment = $this->order->relationLoaded('payment')
|
||||
? $this->order->payment
|
||||
: $this->order->payment()->first();
|
||||
|
||||
$channel = $this->normalizeSnapshotText(
|
||||
$paymentSnapshot['payment_channel'] ?? $payment?->name ?? $payment?->payment
|
||||
);
|
||||
$method = $this->normalizeSnapshotText(
|
||||
$paymentSnapshot['payment_method'] ?? $this->resolvePaymentMethod($payment)
|
||||
);
|
||||
$amount = $this->normalizeSnapshotAmount($paymentSnapshot['payment_amount'] ?? null);
|
||||
$ip = $this->normalizeSnapshotText($paymentSnapshot['payment_ip'] ?? null);
|
||||
|
||||
return array_filter([
|
||||
'payment_channel' => $channel,
|
||||
'payment_method' => $method,
|
||||
'payment_amount' => $amount,
|
||||
'payment_ip' => $ip,
|
||||
], static fn($value) => $value !== null && $value !== '');
|
||||
}
|
||||
|
||||
private function resolvePaymentMethod(?Payment $payment): ?string
|
||||
{
|
||||
if (!$payment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$config = $payment->config;
|
||||
if (is_string($config)) {
|
||||
$decoded = json_decode($config, true);
|
||||
$config = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
if (!is_array($config)) {
|
||||
$config = [];
|
||||
}
|
||||
|
||||
return $this->normalizeSnapshotText(
|
||||
data_get($config, 'token_pay_currency') ?? $payment->payment ?? $payment->name
|
||||
);
|
||||
}
|
||||
|
||||
private function normalizeSnapshotText(mixed $value): ?string
|
||||
{
|
||||
if (!is_scalar($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = trim((string) $value);
|
||||
return $text !== '' ? $text : null;
|
||||
}
|
||||
|
||||
private function normalizeSnapshotAmount(mixed $value): ?int
|
||||
{
|
||||
if ($value === null || $value === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = is_string($value)
|
||||
? str_replace(',', '', trim($value))
|
||||
: $value;
|
||||
|
||||
if (!is_numeric($normalized)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) round(((float) $normalized) * 100);
|
||||
}
|
||||
|
||||
public function cancel(): bool
|
||||
{
|
||||
$order = $this->order;
|
||||
|
||||
@@ -22,11 +22,7 @@ class TicketService
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $message
|
||||
]);
|
||||
$isAdmin = $userId !== $ticket->user_id;
|
||||
$ticket->reply_status = $isAdmin
|
||||
? Ticket::REPLY_STATUS_REPLIED
|
||||
: Ticket::REPLY_STATUS_WAITING;
|
||||
$ticket->last_reply_user_id = $userId;
|
||||
$this->applyReplyState($ticket, (int) $userId);
|
||||
if (!$ticketMessage || !$ticket->save()) {
|
||||
throw new \Exception();
|
||||
}
|
||||
@@ -52,6 +48,24 @@ class TicketService
|
||||
$this->sendEmailNotify($ticket, $ticketMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the unified reply state to a ticket.
|
||||
*
|
||||
* @param Ticket $ticket The target ticket.
|
||||
* @param int $userId The replying user ID.
|
||||
* @return void
|
||||
*/
|
||||
private function applyReplyState(Ticket $ticket, int $userId): void
|
||||
{
|
||||
$isAdmin = $userId !== $ticket->user_id;
|
||||
|
||||
$ticket->status = Ticket::STATUS_OPENING;
|
||||
$ticket->reply_status = $isAdmin
|
||||
? Ticket::REPLY_STATUS_REPLIED
|
||||
: Ticket::REPLY_STATUS_WAITING;
|
||||
$ticket->last_reply_user_id = $userId;
|
||||
}
|
||||
|
||||
public function createTicket($userId, $subject, $level, $message)
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user