Skip to content

Produkt-Bewertungs- und Rezensionssystem

Implementierung eines Produkt-Bewertungs- und Rezensionssystems in NENE2. Enthält die Einschränkung 1 Benutzer–1 Produkt–1 Rezension, Bewertungsaggregation (Durchschnitt, Verteilung) und CRUD.


Endpunktübersicht

MethodePfadBeschreibungAuthentifizierung
POST/products/{productId}/reviewsRezension einreichenErforderlich
GET/products/{productId}/reviewsRezensionen auflisten (Cursor)Erforderlich
GET/products/{productId}/reviews/summaryDurchschnittsbewertung und VerteilungErforderlich
PUT/products/{productId}/reviews/{reviewId}Rezension aktualisierenNur Eigentümer
DELETE/products/{productId}/reviews/{reviewId}Rezension löschenNur Eigentümer

DB-Schema

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)
);

Durchsetzung der 1-Benutzer-1-Produkt-1-Rezension-Einschränkung

Der UNIQUE(product_id, user_id)-Constraint verhindert doppelte Rezensionen auf Datenbankebene.

Vorabprüfung auch auf Anwendungsebene mit 409 Conflict:

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

Nach dem Löschen ist eine erneute Einreichung möglich (UNIQUE-Constraint wird aufgehoben).


Bewertungsvalidierung

php
// is_int() lehnt Fließkommazahlen ab (JSON 4.5 ist float → 422)
if (!isset($body['rating']) || !is_int($body['rating'])) {
    $errors[] = new ValidationError('rating', 'Bewertung muss eine Ganzzahl sein.', 'required');
} elseif ($body['rating'] < 1 || $body['rating'] > 5) {
    $errors[] = new ValidationError('rating', 'Bewertung muss zwischen 1 und 5 liegen.', 'out_of_range');
}
WertErgebnis
5 (int)Bestanden
4.5 (float)422
0422
6422
Ausgelassen422

Bewertungsaggregation (Summary)

sql
-- Gesamt und Durchschnitt
SELECT COUNT(*) as total, AVG(rating) as avg_rating
FROM reviews WHERE product_id = ?

-- Verteilung nach Sternanzahl
SELECT COUNT(*) as cnt FROM reviews WHERE product_id = ? AND rating = ?

Beispielantwort:

json
GET /products/1/reviews/summary

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

Bei 0 Rezensionen: "avg_rating": null.


Eigentümerprüfung (Aktualisieren und Löschen)

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

Die Cross-Prüfung der product_id gibt 404 zurück, auch wenn eine Rezensions-ID eines anderen Produkts angegeben wird.


Cursor-Pagination

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 signalisiert das Ende.


Implementierungsbeispiel (FT154)

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

Veröffentlicht unter der MIT-Lizenz.