fix: ticket reply_status semantics, N+1 query, and admin reply auto-reopen
This commit is contained in:
@@ -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
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
}
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user