fix: ticket reply_status semantics, N+1 query, and admin reply auto-reopen

This commit is contained in:
xboard
2026-04-18 16:40:21 +08:00
parent da8b5018ea
commit 360684245e
6 changed files with 74 additions and 34 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ class CheckTicket extends Command
{ {
Ticket::where('status', 0) Ticket::where('status', 0)
->where('updated_at', '<=', time() - 24 * 3600) ->where('updated_at', '<=', time() - 24 * 3600)
->where('reply_status', 0) ->where('reply_status', Ticket::REPLY_STATUS_REPLIED)
->lazyById(200) ->lazyById(200)
->each(function ($ticket) { ->each(function ($ticket) {
if ($ticket->user_id === $ticket->last_reply_user_id) return; if ($ticket->user_id === $ticket->last_reply_user_id) return;
@@ -55,6 +55,7 @@ class TicketController extends Controller
if (!$ticket) { if (!$ticket) {
return $this->fail([400202, '工单不存在']); return $this->fail([400202, '工单不存在']);
} }
$ticket->messages->each(fn($msg) => $msg->setRelation('ticket', $ticket));
$result = $ticket->toArray(); $result = $ticket->toArray();
$result['user'] = UserController::transformUserData($ticket->user); $result['user'] = UserController::transformUserData($ticket->user);
@@ -144,11 +145,12 @@ class TicketController extends Controller
$ticket = Ticket::with([ $ticket = Ticket::with([
'user', 'user',
'messages' => function ($query) { 'messages' => function ($query) {
$query->with(['user']); // 如果需要用户信息 $query->with(['user']);
} }
])->findOrFail($ticketId); ])->findOrFail($ticketId);
// 自动包含 is_me 属性 $ticket->messages->each(fn($msg) => $msg->setRelation('ticket', $ticket));
return response()->json([ return response()->json([
'data' => $ticket 'data' => $ticket
]); ]);
+3
View File
@@ -39,6 +39,9 @@ class Ticket extends Model
self::STATUS_CLOSED => '关闭' self::STATUS_CLOSED => '关闭'
]; ];
const REPLY_STATUS_WAITING = 0;
const REPLY_STATUS_REPLIED = 1;
public function user(): BelongsTo public function user(): BelongsTo
{ {
return $this->belongsTo(User::class, 'user_id', 'id'); return $this->belongsTo(User::class, 'user_id', 'id');
+3 -2
View File
@@ -29,6 +29,7 @@ class TicketMessage extends Model
]; ];
protected $appends = ['is_from_user', 'is_from_admin']; protected $appends = ['is_from_user', 'is_from_admin'];
protected $hidden = ['ticket'];
/** /**
* 关联的工单 * 关联的工单
@@ -43,7 +44,7 @@ class TicketMessage extends Model
*/ */
public function getIsFromUserAttribute(): bool public function getIsFromUserAttribute(): bool
{ {
return $this->ticket->user_id === $this->user_id; return $this->ticket && $this->ticket->user_id === $this->user_id;
} }
/** /**
@@ -51,6 +52,6 @@ class TicketMessage extends Model
*/ */
public function getIsFromAdminAttribute(): bool public function getIsFromAdminAttribute(): bool
{ {
return $this->ticket->user_id !== $this->user_id; return $this->ticket && $this->ticket->user_id !== $this->user_id;
} }
} }
+13 -29
View File
@@ -22,11 +22,11 @@ class TicketService
'ticket_id' => $ticket->id, 'ticket_id' => $ticket->id,
'message' => $message 'message' => $message
]); ]);
if ($userId !== $ticket->user_id) { $isAdmin = $userId !== $ticket->user_id;
$ticket->reply_status = Ticket::STATUS_OPENING; $ticket->reply_status = $isAdmin
} else { ? Ticket::REPLY_STATUS_REPLIED
$ticket->reply_status = Ticket::STATUS_CLOSED; : Ticket::REPLY_STATUS_WAITING;
} $ticket->last_reply_user_id = $userId;
if (!$ticketMessage || !$ticket->save()) { if (!$ticketMessage || !$ticket->save()) {
throw new \Exception(); throw new \Exception();
} }
@@ -40,33 +40,15 @@ class TicketService
public function replyByAdmin($ticketId, $message, $userId): void public function replyByAdmin($ticketId, $message, $userId): void
{ {
$ticket = Ticket::where('id', $ticketId) $ticket = Ticket::where('id', $ticketId)->first();
->first();
if (!$ticket) { if (!$ticket) {
throw new ApiException('工单不存在'); throw new ApiException('工单不存在');
} }
$ticket->status = Ticket::STATUS_OPENING; $ticketMessage = $this->reply($ticket, $message, $userId);
try { if (!$ticketMessage) {
DB::beginTransaction(); throw new ApiException('工单回复失败');
$ticketMessage = TicketMessage::create([
'user_id' => $userId,
'ticket_id' => $ticket->id,
'message' => $message
]);
if ($userId !== $ticket->user_id) {
$ticket->reply_status = Ticket::STATUS_OPENING;
} else {
$ticket->reply_status = Ticket::STATUS_CLOSED;
}
if (!$ticketMessage || !$ticket->save()) {
throw new ApiException('工单回复失败');
}
DB::commit();
HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]);
} catch (\Exception $e) {
DB::rollBack();
throw $e;
} }
HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]);
$this->sendEmailNotify($ticket, $ticketMessage); $this->sendEmailNotify($ticket, $ticketMessage);
} }
@@ -81,7 +63,9 @@ class TicketService
$ticket = Ticket::create([ $ticket = Ticket::create([
'user_id' => $userId, 'user_id' => $userId,
'subject' => $subject, 'subject' => $subject,
'level' => $level 'level' => $level,
'reply_status' => Ticket::REPLY_STATUS_WAITING,
'last_reply_user_id' => $userId,
]); ]);
if (!$ticket) { if (!$ticket) {
throw new ApiException('工单创建失败'); throw new ApiException('工单创建失败');
@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// Add last_reply_user_id column if not exists
if (!Schema::hasColumn('v2_ticket', 'last_reply_user_id')) {
Schema::table('v2_ticket', function (Blueprint $table) {
$table->integer('last_reply_user_id')->nullable()->after('reply_status');
});
}
// Fix reply_status semantics: swap 0 and 1
// Old: 0=admin replied, 1=user replied (inverted)
// New: 0=待回复(waiting), 1=已回复(replied) — matches frontend expectations
DB::table('v2_ticket')
->whereIn('reply_status', [0, 1])
->update([
'reply_status' => DB::raw("CASE WHEN reply_status = 0 THEN 1 WHEN reply_status = 1 THEN 0 END")
]);
// Fix default: new tickets should be "待回复" (0), not "已回复" (1)
Schema::table('v2_ticket', function (Blueprint $table) {
$table->integer('reply_status')->default(0)->comment('0:待回复 1:已回复')->change();
});
}
public function down(): void
{
// Reverse the swap
DB::table('v2_ticket')
->whereIn('reply_status', [0, 1])
->update([
'reply_status' => DB::raw("CASE WHEN reply_status = 0 THEN 1 WHEN reply_status = 1 THEN 0 END")
]);
Schema::table('v2_ticket', function (Blueprint $table) {
$table->integer('reply_status')->default(1)->comment('0:待回复 1:已回复')->change();
});
// Note: last_reply_user_id column is intentionally kept to avoid dropping
// a column that may have existed before this migration.
}
};