Sfoglia il codice sorgente

评论点赞功能

bianjunhui 4 giorni fa
parent
commit
0840c13367

+ 26 - 0
packages/Webkul/BagistoApi/src/Database/Migrations/2026_08_15_000000_add_like_count_to_product_reviews_table.php

@@ -0,0 +1,26 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('product_reviews', function (Blueprint $table) {
+            if (! Schema::hasColumn('product_reviews', 'like_count')) {
+                $table->unsignedInteger('like_count')->default(0)->after('rating');
+            }
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('product_reviews', function (Blueprint $table) {
+            if (Schema::hasColumn('product_reviews', 'like_count')) {
+                $table->dropColumn('like_count');
+            }
+        });
+    }
+};

+ 66 - 1
packages/Webkul/BagistoApi/src/Models/ProductReview.php

@@ -8,9 +8,11 @@ use ApiPlatform\Metadata\GraphQl\Mutation;
 use ApiPlatform\Metadata\GraphQl\Query;
 use ApiPlatform\Metadata\GraphQl\QueryCollection;
 use ApiPlatform\Metadata\Link;
+use ApiPlatform\Metadata\ApiProperty;
 use Webkul\BagistoApi\Dto\CreateProductReviewInput;
 use Webkul\BagistoApi\Dto\UpdateProductReviewInput;
 use Webkul\BagistoApi\Resolver\BaseQueryItemResolver;
+use Webkul\BagistoApi\State\ProductReviewLikeProcessor;
 use Webkul\BagistoApi\State\ProductReviewProcessor;
 use Webkul\BagistoApi\State\ProductReviewProvider;
 use Webkul\BagistoApi\State\ProductReviewUpdateProvider;
@@ -214,6 +216,51 @@ use ApiPlatform\OpenApi\Model\Operation;
                 ],
             ),
         ),
