299 lines
9.6 KiB
PHP
299 lines
9.6 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 文件功能:用户反馈前台控制器
|
|||
|
|
* 对应独立页面 /feedback,处理用户提交 Bug报告/功能建议、
|
|||
|
|
* 赞同(Toggle)、补充评论、删除等操作
|
|||
|
|
* 所有写操作均需登录(chat.auth 中间件保护)
|
|||
|
|
*
|
|||
|
|
* @author ChatRoom Laravel
|
|||
|
|
*
|
|||
|
|
* @version 1.0.0
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
namespace App\Http\Controllers;
|
|||
|
|
|
|||
|
|
use App\Models\FeedbackItem;
|
|||
|
|
use App\Models\FeedbackReply;
|
|||
|
|
use App\Models\FeedbackVote;
|
|||
|
|
use Illuminate\Http\JsonResponse;
|
|||
|
|
use Illuminate\Http\Request;
|
|||
|
|
use Illuminate\Support\Facades\Auth;
|
|||
|
|
use Illuminate\Support\Facades\DB;
|
|||
|
|
use Illuminate\View\View;
|
|||
|
|
|
|||
|
|
class FeedbackController extends Controller
|
|||
|
|
{
|
|||
|
|
/** 每次懒加载的条数 */
|
|||
|
|
private const PAGE_SIZE = 10;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 用户反馈列表页(SSR首屏)
|
|||
|
|
* 预加载按赞同数倒序的 10 条反馈
|
|||
|
|
*/
|
|||
|
|
public function index(): View
|
|||
|
|
{
|
|||
|
|
$feedbacks = FeedbackItem::with(['replies'])
|
|||
|
|
->orderByDesc('votes_count')
|
|||
|
|
->orderByDesc('created_at')
|
|||
|
|
->limit(self::PAGE_SIZE)
|
|||
|
|
->get();
|
|||
|
|
|
|||
|
|
// 当前用户已赞同的反馈 ID 集合(前端切换按钮状态用)
|
|||
|
|
$myVotedIds = FeedbackVote::where('user_id', Auth::id())
|
|||
|
|
->whereIn('feedback_id', $feedbacks->pluck('id'))
|
|||
|
|
->pluck('feedback_id')
|
|||
|
|
->toArray();
|
|||
|
|
|
|||
|
|
return view('feedback.index', compact('feedbacks', 'myVotedIds'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 懒加载更多反馈(JSON API)
|
|||
|
|
* 支持按类型筛选(bug / suggestion)
|
|||
|
|
*
|
|||
|
|
* @param Request $request 含 after_id / type 筛选参数
|
|||
|
|
*/
|
|||
|
|
public function loadMore(Request $request): JsonResponse
|
|||
|
|
{
|
|||
|
|
$afterId = (int) $request->input('after_id', PHP_INT_MAX);
|
|||
|
|
$type = $request->input('type'); // bug|suggestion|null(全部)
|
|||
|
|
|
|||
|
|
$query = FeedbackItem::with(['replies'])
|
|||
|
|
->where('id', '<', $afterId)
|
|||
|
|
->orderByDesc('votes_count')
|
|||
|
|
->orderByDesc('created_at');
|
|||
|
|
|
|||
|
|
if ($type && in_array($type, ['bug', 'suggestion'])) {
|
|||
|
|
$query->ofType($type);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$items = $query->limit(self::PAGE_SIZE)->get();
|
|||
|
|
|
|||
|
|
// 当前用户已赞同的 ID(用于切换按钮状态)
|
|||
|
|
$myVotedIds = FeedbackVote::where('user_id', Auth::id())
|
|||
|
|
->whereIn('feedback_id', $items->pluck('id'))
|
|||
|
|
->pluck('feedback_id')
|
|||
|
|
->toArray();
|
|||
|
|
|
|||
|
|
return response()->json([
|
|||
|
|
'items' => $this->formatItems($items, $myVotedIds),
|
|||
|
|
'has_more' => $items->count() === self::PAGE_SIZE,
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 提交新反馈(Bug报告或功能建议)
|
|||
|
|
*
|
|||
|
|
* @param Request $request 含 type/title/content 字段
|
|||
|
|
*/
|
|||
|
|
public function store(Request $request): JsonResponse
|
|||
|
|
{
|
|||
|
|
$data = $request->validate([
|
|||
|
|
'type' => 'required|in:bug,suggestion',
|
|||
|
|
'title' => 'required|string|max:200',
|
|||
|
|
'content' => 'required|string|max:2000',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
/** @var \App\Models\User $user */
|
|||
|
|
$user = Auth::user();
|
|||
|
|
|
|||
|
|
$item = FeedbackItem::create([
|
|||
|
|
'user_id' => $user->id,
|
|||
|
|
'username' => $user->username,
|
|||
|
|
'type' => $data['type'],
|
|||
|
|
'title' => $data['title'],
|
|||
|
|
'content' => $data['content'],
|
|||
|
|
'status' => 'pending',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
return response()->json([
|
|||
|
|
'status' => 'success',
|
|||
|
|
'message' => '反馈已提交,感谢您的贡献!',
|
|||
|
|
'item' => $this->formatItem($item, false),
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 赞同/取消赞同反馈(Toggle 操作)
|
|||
|
|
* 每人每条只能赞同一次,再次点击则取消
|
|||
|
|
* 使用数据库事务保证 votes_count 冗余字段与记录一致
|
|||
|
|
*
|
|||
|
|
* @param int $id 反馈 ID
|
|||
|
|
*/
|
|||
|
|
public function vote(int $id): JsonResponse
|
|||
|
|
{
|
|||
|
|
$feedback = FeedbackItem::findOrFail($id);
|
|||
|
|
$userId = Auth::id();
|
|||
|
|
|
|||
|
|
// 不能赞同自己提交的反馈
|
|||
|
|
if ($feedback->user_id === $userId) {
|
|||
|
|
return response()->json([
|
|||
|
|
'status' => 'error',
|
|||
|
|
'message' => '不能赞同自己的反馈',
|
|||
|
|
], 422);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
$voted = false;
|
|||
|
|
|
|||
|
|
DB::transaction(function () use ($feedback, $userId, &$voted): void {
|
|||
|
|
$existing = FeedbackVote::where('feedback_id', $feedback->id)
|
|||
|
|
->where('user_id', $userId)
|
|||
|
|
->first();
|
|||
|
|
|
|||
|
|
if ($existing) {
|
|||
|
|
// 已赞同 → 取消赞同
|
|||
|
|
$existing->delete();
|
|||
|
|
$feedback->decrement('votes_count');
|
|||
|
|
$voted = false;
|
|||
|
|
} else {
|
|||
|
|
// 未赞同 → 新增赞同
|
|||
|
|
FeedbackVote::create([
|
|||
|
|
'feedback_id' => $feedback->id,
|
|||
|
|
'user_id' => $userId,
|
|||
|
|
]);
|
|||
|
|
$feedback->increment('votes_count');
|
|||
|
|
$voted = true;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return response()->json([
|
|||
|
|
'status' => 'success',
|
|||
|
|
'voted' => $voted,
|
|||
|
|
'votes_count' => $feedback->fresh()->votes_count,
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 提交补充评论
|
|||
|
|
* id=1 管理员的回复自动标记 is_admin=true(前台特殊展示)
|
|||
|
|
*
|
|||
|
|
* @param Request $request 含 content 字段
|
|||
|
|
* @param int $id 反馈 ID
|
|||
|
|
*/
|
|||
|
|
public function reply(Request $request, int $id): JsonResponse
|
|||
|
|
{
|
|||
|
|
$feedback = FeedbackItem::findOrFail($id);
|
|||
|
|
|
|||
|
|
$data = $request->validate([
|
|||
|
|
'content' => 'required|string|max:1000',
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
/** @var \App\Models\User $user */
|
|||
|
|
$user = Auth::user();
|
|||
|
|
|
|||
|
|
/** @var FeedbackReply $reply */
|
|||
|
|
$reply = null;
|
|||
|
|
|
|||
|
|
DB::transaction(function () use ($feedback, $data, $user, &$reply): void {
|
|||
|
|
$reply = FeedbackReply::create([
|
|||
|
|
'feedback_id' => $feedback->id,
|
|||
|
|
'user_id' => $user->id,
|
|||
|
|
'username' => $user->username,
|
|||
|
|
'content' => $data['content'],
|
|||
|
|
'is_admin' => $user->id === 1,
|
|||
|
|
]);
|
|||
|
|
|
|||
|
|
$feedback->increment('replies_count');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return response()->json([
|
|||
|
|
'status' => 'success',
|
|||
|
|
'message' => '评论已提交',
|
|||
|
|
'reply' => [
|
|||
|
|
'id' => $reply->id,
|
|||
|
|
'username' => $reply->username,
|
|||
|
|
'content' => $reply->content,
|
|||
|
|
'is_admin' => $reply->is_admin,
|
|||
|
|
'created_at' => $reply->created_at->diffForHumans(),
|
|||
|
|
],
|
|||
|
|
]);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 删除反馈
|
|||
|
|
* 普通用户:仅24小时内可删除自己的反馈
|
|||
|
|
* 管理员(id=1):任意时间可删除任意反馈
|
|||
|
|
*
|
|||
|
|
* @param int $id 反馈 ID
|
|||
|
|
*/
|
|||
|
|
public function destroy(int $id): JsonResponse
|
|||
|
|
{
|
|||
|
|
$feedback = FeedbackItem::findOrFail($id);
|
|||
|
|
|
|||
|
|
/** @var \App\Models\User $user */
|
|||
|
|
$user = Auth::user();
|
|||
|
|
$isOwner = $feedback->user_id === $user->id;
|
|||
|
|
$isAdmin = $user->id === 1;
|
|||
|
|
|
|||
|
|
if (! $isOwner && ! $isAdmin) {
|
|||
|
|
return response()->json(['status' => 'error', 'message' => '无权删除'], 403);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ($isOwner && ! $isAdmin && ! $feedback->is_within_24_hours) {
|
|||
|
|
return response()->json([
|
|||
|
|
'status' => 'error',
|
|||
|
|
'message' => '超过 24 小时的反馈无法删除',
|
|||
|
|
], 422);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 级联删除关联的赞同记录和评论记录
|
|||
|
|
DB::transaction(function () use ($feedback): void {
|
|||
|
|
FeedbackVote::where('feedback_id', $feedback->id)->delete();
|
|||
|
|
FeedbackReply::where('feedback_id', $feedback->id)->delete();
|
|||
|
|
$feedback->delete();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return response()->json(['status' => 'success', 'message' => '已删除']);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ═══════════════ 私有辅助方法 ═══════════════
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 格式化单条反馈数据(供 JSON 返回给前端)
|
|||
|
|
*
|
|||
|
|
* @param FeedbackItem $item 反馈实例
|
|||
|
|
* @param bool $voted 当前用户是否已赞同
|
|||
|
|
*/
|
|||
|
|
private function formatItem(FeedbackItem $item, bool $voted): array
|
|||
|
|
{
|
|||
|
|
return [
|
|||
|
|
'id' => $item->id,
|
|||
|
|
'type' => $item->type,
|
|||
|
|
'type_label' => $item->type_label,
|
|||
|
|
'title' => $item->title,
|
|||
|
|
'content' => $item->content,
|
|||
|
|
'status' => $item->status,
|
|||
|
|
'status_label' => $item->status_label,
|
|||
|
|
'status_color' => $item->status_config['color'],
|
|||
|
|
'admin_remark' => $item->admin_remark,
|
|||
|
|
'votes_count' => $item->votes_count,
|
|||
|
|
'replies_count' => $item->replies_count,
|
|||
|
|
'username' => $item->username,
|
|||
|
|
'created_at' => $item->created_at->diffForHumans(),
|
|||
|
|
'voted' => $voted,
|
|||
|
|
'replies' => ($item->relationLoaded('replies') ? $item->replies : collect())->map(fn ($r) => [
|
|||
|
|
'id' => $r->id,
|
|||
|
|
'username' => $r->username,
|
|||
|
|
'content' => $r->content,
|
|||
|
|
'is_admin' => $r->is_admin,
|
|||
|
|
'created_at' => $r->created_at->diffForHumans(),
|
|||
|
|
])->values()->toArray(),
|
|||
|
|
];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 批量格式化反馈数据集合
|
|||
|
|
*
|
|||
|
|
* @param \Illuminate\Support\Collection<int, FeedbackItem> $items
|
|||
|
|
* @param array<int> $myVotedIds 当前用户已赞同的 ID 列表
|
|||
|
|
*/
|
|||
|
|
private function formatItems(\Illuminate\Support\Collection $items, array $myVotedIds): array
|
|||
|
|
{
|
|||
|
|
return $items->map(fn (FeedbackItem $item) => $this->formatItem(
|
|||
|
|
$item,
|
|||
|
|
in_array($item->id, $myVotedIds)
|
|||
|
|
))->values()->toArray();
|
|||
|
|
}
|
|||
|
|
}
|