소스 검색

feat:用户端-用户评价

刘学玺 4 달 전
부모
커밋
5368ccbd46

+ 102 - 0
app/Http/Controllers/Client/CommentController.php

@@ -0,0 +1,102 @@
+<?php
+
+namespace App\Http\Controllers\Client;
+
+use App\Http\Controllers\Controller;
+use App\Http\Requests\Client\Comment\CreateRequest;
+use App\Services\Client\CommentService;
+use Illuminate\Http\Request;
+
+class CommentController extends Controller
+{
+    protected CommentService $service;
+
+    public function __construct(CommentService $service)
+    {
+        $this->service = $service;
+    }
+
+    /**
+     * 提交评价
+     *
+     * @group 评价管理
+     *
+     * @bodyParam order_id integer required 订单ID
+     * @bodyParam score integer required 评分(1-5)
+     * @bodyParam content string 评价内容
+     * @bodyParam images array 图片列表
+     * @bodyParam images.* string 图片地址
+     * @bodyParam is_anonymous boolean 是否匿名
+     * @bodyParam tag_ids array 标签ID列表
+     * @bodyParam tag_ids.* integer 标签ID
+     *
+     * @response {
+     *  "code": 0,
+     *  "message": "操作成功",
+     *  "data": {
+     *    "id": 1,
+     *    "message": "评价提交成功"
+     *  }
+     * }
+     */
+    public function store(CreateRequest $request): array
+    {
+        $data = $request->validated();
+
+        return $this->success($this->service->create($data));
+    }
+
+    /**
+     * 获取技师评价列表
+     *
+     * @group 评价管理
+     *
+     * @queryParam coach_id integer required 技师ID
+     * @queryParam score integer 评分筛选(1-5)
+     * @queryParam tag_id integer 标签筛选
+     * @queryParam page integer 页码 Example: 1
+     * @queryParam per_page integer 每页数量 Example: 10
+     *
+     * @response {
+     *  "code": 0,
+     *  "message": "操作成功",
+     *  "data": {
+     *    "total": 100,
+     *    "current_page": 1,
+     *    "last_page": 10,
+     *    "items": [
+     *      {
+     *        "id": 1,
+     *        "score": 5,
+     *        "content": "服务很好",
+     *        "images": ["url1", "url2"],
+     *        "created_at": "2024-01-01 12:00:00",
+     *        "user": {
+     *          "id": 1,
+     *          "nickname": "用户昵称",
+     *          "avatar": "头像地址"
+     *        },
+     *        "tags": [
+     *          {
+     *            "id": 1,
+     *            "name": "标签名称"
+     *          }
+     *        ]
+     *      }
+     *    ]
+     *  }
+     * }
+     */
+    public function index(Request $request): array
+    {
+        $filters = $request->validate([
+            'coach_id' => 'required|integer|exists:coach_users,id',
+            'score' => 'nullable|integer|min:1|max:5',
+            'tag_id' => 'nullable|integer|exists:coach_comment_tags,id',
+            'page' => 'nullable|integer|min:1',
+            'per_page' => 'nullable|integer|min:1|max:50',
+        ]);
+
+        return $this->success($this->service->getCoachComments($filters['coach_id'], $filters));
+    }
+}

+ 84 - 0
app/Http/Requests/Client/Comment/CreateRequest.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace App\Http\Requests\Client\Comment;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class CreateRequest extends FormRequest
+{
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    public function rules(): array
+    {
+        return [
+            'order_id' => [
+                'required',
+                'integer',
+                'exists:order,id',
+            ],
+            'score' => [
+                'required',
+                'integer',
+                'min:1',
+                'max:5',
+            ],
+            'content' => [
+                'nullable',
+                'string',
+                'max:500',
+            ],
+            'images' => [
+                'nullable',
+                'array',
+                'max:9',
+            ],
+            'images.*' => [
+                'required',
+                'string',
+                'max:255',
+            ],
+            'is_anonymous' => [
+                'nullable',
+                'boolean',
+            ],
+            'tag_ids' => [
+                'nullable',
+                'array',
+                'max:5',
+            ],
+            'tag_ids.*' => [
+                'required',
+                'integer',
+                'exists:coach_comment_tags,id',
+            ],
+        ];
+    }
+
+    public function messages(): array
+    {
+        return [
+            'order_id.required' => '订单ID不能为空',
+            'order_id.integer' => '订单ID必须是整数',
+            'order_id.exists' => '订单不存在',
+            'score.required' => '评分不能为空',
+            'score.integer' => '评分必须是整数',
+            'score.min' => '评分最小为1分',
+            'score.max' => '评分最大为5分',
+            'content.max' => '评价内容不能超过500个字符',
+            'images.array' => '图片必须是数组',
+            'images.max' => '最多上传9张图片',
+            'images.*.required' => '图片不能为空',
+            'images.*.string' => '图片必须是字符串',
+            'images.*.max' => '图片地址不能超过255个字符',
+            'is_anonymous.boolean' => '是否匿名必须是布尔值',
+            'tag_ids.array' => '标签必须是数组',
+            'tag_ids.max' => '最多选择5个标签',
+            'tag_ids.*.required' => '标签ID不能为空',
+            'tag_ids.*.integer' => '标签ID必须是整数',
+            'tag_ids.*.exists' => '标签不存在',
+        ];
+    }
+}

