diff --git a/app/Console/Commands/CheckTicket.php b/app/Console/Commands/CheckTicket.php index 51d4539..ab39ba8 100644 --- a/app/Console/Commands/CheckTicket.php +++ b/app/Console/Commands/CheckTicket.php @@ -40,7 +40,7 @@ class CheckTicket extends Command { Ticket::where('status', 0) ->where('updated_at', '<=', time() - 24 * 3600) - ->where('reply_status', 0) + ->where('reply_status', Ticket::REPLY_STATUS_REPLIED) ->lazyById(200) ->each(function ($ticket) { if ($ticket->user_id === $ticket->last_reply_user_id) return; diff --git a/app/Http/Controllers/V2/Admin/TicketController.php b/app/Http/Controllers/V2/Admin/TicketController.php index ca6d8c4..9177343 100644 --- a/app/Http/Controllers/V2/Admin/TicketController.php +++ b/app/Http/Controllers/V2/Admin/TicketController.php @@ -55,6 +55,7 @@ class TicketController extends Controller if (!$ticket) { return $this->fail([400202, '工单不存在']); } + $ticket->messages->each(fn($msg) => $msg->setRelation('ticket', $ticket)); $result = $ticket->toArray(); $result['user'] = UserController::transformUserData($ticket->user); @@ -144,11 +145,12 @@ class TicketController extends Controller $ticket = Ticket::with([ 'user', 'messages' => function ($query) { - $query->with(['user']); // 如果需要用户信息 + $query->with(['user']); } ])->findOrFail($ticketId); - // 自动包含 is_me 属性 + $ticket->messages->each(fn($msg) => $msg->setRelation('ticket', $ticket)); + return response()->json([ 'data' => $ticket ]); diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 86ba9b6..913aeef 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -39,6 +39,9 @@ class Ticket extends Model self::STATUS_CLOSED => '关闭' ]; + const REPLY_STATUS_WAITING = 0; + const REPLY_STATUS_REPLIED = 1; + public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id', 'id'); diff --git a/app/Models/TicketMessage.php b/app/Models/TicketMessage.php index c9d45a8..3cca191 100644 --- a/app/Models/TicketMessage.php +++ b/app/Models/TicketMessage.php @@ -29,6 +29,7 @@ class TicketMessage extends Model ]; protected $appends = ['is_from_user', 'is_from_admin']; + protected $hidden = ['ticket']; /** * 关联的工单 @@ -43,7 +44,7 @@ class TicketMessage extends Model */ 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 { - return $this->ticket->user_id !== $this->user_id; + return $this->ticket && $this->ticket->user_id !== $this->user_id; } } diff --git a/app/Services/TicketService.php b/app/Services/TicketService.php index f99d7c7..84ee389 100644 --- a/app/Services/TicketService.php +++ b/app/Services/TicketService.php @@ -22,11 +22,11 @@ class TicketService 'ticket_id' => $ticket->id, 'message' => $message ]); - if ($userId !== $ticket->user_id) { - $ticket->reply_status = Ticket::STATUS_OPENING; - } else { - $ticket->reply_status = Ticket::STATUS_CLOSED; - } + $isAdmin = $userId !== $ticket->user_id; + $ticket->reply_status = $isAdmin + ? Ticket::REPLY_STATUS_REPLIED + : Ticket::REPLY_STATUS_WAITING; + $ticket->last_reply_user_id = $userId; if (!$ticketMessage || !$ticket->save()) { throw new \Exception(); } @@ -40,33 +40,15 @@ class TicketService public function replyByAdmin($ticketId, $message, $userId): void { - $ticket = Ticket::where('id', $ticketId) - ->first(); + $ticket = Ticket::where('id', $ticketId)->first(); if (!$ticket) { throw new ApiException('工单不存在'); } - $ticket->status = Ticket::STATUS_OPENING; - try { - DB::beginTransaction(); - $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; + $ticketMessage = $this->reply($ticket, $message, $userId); + if (!$ticketMessage) { + throw new ApiException('工单回复失败'); } + HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]); $this->sendEmailNotify($ticket, $ticketMessage); } @@ -81,7 +63,9 @@ class TicketService $ticket = Ticket::create([ 'user_id' => $userId, 'subject' => $subject, - 'level' => $level + 'level' => $level, + 'reply_status' => Ticket::REPLY_STATUS_WAITING, + 'last_reply_user_id' => $userId, ]); if (!$ticket) { throw new ApiException('工单创建失败'); diff --git a/database/migrations/2026_04_21_000001_fix_ticket_reply_status.php b/database/migrations/2026_04_21_000001_fix_ticket_reply_status.php new file mode 100644 index 0000000..ced036a --- /dev/null +++ b/database/migrations/2026_04_21_000001_fix_ticket_reply_status.php @@ -0,0 +1,50 @@ +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. + } +};