fix grades

This commit is contained in:
goro 2026-06-16 19:38:23 +03:00
parent 305beabacb
commit 8b47394e8d
9 changed files with 880 additions and 0 deletions

View File

@ -260,3 +260,82 @@ func (server *Server) getUserReviewByLocation(ctx *gin.Context) {
Images: images, Images: images,
}) })
} }
type getLocationReviewGradesReq struct {
LocationID int32 `uri:"location_id" binding:"required"`
}
type locationReviewGradesGroup struct {
AvgTasteGrade *float64 `json:"avg_taste_grade"`
TasteGradeCount int64 `json:"taste_grade_count"`
AvgComfortGrade *float64 `json:"avg_comfort_grade"`
ComfortGradeCount int64 `json:"comfort_grade_count"`
AvgCleanlinessGrade *float64 `json:"avg_cleanliness_grade"`
CleanlinessGradeCount int64 `json:"cleanliness_grade_count"`
AvgServiceGrade *float64 `json:"avg_service_grade"`
ServiceGradeCount int64 `json:"service_grade_count"`
AvgFacilitiesGrade *float64 `json:"avg_facilities_grade"`
FacilitiesGradeCount int64 `json:"facilities_grade_count"`
}
type getLocationReviewGradesRes struct {
CriticsGrades locationReviewGradesGroup `json:"critics_grades"`
UsersGrades locationReviewGradesGroup `json:"users_grades"`
}
func nullableFloat(v pgtype.Float8) *float64 {
if !v.Valid {
return nil
}
return &v.Float64
}
// @Summary Get aggregated review grades for a location
// @Tags reviews
// @Produce json
// @Param location_id path int true "Location ID"
// @Success 200 {object} getLocationReviewGradesRes
// @Failure 400 {object} map[string]any
// @Failure 500 {object} map[string]any
// @Router /location/{location_id}/reviews/grades [get]
func (server *Server) getLocationReviewGrades(ctx *gin.Context) {
var req getLocationReviewGradesReq
if err := ctx.ShouldBindUri(&req); err != nil {
ctx.JSON(http.StatusBadRequest, ValidationErrorResponse(err))
return
}
grades, err := server.Store.GetLocationReviewGrades(ctx, req.LocationID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, ErrorResponse(err, "Something went wrong while trying to retrieve review grades"))
return
}
ctx.JSON(http.StatusOK, getLocationReviewGradesRes{
CriticsGrades: locationReviewGradesGroup{
AvgTasteGrade: nullableFloat(grades.CriticAvgTasteGrade),
TasteGradeCount: grades.CriticTasteGradeCount,
AvgComfortGrade: nullableFloat(grades.CriticAvgComfortGrade),
ComfortGradeCount: grades.CriticComfortGradeCount,
AvgCleanlinessGrade: nullableFloat(grades.CriticAvgCleanlinessGrade),
CleanlinessGradeCount: grades.CriticCleanlinessGradeCount,
AvgServiceGrade: nullableFloat(grades.CriticAvgServiceGrade),
ServiceGradeCount: grades.CriticServiceGradeCount,
AvgFacilitiesGrade: nullableFloat(grades.CriticAvgFacilitiesGrade),
FacilitiesGradeCount: grades.CriticFacilitiesGradeCount,
},
UsersGrades: locationReviewGradesGroup{
AvgTasteGrade: nullableFloat(grades.UserAvgTasteGrade),
TasteGradeCount: grades.UserTasteGradeCount,
AvgComfortGrade: nullableFloat(grades.UserAvgComfortGrade),
ComfortGradeCount: grades.UserComfortGradeCount,
AvgCleanlinessGrade: nullableFloat(grades.UserAvgCleanlinessGrade),
CleanlinessGradeCount: grades.UserCleanlinessGradeCount,
AvgServiceGrade: nullableFloat(grades.UserAvgServiceGrade),
ServiceGradeCount: grades.UserServiceGradeCount,
AvgFacilitiesGrade: nullableFloat(grades.UserAvgFacilitiesGrade),
FacilitiesGradeCount: grades.UserFacilitiesGradeCount,
},
})
}