+ 23 - 0
app/Models/CoachCommentTag.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class CoachCommentTag extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $fillable = [
+        'name',
+        'code',
+        'weight',
+        'description',
+    ];
+
+    protected $casts = [
+        'weight' => 'integer',
+    ];
+}

+ 65 - 0
app/Models/CoachOrderComment.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Models;
+
+use Illuminate\Database\Eloquent\Factories\HasFactory;
+use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
+
+class CoachOrderComment extends Model
+{
+    use HasFactory, SoftDeletes;
+
+    protected $table = 'coach_order_comments';
+
+    protected $fillable = [
+        'order_id',
+        'user_id',
+        'coach_id',
+        'score',
+        'content',
+        'images',
+        'is_anonymous',
+        'status',
+    ];
+
+    protected $casts = [
+        'images' => 'array',
+        'score' => 'integer',
+        'is_anonymous' => 'boolean',
+        'status' => 'integer',
+    ];
+
+    /**
+     * 获取评价的标签
+     */
+    public function tags()
+    {
+        return $this->belongsToMany(CoachCommentTag::class, 'coach_order_comment_tags', 'comment_id', 'tag_id')
+            ->withTimestamps();
+    }
+
+    /**
+     * 获取评价的用户
+     */
+    public function user()
+    {
+        return $this->belongsTo(MemberUser::class, 'user_id');
+    }
+
+    /**
+     * 获取评价的技师
+     */
+    public function coach()
+    {
+        return $this->belongsTo(CoachUser::class, 'coach_id');
+    }
+
+    /**
+     * 获取评价的订单
+     */
+    public function order()
+    {
+        return $this->belongsTo(Order::class, 'order_id');
+    }
+}

