Skip to content

Product Review & Rating System

商品レビュー・評価システムを NENE2 で実装する方法。 1ユーザー1商品1レビューの制約、評価集計(平均・分布)、CRUD を含む。


エンドポイント一覧

MethodPath説明認証
POST/products/{productId}/reviewsレビュー投稿必須
GET/products/{productId}/reviewsレビュー一覧(カーソル)必須
GET/products/{productId}/reviews/summary平均評価・分布必須
PUT/products/{productId}/reviews/{reviewId}レビュー更新本人のみ
DELETE/products/{productId}/reviews/{reviewId}レビュー削除本人のみ

DB スキーマ

sql
CREATE TABLE reviews (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    product_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
    body TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    UNIQUE (product_id, user_id),
    FOREIGN KEY (product_id) REFERENCES products(id),
    FOREIGN KEY (user_id) REFERENCES users(id)
);

1ユーザー1商品1レビューの強制

UNIQUE(product_id, user_id) 制約でデータ層から二重レビューを防ぐ。

アプリ層でも事前チェックし、409 Conflict を返す:

php
if ($this->repository->findByProductAndUser($productId, $userId) !== null) {
    return $this->json->create(['error' => 'already reviewed'], 409);
}

削除後は再投稿可能(UNIQUE 制約が解除されるため)。


評価バリデーション

php
// is_int() で浮動小数点数を拒否(JSONの 4.5 は float → 422)
if (!isset($body['rating']) || !is_int($body['rating'])) {
    $errors[] = new ValidationError('rating', 'Rating must be an integer.', 'required');
} elseif ($body['rating'] < 1 || $body['rating'] > 5) {
    $errors[] = new ValidationError('rating', 'Rating must be between 1 and 5.', 'out_of_range');
}
結果
5 (int)通過
4.5 (float)422
0422
6422
省略422

評価集計(Summary)

sql
-- 合計・平均
SELECT COUNT(*) as total, AVG(rating) as avg_rating
FROM reviews WHERE product_id = ?

-- 星ごとの分布
SELECT COUNT(*) as cnt FROM reviews WHERE product_id = ? AND rating = ?

レスポンス例:

json
GET /products/1/reviews/summary

{
    "total": 150,
    "avg_rating": 4.23,
    "distribution": {
        "1": 5,
        "2": 8,
        "3": 20,
        "4": 52,
        "5": 65
    }
}

レビュー 0 件の場合は "avg_rating": null


所有権チェック(更新・削除)

php
$review = $this->repository->findReviewById($reviewId);
if ($review === null || (int) $review['product_id'] !== $productId) {
    return $this->json->create(['error' => 'review not found'], 404);
}
if ((int) $review['user_id'] !== $userId) {
    return $this->json->create(['error' => 'forbidden'], 403);
}

product_id のクロスチェックにより、他商品のレビューIDを指定した場合も 404 を返す。


カーソルページネーション

GET /products/1/reviews?limit=20
→ { items: [...], next_cursor: 42 }

GET /products/1/reviews?limit=20&before_id=42
→ { items: [...], next_cursor: null }

next_cursor: null で末尾到達。


実装サンプル(FT154)

/home/xi/docker/NENE2-FT/reviewlog/

Released under the MIT License.