+        new \ApiPlatform\Metadata\Post(
+            uriTemplate: '/reviews/{id}/like',
+            processor: ProductReviewLikeProcessor::class,
+            denormalizationContext: [
+                'allow_extra_attributes' => true,
+            ],
+            openapi: new Operation(
+                tags: ['Customer Review'],
+                summary: 'Like a product review',
+                description: 'Adds a like to a review. Uses `client_id` (device identifier) to track whether the current visitor has already liked. If already liked, returns an error. Guests can like; `client_id` is a UUID the client generates and stores locally.',
+                requestBody: new \ApiPlatform\OpenApi\Model\RequestBody(
+                    required: true,
+                    content: new \ArrayObject([
+                        'application/json' => [
+                            'schema' => [
+                                'type'       => 'object',
+                                'required'   => ['client_id'],
+                                'properties' => [
+                                    'client_id' => ['type' => 'string', 'example' => '550e8400-e29b-41d4-a716-446655440000', 'description' => 'Device/visitor identifier (UUID generated by client, stored locally)'],
+                                ],
+                            ],
+                            'example' => [
+                                'client_id' => '550e8400-e29b-41d4-a716-446655440000',
+                            ],
+                        ],
+                    ]),
+                ),
+                responses: [
+                    '200' => new \ApiPlatform\OpenApi\Model\Response(
+                        description: 'Like added.',
+                        content: new \ArrayObject([
+                            'application/json' => [
+                                'example' => [
+                                    'id'        => 2,
+                                    'liked'     => true,
+                                    'likeCount' => 42,
+                                ],
+                            ],
+                        ]),
+                    ),
+                    '400' => new \ApiPlatform\OpenApi\Model\Response(description: 'Already liked this review.'),
+                    '404' => new \ApiPlatform\OpenApi\Model\Response(description: 'Review not found.'),
+                ],
+            ),
+        ),
     ],
     graphQlOperations: [
         new QueryCollection(
@@ -312,6 +359,7 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
         'product_id',
         'customer_id',
         'name',
+        'like_count',
     ];
 
     protected $casts = [
@@ -322,6 +370,7 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
         'comment'     => 'string',
         'name'        => 'string',
         'rating'      => 'int',
+        'like_count'  => 'int',
         'status'      => 'string',
         'created_at'  => 'datetime',
         'updated_at'  => 'datetime',
@@ -346,6 +395,22 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
         return $this->getAttribute('id');
     }
 
+    /**
+     * 点赞总数(Eloquen 访问器,snake_case 输出 like_count)
+     */
+    public function getLikeCountAttribute(): int
+    {
+        return (int) ($this->attributes['like_count'] ?? 0);
+    }
+
+    /**
+     * 当前访客是否点赞(Eloquen 访问器,snake_case 输出 liked)
+     */
+    public function getLikedAttribute(): bool
+    {
+        return (bool) ($this->attributes['liked'] ?? false);
+    }
+
     /**
      * Override __isset to ensure isset() works correctly with __get()
      * This is critical for Symfony PropertyAccessor which checks isset() before reading.
@@ -364,7 +429,7 @@ class ProductReview extends \Webkul\Product\Models\ProductReview
      */
     public function __set($key, $value)
     {
-        if (in_array($key, ['id', 'product_id', 'customer_id', 'title', 'comment', 'rating', 'name', 'email', 'status', 'created_at', 'updated_at'])) {
+        if (in_array($key, ['id', 'product_id', 'customer_id', 'title', 'comment', 'rating', 'name', 'email', 'status', 'like_count', 'liked', 'created_at', 'updated_at'])) {
             $this->setAttribute($key, $value);
         } else {
             parent::__set($key, $value);

+ 1 - 0
packages/Webkul/BagistoApi/src/Providers/BagistoApiServiceProvider.php

@@ -185,6 +185,7 @@ class BagistoApiServiceProvider extends ServiceProvider
         $this->app->tag(PaymentCallbackProcessor::class, ProcessorInterface::class);
         $this->app->tag(PaymentReplayProcessor::class, ProcessorInterface::class);
         $this->app->tag(ProductReviewProcessor::class, ProcessorInterface::class);
+        $this->app->tag(\Webkul\BagistoApi\State\ProductReviewLikeProcessor::class, ProcessorInterface::class);
         $this->app->tag(CompareItemProcessor::class, ProcessorInterface::class);
         $this->app->tag(DownloadableProductProcessor::class, ProcessorInterface::class);
         $this->app->tag(NewsletterSubscriptionProcessor::class, ProcessorInterface::class);

+ 71 - 0
packages/Webkul/BagistoApi/src/State/ProductReviewLikeProcessor.php

@@ -0,0 +1,71 @@
+<?php
+
+namespace Webkul\BagistoApi\State;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProcessorInterface;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Redis;
+use Webkul\BagistoApi\Exception\InvalidInputException;
+use Webkul\BagistoApi\Exception\ResourceNotFoundException;
+use Webkul\BagistoApi\Models\ProductReview;
+
+/**
+ * 评论点赞处理(幂等)
+ *
+ * 点赞状态存 Redis(不记录用户,游客也可点赞),数量存 product_reviews.like_count。
+ * - /reviews/{id}/like :点赞(已点过则提示重复,不重复计数)
+ * Redis key: review_like:{review_id}:{client_id},有效期 3 天。
+ */
+class ProductReviewLikeProcessor implements ProcessorInterface
+{
+    /** 点赞状态缓存有效期(秒) */
+    private const TTL = 3 * 24 * 3600;
+
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
+    {
+        $reviewId = $uriVariables['id'] ?? $uriVariables['reviewId'] ?? null;
+        if (! $reviewId) {
+            throw new InvalidInputException('Review id is required');
+        }
+
+        $review = ProductReview::find($reviewId);
+        if (! $review) {
+            throw new ResourceNotFoundException('Review not found');
+        }
+
+        $request = $context['request'] ?? request();
+        $clientId = (string) ($request->input('client_id') ?? $request->input('clientId') ?? '');
+
+        if ($clientId === '') {
+            throw new InvalidInputException('client_id is required');
+        }
+
+        $key = $this->key($reviewId, $clientId);
+
+        // 已点过:返回提示,不重复计数
+        if (Redis::exists($key)) {
+            throw new InvalidInputException('You have already liked this review');
+        }
+
+        // 首次点赞
+        Redis::setex($key, self::TTL, 1);
+        DB::table('product_reviews')->where('id', $reviewId)->increment('like_count');
+
+        $review->refresh();
+
+        return (object) [
+            'id'        => (int) $review->id,
+            'liked'     => true,
+            'likeCount' => (int) $review->like_count,
+        ];
+    }
+
+    /**
+     * 生成 Redis key
+     */
+    private function key(int $reviewId, string $clientId): string
+    {
+        return "review_like:{$reviewId}:{$clientId}";
+    }
+}

+ 10 - 0
packages/Webkul/BagistoApi/src/State/ProductReviewProvider.php

@@ -7,6 +7,7 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\Pagination\Pagination;
 use ApiPlatform\State\ProviderInterface;
 use Illuminate\Pagination\LengthAwarePaginator;
+use Illuminate\Support\Facades\Redis;
 use Webkul\BagistoApi\Models\ProductReview;
 
 /**
@@ -121,6 +122,15 @@ class ProductReviewProvider implements ProviderInterface
 
         $currentPage = $total > 0 ? (int) floor($offset / $perPage) + 1 : 1;
 
+        // 根据 client_id 从 Redis 标记当前访客是否点赞了每条评论
+        $clientId = (string) ($args['client_id'] ?? $args['clientId'] ?? '');
+        if ($clientId !== '') {
+            $items->each(function ($review) use ($clientId) {
+                $key = "review_like:{$review->id}:{$clientId}";
+                $review->setAttribute('liked', (bool) Redis::exists($key));
+            });
+        }
+
         return new Paginator(
             new LengthAwarePaginator(
                 $items,