+ 164 - 0
app/Services/Client/CommentService.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace App\Services\Client;
+
+use App\Models\CoachOrderComment;
+use App\Models\CoachStatistic;
+use App\Models\Order;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class CommentService
+{
+    /**
+     * 提交评价
+     */
+    public function create(array $data): array
+    {
+        // 开启事务
+        return DB::transaction(function () use ($data) {
+            // 获取订单信息
+            $order = Order::with('coach')->findOrFail($data['order_id']);
+
+            // 记录订单信息
+            Log::info('订单信息', [
+                'order_id' => $order->id,
+                'coach_id' => $order->coach_id,
+                'order_data' => $order->toArray()
+            ]);
+
+            // 检查订单是否属于当前用户
+            if ($order->user_id !== Auth::id()) {
+                abort(403, '无权评价此订单');
+            }
+
+            // 检查订单是否已评价
+            if (CoachOrderComment::where('order_id', $order->id)->exists()) {
+                abort(400, '订单已评价');
+            }
+
+            // 确保技师ID存在
+            if (empty($order->coach_id)) {
+                abort(400, '订单未关联技师');
+            }
+
+            // 创建评价
+            $comment = CoachOrderComment::create([
+                'order_id' => $order->id,
+                'user_id' => Auth::id(),
+                'coach_id' => $order->coach_id,
+                'score' => $data['score'],
+                'content' => $data['content'] ?? null,
+                'images' => $data['images'] ?? null,
+                'is_anonymous' => $data['is_anonymous'] ?? false,
+                'status' => 1,
+            ]);
+
+            // 关联标签
+            if (!empty($data['tag_ids'])) {
+                $comment->tags()->attach($data['tag_ids']);
+            }
+
+            // 更新技师统计数据
+            $this->updateCoachStatistics($order->coach_id);
+
+            return [
+                'id' => $comment->id,
+                'message' => '评价提交成功',
+            ];
+        });
+    }
+
+    /**
+     * 获取技师评价列表
+     */
+    public function getCoachComments(int $coachId, array $filters = []): array
+    {
+        $query = CoachOrderComment::query()
+            ->where('coach_id', $coachId)
+            ->where('status', 1)
+            ->with(['user', 'tags'])
+            ->latest();
+
+        // 评分筛选
+        if (isset($filters['score'])) {
+            $query->where('score', $filters['score']);
+        }
+
+        // 标签筛选
+        if (!empty($filters['tag_id'])) {
+            $query->whereHas('tags', function ($q) use ($filters) {
+                $q->where('tag_id', $filters['tag_id']);
+            });
+        }
+
+        // 分页获取评价列表
+        $comments = $query->paginate($filters['per_page'] ?? 10);
+
+        return [
+            'total' => $comments->total(),
+            'current_page' => $comments->currentPage(),
+            'last_page' => $comments->lastPage(),
+            'items' => $comments->map(function ($comment) {
+                return [
+                    'id' => $comment->id,
+                    'score' => $comment->score,
+                    'content' => $comment->content,
+                    'images' => $comment->images,
+                    'created_at' => $comment->created_at->format('Y-m-d H:i:s'),
+                    'user' => $comment->is_anonymous ? null : [
+                        'id' => $comment->user->id,
+                        'nickname' => $comment->user->nickname,
+                        'avatar' => $comment->user->avatar,
+                    ],
+                    'tags' => $comment->tags->map(function ($tag) {
+                        return [
+                            'id' => $tag->id,
+                            'name' => $tag->name,
+                        ];
+                    }),
+                ];
+            }),
+        ];
+    }
+
+    /**
+     * 更新技师统计数据
+     */
+    protected function updateCoachStatistics(int $coachId): void
+    {
+        // 获取技师所有评价
+        $comments = CoachOrderComment::where('coach_id', $coachId)
+            ->where('status', 1)
+            ->get();
+
+        // 计算评分统计
+        $totalScore = $comments->sum('score');
+        $commentCount = $comments->count();
+        $avgScore = $commentCount > 0 ? round($totalScore / $commentCount, 2) : 0;
+        $goodCount = $comments->where('score', '>=', 4)->count();
+        $mediumCount = $comments->where('score', '=', 3)->count();
+        $badCount = $comments->where('score', '<=', 2)->count();
+
+        // 计算标签统计
+        $tagCounts = [];
+        $comments->load('tags');
+        foreach ($comments as $comment) {
+            foreach ($comment->tags as $tag) {
+                $tagCounts[$tag->id] = ($tagCounts[$tag->id] ?? 0) + 1;
+            }
+        }
+
+        // 更新或创建统计记录
+        $statistic = CoachStatistic::firstOrNew(['coach_id' => $coachId]);
+        $statistic->updateScoreStatistics(
+            $avgScore,
+            $commentCount,
+            $goodCount,
+            $mediumCount,
+            $badCount
+        );
+        $statistic->updateTagStatistics($tagCounts);
+    }
+}

+ 8 - 1
routes/api.php

@@ -3,6 +3,7 @@
 use App\Http\Controllers\Client\AccountController;
 use App\Http\Controllers\Client\CoachController;
 use App\Http\Controllers\Client\CoachLocationController;
+use App\Http\Controllers\Client\CommentController;
 use App\Http\Controllers\Client\MarketDistTeamController;
 use App\Http\Controllers\Client\OrderController;
 use App\Http\Controllers\Client\ProjectController;
@@ -129,7 +130,7 @@ Route::prefix('client')->group(function () {
         // 钱包相关
         Route::prefix('wallet')->group(function () {
             Route::get('records', [WalletController::class, 'records']);
-            // ���取钱包信息
+            // 取钱包信息
             Route::get('wallet', [WalletController::class, 'wallet']);
             // 提现
             Route::post('withdraw', [WalletController::class, 'withdraw']);
@@ -146,6 +147,12 @@ Route::prefix('client')->group(function () {
             Route::post('callback', [WechatController::class, 'handleCallback']);
         });
 
+        // 评价管理
+        Route::post('comments', [CommentController::class, 'store'])->name('client.comments.store');
+        Route::get('comments', [CommentController::class, 'index'])->name('client.comments.index');
+
+        // 技师分组
+        Route::get('coach-groups', [App\Http\Controllers\Client\CoachGroupController::class, 'index']);
     });
 });