View File

@ -91,6 +91,7 @@ func (server *Server) getRoutes() {
router.GET("/location/tags/:location_id", server.getTagsByLocation) router.GET("/location/tags/:location_id", server.getTagsByLocation)
router.GET("/location/reviews", server.getListLocationReviews) router.GET("/location/reviews", server.getListLocationReviews)
router.GET("/location/:location_id/review/:review_id", server.getReview) router.GET("/location/:location_id/review/:review_id", server.getReview)
router.GET("/location/grades/:location_id", server.getLocationReviewGrades)
router.GET("/locations/search", server.searchLocations) router.GET("/locations/search", server.searchLocations)
router.POST("/location/:location_id/visit", server.recordLocationVisit) router.POST("/location/:location_id/visit", server.recordLocationVisit)

BIN
db/.DS_Store vendored

Binary file not shown.

View File

@ -338,6 +338,21 @@ func (mr *MockStoreMockRecorder) GetLocationLifetimeVisits(ctx, locationID any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLocationLifetimeVisits", reflect.TypeOf((*MockStore)(nil).GetLocationLifetimeVisits), ctx, locationID) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLocationLifetimeVisits", reflect.TypeOf((*MockStore)(nil).GetLocationLifetimeVisits), ctx, locationID)
} }
// GetLocationReviewGrades mocks base method.
func (m *MockStore) GetLocationReviewGrades(ctx context.Context, locationID int32) (db.GetLocationReviewGradesRow, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetLocationReviewGrades", ctx, locationID)
ret0, _ := ret[0].(db.GetLocationReviewGradesRow)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetLocationReviewGrades indicates an expected call of GetLocationReviewGrades.
func (mr *MockStoreMockRecorder) GetLocationReviewGrades(ctx, locationID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLocationReviewGrades", reflect.TypeOf((*MockStore)(nil).GetLocationReviewGrades), ctx, locationID)
}
// GetLocationTag mocks base method. // GetLocationTag mocks base method.
func (m *MockStore) GetLocationTag(ctx context.Context, targetID int32) ([]string, error) { func (m *MockStore) GetLocationTag(ctx context.Context, targetID int32) ([]string, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()

View File

@ -146,6 +146,84 @@ func (q *Queries) GetReviewByLocationAndId(ctx context.Context, arg GetReviewByL
return i, err return i, err
} }
type GetLocationReviewGradesRow struct {
CriticAvgTasteGrade pgtype.Float8 `json:"critic_avg_taste_grade"`
CriticTasteGradeCount int64 `json:"critic_taste_grade_count"`
CriticAvgComfortGrade pgtype.Float8 `json:"critic_avg_comfort_grade"`
CriticComfortGradeCount int64 `json:"critic_comfort_grade_count"`
CriticAvgCleanlinessGrade pgtype.Float8 `json:"critic_avg_cleanliness_grade"`
CriticCleanlinessGradeCount int64 `json:"critic_cleanliness_grade_count"`
CriticAvgServiceGrade pgtype.Float8 `json:"critic_avg_service_grade"`
CriticServiceGradeCount int64 `json:"critic_service_grade_count"`
CriticAvgFacilitiesGrade pgtype.Float8 `json:"critic_avg_facilities_grade"`
CriticFacilitiesGradeCount int64 `json:"critic_facilities_grade_count"`
UserAvgTasteGrade pgtype.Float8 `json:"user_avg_taste_grade"`
UserTasteGradeCount int64 `json:"user_taste_grade_count"`
UserAvgComfortGrade pgtype.Float8 `json:"user_avg_comfort_grade"`
UserComfortGradeCount int64 `json:"user_comfort_grade_count"`
UserAvgCleanlinessGrade pgtype.Float8 `json:"user_avg_cleanliness_grade"`
UserCleanlinessGradeCount int64 `json:"user_cleanliness_grade_count"`
UserAvgServiceGrade pgtype.Float8 `json:"user_avg_service_grade"`
UserServiceGradeCount int64 `json:"user_service_grade_count"`
UserAvgFacilitiesGrade pgtype.Float8 `json:"user_avg_facilities_grade"`
UserFacilitiesGradeCount int64 `json:"user_facilities_grade_count"`
}
const getLocationReviewGrades = `
SELECT
AVG(re.taste_grade) FILTER (WHERE re.is_from_critic = true)::float8 AS critic_avg_taste_grade,
COUNT(re.taste_grade) FILTER (WHERE re.is_from_critic = true) AS critic_taste_grade_count,
AVG(re.comfort_grade) FILTER (WHERE re.is_from_critic = true)::float8 AS critic_avg_comfort_grade,
COUNT(re.comfort_grade) FILTER (WHERE re.is_from_critic = true) AS critic_comfort_grade_count,
AVG(re.cleanliness_grade) FILTER (WHERE re.is_from_critic = true)::float8 AS critic_avg_cleanliness_grade,
COUNT(re.cleanliness_grade) FILTER (WHERE re.is_from_critic = true) AS critic_cleanliness_grade_count,
AVG(re.service_grade) FILTER (WHERE re.is_from_critic = true)::float8 AS critic_avg_service_grade,
COUNT(re.service_grade) FILTER (WHERE re.is_from_critic = true) AS critic_service_grade_count,
AVG(re.facilities_grade) FILTER (WHERE re.is_from_critic = true)::float8 AS critic_avg_facilities_grade,
COUNT(re.facilities_grade) FILTER (WHERE re.is_from_critic = true) AS critic_facilities_grade_count,
AVG(re.taste_grade) FILTER (WHERE re.is_from_critic = false)::float8 AS user_avg_taste_grade,
COUNT(re.taste_grade) FILTER (WHERE re.is_from_critic = false) AS user_taste_grade_count,
AVG(re.comfort_grade) FILTER (WHERE re.is_from_critic = false)::float8 AS user_avg_comfort_grade,
COUNT(re.comfort_grade) FILTER (WHERE re.is_from_critic = false) AS user_comfort_grade_count,
AVG(re.cleanliness_grade) FILTER (WHERE re.is_from_critic = false)::float8 AS user_avg_cleanliness_grade,
COUNT(re.cleanliness_grade) FILTER (WHERE re.is_from_critic = false) AS user_cleanliness_grade_count,
AVG(re.service_grade) FILTER (WHERE re.is_from_critic = false)::float8 AS user_avg_service_grade,
COUNT(re.service_grade) FILTER (WHERE re.is_from_critic = false) AS user_service_grade_count,
AVG(re.facilities_grade) FILTER (WHERE re.is_from_critic = false)::float8 AS user_avg_facilities_grade,
COUNT(re.facilities_grade) FILTER (WHERE re.is_from_critic = false) AS user_facilities_grade_count
FROM reviews re
WHERE re.location_id = $1 AND re.is_hided = false;
`
func (q *Queries) GetLocationReviewGrades(ctx context.Context, locationID int32) (GetLocationReviewGradesRow, error) {
row := q.db.QueryRow(ctx, getLocationReviewGrades, locationID)
var i GetLocationReviewGradesRow
err := row.Scan(
&i.CriticAvgTasteGrade,
&i.CriticTasteGradeCount,
&i.CriticAvgComfortGrade,
&i.CriticComfortGradeCount,
&i.CriticAvgCleanlinessGrade,
&i.CriticCleanlinessGradeCount,
&i.CriticAvgServiceGrade,
&i.CriticServiceGradeCount,
&i.CriticAvgFacilitiesGrade,
&i.CriticFacilitiesGradeCount,
&i.UserAvgTasteGrade,
&i.UserTasteGradeCount,
&i.UserAvgComfortGrade,
&i.UserComfortGradeCount,
&i.UserAvgCleanlinessGrade,
&i.UserCleanlinessGradeCount,
&i.UserAvgServiceGrade,
&i.UserServiceGradeCount,
&i.UserAvgFacilitiesGrade,
&i.UserFacilitiesGradeCount,
)
return i, err
}
func (q *Queries) GetListLocationReviews(ctx context.Context, arg GetListLocationReviewsParams) ([]GetListLocationReviewsRow, error) { func (q *Queries) GetListLocationReviews(ctx context.Context, arg GetListLocationReviewsParams) ([]GetListLocationReviewsRow, error) {
rows, err := q.db.Query(ctx, getListLocationReviews, arg.LocationID, arg.IsCritics, arg.Limit, arg.Offset) rows, err := q.db.Query(ctx, getListLocationReviews, arg.LocationID, arg.IsCritics, arg.Limit, arg.Offset)
if err != nil { if err != nil {

View File

@ -29,6 +29,7 @@ type Store interface {
RecordLocationVisit(ctx context.Context, locationID int32) error RecordLocationVisit(ctx context.Context, locationID int32) error
GetLocationLifetimeVisits(ctx context.Context, locationID int32) (int64, error) GetLocationLifetimeVisits(ctx context.Context, locationID int32) (int64, error)
GetTrendingLocations(ctx context.Context, arg GetTrendingLocationsParams) ([]GetTrendingLocationsRow, error) GetTrendingLocations(ctx context.Context, arg GetTrendingLocationsParams) ([]GetTrendingLocationsRow, error)
GetLocationReviewGrades(ctx context.Context, locationID int32) (GetLocationReviewGradesRow, error)
} }
type SQLStore struct { type SQLStore struct {

View File

@ -258,6 +258,88 @@ const docTemplate = `{
} }
} }
}, },
"/location/{location_id}/reviews/grades": {
"get": {
"produces": [
"application/json"
],
"tags": [
"reviews"
],
"summary": "Get aggregated review grades for a location",
"parameters": [
{
"type": "integer",
"description": "Location ID",
"name": "location_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.getLocationReviewGradesRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/location/{location_id}/visit": {
"post": {
"description": "Increments today's visit bucket. A single client (IP) is rate-limited to one visit per location per 30 minutes via Redis to deter trivial F5 inflation.",
"produces": [
"application/json"
],
"tags": [
"locations"
],
"summary": "Record a page visit for a location",
"parameters": [
{
"type": "integer",
"description": "Location ID",
"name": "location_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/locations": { "/locations": {
"get": { "get": {
"produces": [ "produces": [
@ -356,6 +438,12 @@ const docTemplate = `{
"name": "amenities", "name": "amenities",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"description": "Business hours JSON: [{\\",
"name": "business_hours",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"description": "Google Maps link", "description": "Google Maps link",
@ -536,6 +624,104 @@ const docTemplate = `{
} }
} }
}, },
"/locations/trending": {
"get": {
"description": "Ranks approved, non-deleted locations by total page-visits in the trailing window.",
"produces": [
"application/json"
],
"tags": [
"locations"
],
"summary": "Trending locations",
"parameters": [
{
"type": "string",
"description": "Window: week | month | 3month | semester | year",
"name": "window",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Page (min 1)",
"name": "page",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Page size (1-50)",
"name": "page_size",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/menu-items": {
"get": {
"produces": [
"application/json"
],
"tags": [
"menu"
],
"summary": "Get menu items",
"parameters": [
{
"type": "integer",
"description": "Filter by location ID (omit for all)",
"name": "location_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/db.GetMenuItemsRow"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/news-events": { "/news-events": {
"get": { "get": {
"produces": [ "produces": [
@ -1187,6 +1373,84 @@ const docTemplate = `{
"type": "string" "type": "string"
} }
} }
},
"api.getLocationReviewGradesRes": {
"type": "object",
"properties": {
"critics_grades": {
"$ref": "#/definitions/api.locationReviewGradesGroup"
},
"users_grades": {
"$ref": "#/definitions/api.locationReviewGradesGroup"
}
}
},
"api.locationReviewGradesGroup": {
"type": "object",
"properties": {
"avg_cleanliness_grade": {
"type": "number"
},
"avg_comfort_grade": {
"type": "number"
},
"avg_facilities_grade": {
"type": "number"
},
"avg_service_grade": {
"type": "number"
},
"avg_taste_grade": {
"type": "number"
},
"cleanliness_grade_count": {
"type": "integer"
},
"comfort_grade_count": {
"type": "integer"
},
"facilities_grade_count": {
"type": "integer"
},
"service_grade_count": {
"type": "integer"
},
"taste_grade_count": {
"type": "integer"
}
}
},
"db.GetMenuItemsRow": {
"type": "object",
"properties": {
"avg_score": {
"type": "number"
},
"category": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_available": {
"type": "boolean"
},
"location_id": {
"type": "integer"
},
"name": {
"type": "string"
},
"price": {
"type": "integer"
},
"submitted_by": {
"type": "integer"
}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@ -252,6 +252,88 @@
} }
} }
}, },
"/location/{location_id}/reviews/grades": {
"get": {
"produces": [
"application/json"
],
"tags": [
"reviews"
],
"summary": "Get aggregated review grades for a location",
"parameters": [
{
"type": "integer",
"description": "Location ID",
"name": "location_id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/api.getLocationReviewGradesRes"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/location/{location_id}/visit": {
"post": {
"description": "Increments today's visit bucket. A single client (IP) is rate-limited to one visit per location per 30 minutes via Redis to deter trivial F5 inflation.",
"produces": [
"application/json"
],
"tags": [
"locations"
],
"summary": "Record a page visit for a location",
"parameters": [
{
"type": "integer",
"description": "Location ID",
"name": "location_id",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"description": "No Content"
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/locations": { "/locations": {
"get": { "get": {
"produces": [ "produces": [
@ -350,6 +432,12 @@
"name": "amenities", "name": "amenities",
"in": "formData" "in": "formData"
}, },
{
"type": "string",
"description": "Business hours JSON: [{\\",
"name": "business_hours",
"in": "formData"
},
{ {
"type": "string", "type": "string",
"description": "Google Maps link", "description": "Google Maps link",
@ -530,6 +618,104 @@
} }
} }
}, },
"/locations/trending": {
"get": {
"description": "Ranks approved, non-deleted locations by total page-visits in the trailing window.",
"produces": [
"application/json"
],
"tags": [
"locations"
],
"summary": "Trending locations",
"parameters": [
{
"type": "string",
"description": "Window: week | month | 3month | semester | year",
"name": "window",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Page (min 1)",
"name": "page",
"in": "query",
"required": true
},
{
"type": "integer",
"description": "Page size (1-50)",
"name": "page_size",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/menu-items": {
"get": {
"produces": [
"application/json"
],
"tags": [
"menu"
],
"summary": "Get menu items",
"parameters": [
{
"type": "integer",
"description": "Filter by location ID (omit for all)",
"name": "location_id",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/db.GetMenuItemsRow"
}
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
}
},
"/news-events": { "/news-events": {
"get": { "get": {
"produces": [ "produces": [
@ -1181,6 +1367,84 @@
"type": "string" "type": "string"
} }
} }
},
"api.getLocationReviewGradesRes": {
"type": "object",
"properties": {
"critics_grades": {
"$ref": "#/definitions/api.locationReviewGradesGroup"
},
"users_grades": {
"$ref": "#/definitions/api.locationReviewGradesGroup"
}
}
},
"api.locationReviewGradesGroup": {
"type": "object",
"properties": {
"avg_cleanliness_grade": {
"type": "number"
},
"avg_comfort_grade": {
"type": "number"
},
"avg_facilities_grade": {
"type": "number"
},
"avg_service_grade": {
"type": "number"
},
"avg_taste_grade": {
"type": "number"
},
"cleanliness_grade_count": {
"type": "integer"
},
"comfort_grade_count": {
"type": "integer"
},
"facilities_grade_count": {
"type": "integer"
},
"service_grade_count": {
"type": "integer"
},
"taste_grade_count": {
"type": "integer"
}
}
},
"db.GetMenuItemsRow": {
"type": "object",
"properties": {
"avg_score": {
"type": "number"
},
"category": {
"type": "string"
},
"description": {
"type": "string"
},
"id": {
"type": "integer"
},
"is_available": {
"type": "boolean"
},
"location_id": {
"type": "integer"
},
"name": {
"type": "string"
},
"price": {
"type": "integer"
},
"submitted_by": {
"type": "integer"
}
}
} }
}, },
"securityDefinitions": { "securityDefinitions": {

View File

@ -58,6 +58,57 @@ definitions:
- password - password
- username - username
type: object type: object
api.getLocationReviewGradesRes:
properties:
critics_grades:
$ref: '#/definitions/api.locationReviewGradesGroup'
users_grades:
$ref: '#/definitions/api.locationReviewGradesGroup'
type: object
api.locationReviewGradesGroup:
properties:
avg_cleanliness_grade:
type: number
avg_comfort_grade:
type: number
avg_facilities_grade:
type: number
avg_service_grade:
type: number
avg_taste_grade:
type: number
cleanliness_grade_count:
type: integer
comfort_grade_count:
type: integer
facilities_grade_count:
type: integer
service_grade_count:
type: integer
taste_grade_count:
type: integer
type: object
db.GetMenuItemsRow:
properties:
avg_score:
type: number
category:
type: string
description:
type: string
id:
type: integer
is_available:
type: boolean
location_id:
type: integer
name:
type: string
price:
type: integer
submitted_by:
type: integer
type: object
host: localhost:8888 host: localhost:8888
info: info:
contact: {} contact: {}
@ -164,6 +215,62 @@ paths:
summary: Get a single review summary: Get a single review
tags: tags:
- reviews - reviews
/location/{location_id}/reviews/grades:
get:
parameters:
- description: Location ID
in: path
name: location_id
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/api.getLocationReviewGradesRes'
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties: true
type: object
summary: Get aggregated review grades for a location
tags:
- reviews
/location/{location_id}/visit:
post:
description: Increments today's visit bucket. A single client (IP) is rate-limited
to one visit per location per 30 minutes via Redis to deter trivial F5 inflation.
parameters:
- description: Location ID
in: path
name: location_id
required: true
type: integer
produces:
- application/json
responses:
"204":
description: No Content
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties: true
type: object
summary: Record a page visit for a location
tags:
- locations
/location/reviews: /location/reviews:
get: get:
parameters: parameters:
@ -293,6 +400,10 @@ paths:
in: formData in: formData
name: amenities name: amenities
type: string type: string
- description: 'Business hours JSON: [{\'
in: formData
name: business_hours
type: string
- description: Google Maps link - description: Google Maps link
in: formData in: formData
name: google_maps_link name: google_maps_link
@ -416,6 +527,73 @@ paths:
summary: Top-rated locations summary: Top-rated locations
tags: tags:
- locations - locations
/locations/trending:
get:
description: Ranks approved, non-deleted locations by total page-visits in the
trailing window.
parameters:
- description: 'Window: week | month | 3month | semester | year'
in: query
name: window
required: true
type: string
- description: Page (min 1)
in: query
name: page
required: true
type: integer
- description: Page size (1-50)
in: query
name: page_size
required: true
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
additionalProperties: true
type: object
type: array
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
"500":
description: Internal Server Error
schema:
additionalProperties: true
type: object
summary: Trending locations
tags:
- locations
/menu-items:
get:
parameters:
- description: Filter by location ID (omit for all)
in: query
name: location_id
type: integer
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/db.GetMenuItemsRow'
type: array
"400":
description: Bad Request
schema:
additionalProperties: true
type: object
summary: Get menu items
tags:
- menu
/news-events: /news-events:
get: get:
parameters: parameters: