initial commit

This commit is contained in:
2025-11-29 00:28:21 +05:00
parent 46229acc82
commit ec3b03a935
76 changed files with 13492 additions and 0 deletions

View File

@@ -0,0 +1,548 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: auth.sql
package database
import (
"context"
"net/netip"
"github.com/jackc/pgx/v5/pgtype"
)
const CleanupExpiredSessions = `-- name: CleanupExpiredSessions :exec
DELETE FROM user_sessions
WHERE expires_at < CURRENT_TIMESTAMP
OR last_activity_at < CURRENT_TIMESTAMP - INTERVAL '7 days'
`
func (q *Queries) CleanupExpiredSessions(ctx context.Context) error {
_, err := q.db.Exec(ctx, CleanupExpiredSessions)
return err
}
const CleanupExpiredTokens = `-- name: CleanupExpiredTokens :exec
DELETE FROM refresh_tokens
WHERE expires_at < CURRENT_TIMESTAMP
OR (revoked = TRUE AND revoked_at < CURRENT_TIMESTAMP - INTERVAL '30 days')
`
func (q *Queries) CleanupExpiredTokens(ctx context.Context) error {
_, err := q.db.Exec(ctx, CleanupExpiredTokens)
return err
}
const CreateRefreshToken = `-- name: CreateRefreshToken :one
INSERT INTO refresh_tokens (
user_id,
token,
expires_at,
user_agent,
ip_address
) VALUES (
$1,
$2,
$3,
$4,
$5
) RETURNING id, user_id, token, expires_at, user_agent, ip_address, revoked, revoked_at, created_at
`
type CreateRefreshTokenParams struct {
UserID int64 `json:"user_id"`
Token string `json:"token"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
UserAgent pgtype.Text `json:"user_agent"`
IpAddress *netip.Addr `json:"ip_address"`
}
// ============================================================================
// Refresh Tokens
// ============================================================================
func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error) {
row := q.db.QueryRow(ctx, CreateRefreshToken,
arg.UserID,
arg.Token,
arg.ExpiresAt,
arg.UserAgent,
arg.IpAddress,
)
var i RefreshToken
err := row.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.ExpiresAt,
&i.UserAgent,
&i.IpAddress,
&i.Revoked,
&i.RevokedAt,
&i.CreatedAt,
)
return i, err
}
const CreateUser = `-- name: CreateUser :one
INSERT INTO users (
email,
phone,
password_hash,
first_name,
last_name,
location,
address,
city
) VALUES (
$1,
$2,
$3,
$4,
$5,
ST_SetSRID(ST_MakePoint($6, $7), 4326)::geography,
$8,
$9
) RETURNING
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
`
type CreateUserParams struct {
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
PasswordHash string `json:"password_hash"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
StMakepoint interface{} `json:"st_makepoint"`
StMakepoint_2 interface{} `json:"st_makepoint_2"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
}
type CreateUserRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
PasswordHash string `json:"password_hash"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
IsBlocked pgtype.Bool `json:"is_blocked"`
EmailVerified pgtype.Bool `json:"email_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
// Фаза 1A: Аутентификация (КРИТИЧНО)
// Запросы для регистрации, входа и управления токенами
// ============================================================================
// Пользователи
// ============================================================================
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRow(ctx, CreateUser,
arg.Email,
arg.Phone,
arg.PasswordHash,
arg.FirstName,
arg.LastName,
arg.StMakepoint,
arg.StMakepoint_2,
arg.Address,
arg.City,
)
var i CreateUserRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Phone,
&i.PasswordHash,
&i.FirstName,
&i.LastName,
&i.AvatarUrl,
&i.Latitude,
&i.Longitude,
&i.Address,
&i.City,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.IsVerified,
&i.IsBlocked,
&i.EmailVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastLoginAt,
&i.DeletedAt,
)
return i, err
}
const CreateUserSession = `-- name: CreateUserSession :one
INSERT INTO user_sessions (
user_id,
session_token,
refresh_token_id,
expires_at,
user_agent,
ip_address,
device_info
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
) RETURNING id, user_id, session_token, refresh_token_id, expires_at, last_activity_at, user_agent, ip_address, device_info, created_at
`
type CreateUserSessionParams struct {
UserID int64 `json:"user_id"`
SessionToken string `json:"session_token"`
RefreshTokenID pgtype.Int8 `json:"refresh_token_id"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
UserAgent pgtype.Text `json:"user_agent"`
IpAddress *netip.Addr `json:"ip_address"`
DeviceInfo []byte `json:"device_info"`
}
// ============================================================================
// User Sessions
// ============================================================================
func (q *Queries) CreateUserSession(ctx context.Context, arg CreateUserSessionParams) (UserSession, error) {
row := q.db.QueryRow(ctx, CreateUserSession,
arg.UserID,
arg.SessionToken,
arg.RefreshTokenID,
arg.ExpiresAt,
arg.UserAgent,
arg.IpAddress,
arg.DeviceInfo,
)
var i UserSession
err := row.Scan(
&i.ID,
&i.UserID,
&i.SessionToken,
&i.RefreshTokenID,
&i.ExpiresAt,
&i.LastActivityAt,
&i.UserAgent,
&i.IpAddress,
&i.DeviceInfo,
&i.CreatedAt,
)
return i, err
}
const EmailExists = `-- name: EmailExists :one
SELECT EXISTS(
SELECT 1 FROM users
WHERE email = $1 AND deleted_at IS NULL
)
`
func (q *Queries) EmailExists(ctx context.Context, email string) (bool, error) {
row := q.db.QueryRow(ctx, EmailExists, email)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const GetRefreshToken = `-- name: GetRefreshToken :one
SELECT id, user_id, token, expires_at, user_agent, ip_address, revoked, revoked_at, created_at FROM refresh_tokens
WHERE token = $1
AND revoked = FALSE
AND expires_at > CURRENT_TIMESTAMP
`
func (q *Queries) GetRefreshToken(ctx context.Context, token string) (RefreshToken, error) {
row := q.db.QueryRow(ctx, GetRefreshToken, token)
var i RefreshToken
err := row.Scan(
&i.ID,
&i.UserID,
&i.Token,
&i.ExpiresAt,
&i.UserAgent,
&i.IpAddress,
&i.Revoked,
&i.RevokedAt,
&i.CreatedAt,
)
return i, err
}
const GetUserByEmail = `-- name: GetUserByEmail :one
SELECT
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
FROM users
WHERE email = $1 AND deleted_at IS NULL
`
type GetUserByEmailRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
PasswordHash string `json:"password_hash"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
IsBlocked pgtype.Bool `json:"is_blocked"`
EmailVerified pgtype.Bool `json:"email_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
func (q *Queries) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) {
row := q.db.QueryRow(ctx, GetUserByEmail, email)
var i GetUserByEmailRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Phone,
&i.PasswordHash,
&i.FirstName,
&i.LastName,
&i.AvatarUrl,
&i.Latitude,
&i.Longitude,
&i.Address,
&i.City,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.IsVerified,
&i.IsBlocked,
&i.EmailVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastLoginAt,
&i.DeletedAt,
)
return i, err
}
const GetUserByID = `-- name: GetUserByID :one
SELECT
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
FROM users
WHERE id = $1 AND deleted_at IS NULL
`
type GetUserByIDRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
PasswordHash string `json:"password_hash"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
IsBlocked pgtype.Bool `json:"is_blocked"`
EmailVerified pgtype.Bool `json:"email_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
func (q *Queries) GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error) {
row := q.db.QueryRow(ctx, GetUserByID, id)
var i GetUserByIDRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Phone,
&i.PasswordHash,
&i.FirstName,
&i.LastName,
&i.AvatarUrl,
&i.Latitude,
&i.Longitude,
&i.Address,
&i.City,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.IsVerified,
&i.IsBlocked,
&i.EmailVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastLoginAt,
&i.DeletedAt,
)
return i, err
}
const GetUserSession = `-- name: GetUserSession :one
SELECT id, user_id, session_token, refresh_token_id, expires_at, last_activity_at, user_agent, ip_address, device_info, created_at FROM user_sessions
WHERE session_token = $1
AND expires_at > CURRENT_TIMESTAMP
`
func (q *Queries) GetUserSession(ctx context.Context, sessionToken string) (UserSession, error) {
row := q.db.QueryRow(ctx, GetUserSession, sessionToken)
var i UserSession
err := row.Scan(
&i.ID,
&i.UserID,
&i.SessionToken,
&i.RefreshTokenID,
&i.ExpiresAt,
&i.LastActivityAt,
&i.UserAgent,
&i.IpAddress,
&i.DeviceInfo,
&i.CreatedAt,
)
return i, err
}
const InvalidateAllUserSessions = `-- name: InvalidateAllUserSessions :exec
DELETE FROM user_sessions
WHERE user_id = $1
`
func (q *Queries) InvalidateAllUserSessions(ctx context.Context, userID int64) error {
_, err := q.db.Exec(ctx, InvalidateAllUserSessions, userID)
return err
}
const InvalidateUserSession = `-- name: InvalidateUserSession :exec
DELETE FROM user_sessions
WHERE id = $1
`
func (q *Queries) InvalidateUserSession(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, InvalidateUserSession, id)
return err
}
const RevokeAllUserTokens = `-- name: RevokeAllUserTokens :exec
UPDATE refresh_tokens
SET revoked = TRUE, revoked_at = CURRENT_TIMESTAMP
WHERE user_id = $1 AND revoked = FALSE
`
func (q *Queries) RevokeAllUserTokens(ctx context.Context, userID int64) error {
_, err := q.db.Exec(ctx, RevokeAllUserTokens, userID)
return err
}
const RevokeRefreshToken = `-- name: RevokeRefreshToken :exec
UPDATE refresh_tokens
SET revoked = TRUE, revoked_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) RevokeRefreshToken(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, RevokeRefreshToken, id)
return err
}
const UpdateLastLogin = `-- name: UpdateLastLogin :exec
UPDATE users
SET last_login_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) UpdateLastLogin(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, UpdateLastLogin, id)
return err
}
const UpdateSessionActivity = `-- name: UpdateSessionActivity :exec
UPDATE user_sessions
SET last_activity_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) UpdateSessionActivity(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, UpdateSessionActivity, id)
return err
}

32
internal/database/db.go Normal file
View File

@@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -0,0 +1,43 @@
package database
import (
"database/sql/driver"
"fmt"
)
// GeographyPoint представляет PostGIS GEOGRAPHY(POINT) в WGS84
// Для Sprint 1 мы используем ST_X() и ST_Y() в SELECT запросах,
// поэтому этот тип используется только для INSERT операций
type GeographyPoint struct {
Longitude float64
Latitude float64
Valid bool
}
func (g *GeographyPoint) Scan(value interface{}) error {
if value == nil {
g.Valid = false
return nil
}
// В Sprint 1 мы не используем Scan, так как извлекаем координаты через ST_X/ST_Y
// Для production: использовать github.com/twpayne/go-geom для полноценного парсинга
return fmt.Errorf("GeographyPoint.Scan not implemented - use ST_X/ST_Y in queries")
}
// Value реализует driver.Valuer для использования в INSERT/UPDATE запросах
func (g GeographyPoint) Value() (driver.Value, error) {
if !g.Valid {
return nil, nil
}
// Возвращаем WKT формат с SRID для PostGIS
return fmt.Sprintf("SRID=4326;POINT(%f %f)", g.Longitude, g.Latitude), nil
}
// NewGeographyPoint создает новую точку с координатами
func NewGeographyPoint(lon, lat float64) *GeographyPoint {
return &GeographyPoint{
Longitude: lon,
Latitude: lat,
Valid: true,
}
}

View File

@@ -0,0 +1,412 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: geospatial.sql
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CountRequestsNearby = `-- name: CountRequestsNearby :one
SELECT COUNT(*) FROM requests r
WHERE r.deleted_at IS NULL
AND r.status = $3
AND ST_DWithin(
r.location,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography,
$4
)
`
type CountRequestsNearbyParams struct {
StMakepoint interface{} `json:"st_makepoint"`
StMakepoint_2 interface{} `json:"st_makepoint_2"`
Status NullRequestStatus `json:"status"`
StDwithin interface{} `json:"st_dwithin"`
}
// ============================================================================
// Подсчет заявок поблизости
// ============================================================================
func (q *Queries) CountRequestsNearby(ctx context.Context, arg CountRequestsNearbyParams) (int64, error) {
row := q.db.QueryRow(ctx, CountRequestsNearby,
arg.StMakepoint,
arg.StMakepoint_2,
arg.Status,
arg.StDwithin,
)
var count int64
err := row.Scan(&count)
return count, err
}
const FindNearestRequestsForVolunteer = `-- name: FindNearestRequestsForVolunteer :many
SELECT
r.id,
r.title,
r.description,
r.urgency,
r.status,
r.created_at,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
ST_Distance(
r.location,
(SELECT u.location FROM users u WHERE u.id = $1)
) as distance_meters,
rt.name as request_type_name,
rt.icon as request_type_icon
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
WHERE r.deleted_at IS NULL
AND r.status = 'approved'
AND r.assigned_volunteer_id IS NULL
AND ST_DWithin(
r.location,
(SELECT u.location FROM users u WHERE u.id = $1),
$2
)
ORDER BY
CASE r.urgency
WHEN 'urgent' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
ELSE 4
END,
distance_meters
LIMIT $3
`
type FindNearestRequestsForVolunteerParams struct {
ID int64 `json:"id"`
StDwithin interface{} `json:"st_dwithin"`
Limit int32 `json:"limit"`
}
type FindNearestRequestsForVolunteerRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Urgency pgtype.Text `json:"urgency"`
Status NullRequestStatus `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
DistanceMeters interface{} `json:"distance_meters"`
RequestTypeName string `json:"request_type_name"`
RequestTypeIcon pgtype.Text `json:"request_type_icon"`
}
// ============================================================================
// Поиск ближайших заявок для волонтера
// ============================================================================
func (q *Queries) FindNearestRequestsForVolunteer(ctx context.Context, arg FindNearestRequestsForVolunteerParams) ([]FindNearestRequestsForVolunteerRow, error) {
rows, err := q.db.Query(ctx, FindNearestRequestsForVolunteer, arg.ID, arg.StDwithin, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FindNearestRequestsForVolunteerRow{}
for rows.Next() {
var i FindNearestRequestsForVolunteerRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Urgency,
&i.Status,
&i.CreatedAt,
&i.Latitude,
&i.Longitude,
&i.DistanceMeters,
&i.RequestTypeName,
&i.RequestTypeIcon,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const FindRequestsInBounds = `-- name: FindRequestsInBounds :many
SELECT
r.id,
r.title,
r.urgency,
r.status,
r.created_at,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
rt.icon as request_type_icon,
rt.name as request_type_name
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
WHERE r.deleted_at IS NULL
AND r.status::text = ANY($1::text[])
AND ST_Within(
r.location::geometry,
ST_MakeEnvelope($2, $3, $4, $5, 4326)
)
ORDER BY r.created_at DESC
LIMIT 200
`
type FindRequestsInBoundsParams struct {
Column1 []string `json:"column_1"`
StMakeenvelope interface{} `json:"st_makeenvelope"`
StMakeenvelope_2 interface{} `json:"st_makeenvelope_2"`
StMakeenvelope_3 interface{} `json:"st_makeenvelope_3"`
StMakeenvelope_4 interface{} `json:"st_makeenvelope_4"`
}
type FindRequestsInBoundsRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Urgency pgtype.Text `json:"urgency"`
Status NullRequestStatus `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
RequestTypeIcon pgtype.Text `json:"request_type_icon"`
RequestTypeName string `json:"request_type_name"`
}
// ============================================================================
// Поиск заявок в прямоугольной области (для карты)
// ============================================================================
func (q *Queries) FindRequestsInBounds(ctx context.Context, arg FindRequestsInBoundsParams) ([]FindRequestsInBoundsRow, error) {
rows, err := q.db.Query(ctx, FindRequestsInBounds,
arg.Column1,
arg.StMakeenvelope,
arg.StMakeenvelope_2,
arg.StMakeenvelope_3,
arg.StMakeenvelope_4,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FindRequestsInBoundsRow{}
for rows.Next() {
var i FindRequestsInBoundsRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Urgency,
&i.Status,
&i.CreatedAt,
&i.Latitude,
&i.Longitude,
&i.RequestTypeIcon,
&i.RequestTypeName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const FindRequestsNearby = `-- name: FindRequestsNearby :many
SELECT
r.id,
r.title,
r.description,
r.address,
r.city,
r.urgency,
r.status,
r.created_at,
r.desired_completion_date,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
ST_Distance(
r.location,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography
) as distance_meters,
rt.name as request_type_name,
rt.icon as request_type_icon,
(u.first_name || ' ' || u.last_name) as requester_name
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
JOIN users u ON u.id = r.requester_id
WHERE r.deleted_at IS NULL
AND r.status::text = ANY($3::text[])
AND ST_DWithin(
r.location,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography,
$4
)
ORDER BY distance_meters
LIMIT $5 OFFSET $6
`
type FindRequestsNearbyParams struct {
StMakepoint interface{} `json:"st_makepoint"`
StMakepoint_2 interface{} `json:"st_makepoint_2"`
Column3 []string `json:"column_3"`
StDwithin interface{} `json:"st_dwithin"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type FindRequestsNearbyRow struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Address string `json:"address"`
City pgtype.Text `json:"city"`
Urgency pgtype.Text `json:"urgency"`
Status NullRequestStatus `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
DesiredCompletionDate pgtype.Timestamptz `json:"desired_completion_date"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
DistanceMeters interface{} `json:"distance_meters"`
RequestTypeName string `json:"request_type_name"`
RequestTypeIcon pgtype.Text `json:"request_type_icon"`
RequesterName interface{} `json:"requester_name"`
}
// Фаза 2B: Геопространственные запросы (ВЫСОКИЙ ПРИОРИТЕТ)
// PostGIS запросы для поиска заявок по геолокации
// ============================================================================
// Поиск заявок рядом с точкой
// ============================================================================
func (q *Queries) FindRequestsNearby(ctx context.Context, arg FindRequestsNearbyParams) ([]FindRequestsNearbyRow, error) {
rows, err := q.db.Query(ctx, FindRequestsNearby,
arg.StMakepoint,
arg.StMakepoint_2,
arg.Column3,
arg.StDwithin,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FindRequestsNearbyRow{}
for rows.Next() {
var i FindRequestsNearbyRow
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.Address,
&i.City,
&i.Urgency,
&i.Status,
&i.CreatedAt,
&i.DesiredCompletionDate,
&i.Latitude,
&i.Longitude,
&i.DistanceMeters,
&i.RequestTypeName,
&i.RequestTypeIcon,
&i.RequesterName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const FindVolunteersNearRequest = `-- name: FindVolunteersNearRequest :many
SELECT
u.id,
(u.first_name || ' ' || u.last_name) as full_name,
u.avatar_url,
u.volunteer_rating,
u.completed_requests_count,
ST_Y(u.location::geometry) as latitude,
ST_X(u.location::geometry) as longitude,
ST_Distance(
u.location,
(SELECT req.location FROM requests req WHERE req.id = $1)
) as distance_meters
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN roles r ON r.id = ur.role_id
WHERE r.name = 'volunteer'
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
AND u.location IS NOT NULL
AND ST_DWithin(
u.location,
(SELECT req.location FROM requests req WHERE req.id = $1),
$2
)
ORDER BY distance_meters
LIMIT $3
`
type FindVolunteersNearRequestParams struct {
ID int64 `json:"id"`
StDwithin interface{} `json:"st_dwithin"`
Limit int32 `json:"limit"`
}
type FindVolunteersNearRequestRow struct {
ID int64 `json:"id"`
FullName interface{} `json:"full_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
DistanceMeters interface{} `json:"distance_meters"`
}
// ============================================================================
// Поиск волонтеров рядом с заявкой
// ============================================================================
func (q *Queries) FindVolunteersNearRequest(ctx context.Context, arg FindVolunteersNearRequestParams) ([]FindVolunteersNearRequestRow, error) {
rows, err := q.db.Query(ctx, FindVolunteersNearRequest, arg.ID, arg.StDwithin, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
items := []FindVolunteersNearRequestRow{}
for rows.Next() {
var i FindVolunteersNearRequestRow
if err := rows.Scan(
&i.ID,
&i.FullName,
&i.AvatarUrl,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.Latitude,
&i.Longitude,
&i.DistanceMeters,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

530
internal/database/models.go Normal file
View File

@@ -0,0 +1,530 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"database/sql/driver"
"fmt"
"net/netip"
"github.com/jackc/pgx/v5/pgtype"
)
// Статусы жизненного цикла жалобы
type ComplaintStatus string
const (
ComplaintStatusPending ComplaintStatus = "pending"
ComplaintStatusInReview ComplaintStatus = "in_review"
ComplaintStatusResolved ComplaintStatus = "resolved"
ComplaintStatusRejected ComplaintStatus = "rejected"
)
func (e *ComplaintStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ComplaintStatus(s)
case string:
*e = ComplaintStatus(s)
default:
return fmt.Errorf("unsupported scan type for ComplaintStatus: %T", src)
}
return nil
}
type NullComplaintStatus struct {
ComplaintStatus ComplaintStatus `json:"complaint_status"`
Valid bool `json:"valid"` // Valid is true if ComplaintStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullComplaintStatus) Scan(value interface{}) error {
if value == nil {
ns.ComplaintStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ComplaintStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullComplaintStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ComplaintStatus), nil
}
func AllComplaintStatusValues() []ComplaintStatus {
return []ComplaintStatus{
ComplaintStatusPending,
ComplaintStatusInReview,
ComplaintStatusResolved,
ComplaintStatusRejected,
}
}
// Типы жалоб на пользователей
type ComplaintType string
const (
ComplaintTypeInappropriateBehavior ComplaintType = "inappropriate_behavior"
ComplaintTypeNoShow ComplaintType = "no_show"
ComplaintTypeFraud ComplaintType = "fraud"
ComplaintTypeSpam ComplaintType = "spam"
ComplaintTypeOther ComplaintType = "other"
)
func (e *ComplaintType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ComplaintType(s)
case string:
*e = ComplaintType(s)
default:
return fmt.Errorf("unsupported scan type for ComplaintType: %T", src)
}
return nil
}
type NullComplaintType struct {
ComplaintType ComplaintType `json:"complaint_type"`
Valid bool `json:"valid"` // Valid is true if ComplaintType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullComplaintType) Scan(value interface{}) error {
if value == nil {
ns.ComplaintType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ComplaintType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullComplaintType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ComplaintType), nil
}
func AllComplaintTypeValues() []ComplaintType {
return []ComplaintType{
ComplaintTypeInappropriateBehavior,
ComplaintTypeNoShow,
ComplaintTypeFraud,
ComplaintTypeSpam,
ComplaintTypeOther,
}
}
// Типы действий модераторов для аудита
type ModeratorActionType string
const (
ModeratorActionTypeApproveRequest ModeratorActionType = "approve_request"
ModeratorActionTypeRejectRequest ModeratorActionType = "reject_request"
ModeratorActionTypeBlockUser ModeratorActionType = "block_user"
ModeratorActionTypeUnblockUser ModeratorActionType = "unblock_user"
ModeratorActionTypeResolveComplaint ModeratorActionType = "resolve_complaint"
ModeratorActionTypeRejectComplaint ModeratorActionType = "reject_complaint"
ModeratorActionTypeEditRequest ModeratorActionType = "edit_request"
ModeratorActionTypeDeleteRequest ModeratorActionType = "delete_request"
)
func (e *ModeratorActionType) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ModeratorActionType(s)
case string:
*e = ModeratorActionType(s)
default:
return fmt.Errorf("unsupported scan type for ModeratorActionType: %T", src)
}
return nil
}
type NullModeratorActionType struct {
ModeratorActionType ModeratorActionType `json:"moderator_action_type"`
Valid bool `json:"valid"` // Valid is true if ModeratorActionType is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullModeratorActionType) Scan(value interface{}) error {
if value == nil {
ns.ModeratorActionType, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ModeratorActionType.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullModeratorActionType) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ModeratorActionType), nil
}
func AllModeratorActionTypeValues() []ModeratorActionType {
return []ModeratorActionType{
ModeratorActionTypeApproveRequest,
ModeratorActionTypeRejectRequest,
ModeratorActionTypeBlockUser,
ModeratorActionTypeUnblockUser,
ModeratorActionTypeResolveComplaint,
ModeratorActionTypeRejectComplaint,
ModeratorActionTypeEditRequest,
ModeratorActionTypeDeleteRequest,
}
}
// Статусы жизненного цикла заявки на помощь
type RequestStatus string
const (
RequestStatusPendingModeration RequestStatus = "pending_moderation"
RequestStatusApproved RequestStatus = "approved"
RequestStatusInProgress RequestStatus = "in_progress"
RequestStatusCompleted RequestStatus = "completed"
RequestStatusCancelled RequestStatus = "cancelled"
RequestStatusRejected RequestStatus = "rejected"
)
func (e *RequestStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = RequestStatus(s)
case string:
*e = RequestStatus(s)
default:
return fmt.Errorf("unsupported scan type for RequestStatus: %T", src)
}
return nil
}
type NullRequestStatus struct {
RequestStatus RequestStatus `json:"request_status"`
Valid bool `json:"valid"` // Valid is true if RequestStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullRequestStatus) Scan(value interface{}) error {
if value == nil {
ns.RequestStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.RequestStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullRequestStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.RequestStatus), nil
}
func AllRequestStatusValues() []RequestStatus {
return []RequestStatus{
RequestStatusPendingModeration,
RequestStatusApproved,
RequestStatusInProgress,
RequestStatusCompleted,
RequestStatusCancelled,
RequestStatusRejected,
}
}
// Статусы отклика волонтёра на заявку
type ResponseStatus string
const (
ResponseStatusPending ResponseStatus = "pending"
ResponseStatusAccepted ResponseStatus = "accepted"
ResponseStatusRejected ResponseStatus = "rejected"
ResponseStatusCancelled ResponseStatus = "cancelled"
)
func (e *ResponseStatus) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = ResponseStatus(s)
case string:
*e = ResponseStatus(s)
default:
return fmt.Errorf("unsupported scan type for ResponseStatus: %T", src)
}
return nil
}
type NullResponseStatus struct {
ResponseStatus ResponseStatus `json:"response_status"`
Valid bool `json:"valid"` // Valid is true if ResponseStatus is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullResponseStatus) Scan(value interface{}) error {
if value == nil {
ns.ResponseStatus, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.ResponseStatus.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullResponseStatus) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.ResponseStatus), nil
}
func AllResponseStatusValues() []ResponseStatus {
return []ResponseStatus{
ResponseStatusPending,
ResponseStatusAccepted,
ResponseStatusRejected,
ResponseStatusCancelled,
}
}
// Жалобы пользователей друг на друга
type Complaint struct {
ID int64 `json:"id"`
// Пользователь, подающий жалобу
ComplainantID int64 `json:"complainant_id"`
// Пользователь, на которого жалуются
DefendantID int64 `json:"defendant_id"`
RequestID pgtype.Int8 `json:"request_id"`
Type ComplaintType `json:"type"`
Title string `json:"title"`
Description string `json:"description"`
Status NullComplaintStatus `json:"status"`
ModeratorID pgtype.Int8 `json:"moderator_id"`
ModeratorComment pgtype.Text `json:"moderator_comment"`
ResolvedAt pgtype.Timestamptz `json:"resolved_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
// Полный аудит всех действий модераторов в системе
type ModeratorAction struct {
ID int64 `json:"id"`
ModeratorID int64 `json:"moderator_id"`
ActionType ModeratorActionType `json:"action_type"`
TargetUserID pgtype.Int8 `json:"target_user_id"`
TargetRequestID pgtype.Int8 `json:"target_request_id"`
TargetComplaintID pgtype.Int8 `json:"target_complaint_id"`
Comment pgtype.Text `json:"comment"`
// Дополнительные данные в JSON (изменённые поля, причины и т.д.)
Metadata []byte `json:"metadata"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Справочник разрешений для RBAC системы
type Permission struct {
ID int64 `json:"id"`
Name string `json:"name"`
// Ресурс: request, user, complaint и т.д.
Resource string `json:"resource"`
// Действие: create, read, update, delete, moderate
Action string `json:"action"`
Description pgtype.Text `json:"description"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Рейтинги волонтёров за выполненную помощь
type Rating struct {
ID int64 `json:"id"`
// Связь с откликом (один рейтинг на один отклик)
VolunteerResponseID int64 `json:"volunteer_response_id"`
// Денормализация для быстрого доступа
VolunteerID int64 `json:"volunteer_id"`
// Кто оставил рейтинг
RequesterID int64 `json:"requester_id"`
RequestID int64 `json:"request_id"`
// Оценка от 1 до 5 звёзд
Rating int32 `json:"rating"`
Comment pgtype.Text `json:"comment"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
// Refresh токены для JWT аутентификации
type RefreshToken struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
// Хеш refresh токена
Token string `json:"token"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
UserAgent pgtype.Text `json:"user_agent"`
IpAddress *netip.Addr `json:"ip_address"`
// Токен отозван (для принудительного логаута)
Revoked pgtype.Bool `json:"revoked"`
RevokedAt pgtype.Timestamptz `json:"revoked_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Заявки на помощь от маломобильных граждан
type Request struct {
ID int64 `json:"id"`
RequesterID int64 `json:"requester_id"`
RequestTypeID int64 `json:"request_type_id"`
// Волонтёр, который взял заявку в работу
AssignedVolunteerID pgtype.Int8 `json:"assigned_volunteer_id"`
Title string `json:"title"`
Description string `json:"description"`
// Координаты места, где нужна помощь (WGS84, SRID 4326)
Location interface{} `json:"location"`
Address string `json:"address"`
City pgtype.Text `json:"city"`
DesiredCompletionDate pgtype.Timestamptz `json:"desired_completion_date"`
// Срочность: low, medium, high, urgent
Urgency pgtype.Text `json:"urgency"`
Status NullRequestStatus `json:"status"`
ModerationComment pgtype.Text `json:"moderation_comment"`
ModeratedBy pgtype.Int8 `json:"moderated_by"`
ModeratedAt pgtype.Timestamptz `json:"moderated_at"`
ContactPhone pgtype.Text `json:"contact_phone"`
// Дополнительная информация: код домофона, этаж и т.д.
ContactNotes pgtype.Text `json:"contact_notes"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
// Soft delete - дата удаления заявки
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
// Полная история изменения статусов заявок для аудита
type RequestStatusHistory struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
// Предыдущий статус (NULL при создании)
FromStatus NullRequestStatus `json:"from_status"`
ToStatus RequestStatus `json:"to_status"`
ChangedBy int64 `json:"changed_by"`
Comment pgtype.Text `json:"comment"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Справочник типов помощи (продукты, медикаменты, техника)
type RequestType struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
// Название иконки для UI
Icon pgtype.Text `json:"icon"`
// Активность типа (для скрытия без удаления)
IsActive pgtype.Bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Справочник ролей для RBAC системы
type Role struct {
ID int64 `json:"id"`
// Уникальное название роли
Name string `json:"name"`
Description pgtype.Text `json:"description"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Связь ролей и разрешений (Many-to-Many) для гибкой системы RBAC
type RolePermission struct {
ID int64 `json:"id"`
RoleID int64 `json:"role_id"`
PermissionID int64 `json:"permission_id"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Пользователи системы: маломобильные граждане, волонтёры, модераторы
type User struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
PasswordHash string `json:"password_hash"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
// Координаты домашнего адреса в формате WGS84 (SRID 4326)
Location interface{} `json:"location"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
// Средний рейтинг волонтёра (0-5), обновляется триггером
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
// Количество выполненных заявок, обновляется триггером
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
IsBlocked pgtype.Bool `json:"is_blocked"`
EmailVerified pgtype.Bool `json:"email_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
// Soft delete - дата удаления пользователя
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
// Блокировки пользователей модераторами
type UserBlock struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
BlockedBy int64 `json:"blocked_by"`
ComplaintID pgtype.Int8 `json:"complaint_id"`
Reason string `json:"reason"`
// Дата окончания блокировки (NULL = бессрочная)
BlockedUntil pgtype.Timestamptz `json:"blocked_until"`
// Активна ли блокировка в данный момент
IsActive pgtype.Bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UnblockedAt pgtype.Timestamptz `json:"unblocked_at"`
UnblockedBy pgtype.Int8 `json:"unblocked_by"`
}
// Связь пользователей и ролей (Many-to-Many). Один пользователь может иметь несколько ролей
type UserRole struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
RoleID int64 `json:"role_id"`
AssignedAt pgtype.Timestamptz `json:"assigned_at"`
// Кто назначил роль (для аудита)
AssignedBy pgtype.Int8 `json:"assigned_by"`
}
// Активные сессии пользователей для отслеживания активности
type UserSession struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
SessionToken string `json:"session_token"`
RefreshTokenID pgtype.Int8 `json:"refresh_token_id"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
// Последняя активность пользователя в сессии
LastActivityAt pgtype.Timestamptz `json:"last_activity_at"`
UserAgent pgtype.Text `json:"user_agent"`
IpAddress *netip.Addr `json:"ip_address"`
// Информация об устройстве: ОС, браузер, версия и т.д.
DeviceInfo []byte `json:"device_info"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
// Отклики волонтёров на заявки помощи
type VolunteerResponse struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
VolunteerID int64 `json:"volunteer_id"`
Status NullResponseStatus `json:"status"`
// Сообщение волонтёра при отклике (опционально)
Message pgtype.Text `json:"message"`
// Время создания отклика
RespondedAt pgtype.Timestamptz `json:"responded_at"`
AcceptedAt pgtype.Timestamptz `json:"accepted_at"`
RejectedAt pgtype.Timestamptz `json:"rejected_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}

View File

@@ -0,0 +1,185 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
type Querier interface {
AcceptVolunteerResponse(ctx context.Context, id int64) error
ApproveRequest(ctx context.Context, arg ApproveRequestParams) error
AssignRoleToUser(ctx context.Context, arg AssignRoleToUserParams) (UserRole, error)
AssignVolunteerToRequest(ctx context.Context, arg AssignVolunteerToRequestParams) error
BlockUser(ctx context.Context, id int64) error
CalculateVolunteerAverageRating(ctx context.Context, volunteerID int64) (CalculateVolunteerAverageRatingRow, error)
// ============================================================================
// Хранимые процедуры
// ============================================================================
CallAcceptVolunteerResponse(ctx context.Context, arg CallAcceptVolunteerResponseParams) (CallAcceptVolunteerResponseRow, error)
CallCompleteRequestWithRating(ctx context.Context, arg CallCompleteRequestWithRatingParams) (CallCompleteRequestWithRatingRow, error)
CallModerateRequest(ctx context.Context, arg CallModerateRequestParams) (CallModerateRequestRow, error)
CancelRequest(ctx context.Context, id int64) error
CleanupExpiredSessions(ctx context.Context) error
CleanupExpiredTokens(ctx context.Context) error
CompleteRequest(ctx context.Context, id int64) error
CountPendingResponsesByVolunteer(ctx context.Context, volunteerID int64) (int64, error)
// ============================================================================
// Статистика
// ============================================================================
CountRequestsByRequester(ctx context.Context, requesterID int64) (int64, error)
CountRequestsByStatus(ctx context.Context, status NullRequestStatus) (int64, error)
// ============================================================================
// Подсчет заявок поблизости
// ============================================================================
CountRequestsNearby(ctx context.Context, arg CountRequestsNearbyParams) (int64, error)
CountResponsesByRequest(ctx context.Context, requestID int64) (int64, error)
// ============================================================================
// Рейтинги
// ============================================================================
CreateRating(ctx context.Context, arg CreateRatingParams) (Rating, error)
// ============================================================================
// Refresh Tokens
// ============================================================================
CreateRefreshToken(ctx context.Context, arg CreateRefreshTokenParams) (RefreshToken, error)
// Фаза 2A: Управление заявками (ВЫСОКИЙ ПРИОРИТЕТ)
// CRUD операции для заявок на помощь
// ============================================================================
// Создание и получение заявок
// ============================================================================
CreateRequest(ctx context.Context, arg CreateRequestParams) (CreateRequestRow, error)
// ============================================================================
// История изменения статусов заявок
// ============================================================================
CreateStatusHistoryEntry(ctx context.Context, arg CreateStatusHistoryEntryParams) (RequestStatusHistory, error)
// Фаза 1A: Аутентификация (КРИТИЧНО)
// Запросы для регистрации, входа и управления токенами
// ============================================================================
// Пользователи
// ============================================================================
CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error)
// ============================================================================
// User Sessions
// ============================================================================
CreateUserSession(ctx context.Context, arg CreateUserSessionParams) (UserSession, error)
// Фаза 3: Отклики волонтеров и история статусов (СРЕДНИЙ ПРИОРИТЕТ)
// Запросы для управления откликами волонтеров и историей изменения статусов заявок
// ============================================================================
// Отклики волонтеров
// ============================================================================
CreateVolunteerResponse(ctx context.Context, arg CreateVolunteerResponseParams) (VolunteerResponse, error)
// ============================================================================
// Удаление заявок
// ============================================================================
DeleteRequest(ctx context.Context, arg DeleteRequestParams) error
EmailExists(ctx context.Context, email string) (bool, error)
// ============================================================================
// Поиск ближайших заявок для волонтера
// ============================================================================
FindNearestRequestsForVolunteer(ctx context.Context, arg FindNearestRequestsForVolunteerParams) ([]FindNearestRequestsForVolunteerRow, error)
// ============================================================================
// Поиск заявок в прямоугольной области (для карты)
// ============================================================================
FindRequestsInBounds(ctx context.Context, arg FindRequestsInBoundsParams) ([]FindRequestsInBoundsRow, error)
// Фаза 2B: Геопространственные запросы (ВЫСОКИЙ ПРИОРИТЕТ)
// PostGIS запросы для поиска заявок по геолокации
// ============================================================================
// Поиск заявок рядом с точкой
// ============================================================================
FindRequestsNearby(ctx context.Context, arg FindRequestsNearbyParams) ([]FindRequestsNearbyRow, error)
// ============================================================================
// Поиск волонтеров рядом с заявкой
// ============================================================================
FindVolunteersNearRequest(ctx context.Context, arg FindVolunteersNearRequestParams) ([]FindVolunteersNearRequestRow, error)
GetLatestStatusChange(ctx context.Context, requestID int64) (GetLatestStatusChangeRow, error)
GetModeratedRequests(ctx context.Context, arg GetModeratedRequestsParams) ([]GetModeratedRequestsRow, error)
GetModeratorActionsByModerator(ctx context.Context, arg GetModeratorActionsByModeratorParams) ([]GetModeratorActionsByModeratorRow, error)
// ============================================================================
// Аудит действий модераторов
// ============================================================================
GetModeratorActionsByRequest(ctx context.Context, targetRequestID pgtype.Int8) ([]GetModeratorActionsByRequestRow, error)
// ============================================================================
// Модерация заявок
// ============================================================================
GetPendingModerationRequests(ctx context.Context, arg GetPendingModerationRequestsParams) ([]GetPendingModerationRequestsRow, error)
GetPermissionByName(ctx context.Context, name string) (Permission, error)
GetRatingByResponseID(ctx context.Context, volunteerResponseID int64) (Rating, error)
GetRatingsByVolunteer(ctx context.Context, arg GetRatingsByVolunteerParams) ([]GetRatingsByVolunteerRow, error)
GetRefreshToken(ctx context.Context, token string) (RefreshToken, error)
GetRequestByID(ctx context.Context, id int64) (GetRequestByIDRow, error)
GetRequestStatusHistory(ctx context.Context, requestID int64) ([]GetRequestStatusHistoryRow, error)
GetRequestTypeByID(ctx context.Context, id int64) (RequestType, error)
GetRequestTypeByName(ctx context.Context, name string) (RequestType, error)
GetRequestsByRequester(ctx context.Context, arg GetRequestsByRequesterParams) ([]GetRequestsByRequesterRow, error)
GetRequestsByStatus(ctx context.Context, arg GetRequestsByStatusParams) ([]GetRequestsByStatusRow, error)
GetResponseByID(ctx context.Context, id int64) (GetResponseByIDRow, error)
GetResponsesByRequest(ctx context.Context, requestID int64) ([]GetResponsesByRequestRow, error)
GetResponsesByVolunteer(ctx context.Context, arg GetResponsesByVolunteerParams) ([]GetResponsesByVolunteerRow, error)
GetRoleByID(ctx context.Context, id int64) (Role, error)
// Фаза 1B: RBAC (Role-Based Access Control) (КРИТИЧНО)
// Запросы для управления ролями и правами доступа
// ============================================================================
// Роли
// ============================================================================
GetRoleByName(ctx context.Context, name string) (Role, error)
GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error)
GetUserByID(ctx context.Context, id int64) (GetUserByIDRow, error)
// ============================================================================
// Права доступа
// ============================================================================
GetUserPermissions(ctx context.Context, id int64) ([]GetUserPermissionsRow, error)
// Фаза 1C: Управление профилем (КРИТИЧНО)
// Запросы для получения и обновления профилей пользователей
// ============================================================================
// Профиль пользователя
// ============================================================================
GetUserProfile(ctx context.Context, id int64) (GetUserProfileRow, error)
// ============================================================================
// Пользовательские роли
// ============================================================================
GetUserRoles(ctx context.Context, userID int64) ([]Role, error)
GetUserSession(ctx context.Context, sessionToken string) (UserSession, error)
// ============================================================================
// Поиск пользователей
// ============================================================================
GetUsersByIDs(ctx context.Context, dollar_1 []int64) ([]GetUsersByIDsRow, error)
GetVolunteerStatistics(ctx context.Context, id int64) (GetVolunteerStatisticsRow, error)
InvalidateAllUserSessions(ctx context.Context, userID int64) error
InvalidateUserSession(ctx context.Context, id int64) error
ListAllRoles(ctx context.Context) ([]Role, error)
ListPermissionsByRole(ctx context.Context, roleID int64) ([]Permission, error)
// ============================================================================
// Типы заявок
// ============================================================================
ListRequestTypes(ctx context.Context) ([]RequestType, error)
ModerateRequest(ctx context.Context, arg ModerateRequestParams) error
RejectRequest(ctx context.Context, arg RejectRequestParams) error
RejectVolunteerResponse(ctx context.Context, id int64) error
RemoveRoleFromUser(ctx context.Context, arg RemoveRoleFromUserParams) error
RevokeAllUserTokens(ctx context.Context, userID int64) error
RevokeRefreshToken(ctx context.Context, id int64) error
SearchUsersByName(ctx context.Context, arg SearchUsersByNameParams) ([]SearchUsersByNameRow, error)
SoftDeleteUser(ctx context.Context, id int64) error
UnblockUser(ctx context.Context, id int64) error
UpdateLastLogin(ctx context.Context, id int64) error
UpdateRating(ctx context.Context, arg UpdateRatingParams) error
// ============================================================================
// Обновление заявок
// ============================================================================
UpdateRequestStatus(ctx context.Context, arg UpdateRequestStatusParams) error
UpdateSessionActivity(ctx context.Context, id int64) error
UpdateUserLocation(ctx context.Context, arg UpdateUserLocationParams) error
UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error
UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error
UserHasAnyPermission(ctx context.Context, arg UserHasAnyPermissionParams) (bool, error)
UserHasPermission(ctx context.Context, arg UserHasPermissionParams) (bool, error)
UserHasRole(ctx context.Context, arg UserHasRoleParams) (bool, error)
UserHasRoleByName(ctx context.Context, arg UserHasRoleByNameParams) (bool, error)
VerifyUserEmail(ctx context.Context, id int64) error
}
var _ Querier = (*Queries)(nil)

View File

@@ -0,0 +1,194 @@
-- Фаза 1A: Аутентификация (КРИТИЧНО)
-- Запросы для регистрации, входа и управления токенами
-- ============================================================================
-- Пользователи
-- ============================================================================
-- name: CreateUser :one
INSERT INTO users (
email,
phone,
password_hash,
first_name,
last_name,
location,
address,
city
) VALUES (
$1,
$2,
$3,
$4,
$5,
ST_SetSRID(ST_MakePoint($6, $7), 4326)::geography,
$8,
$9
) RETURNING
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at;
-- name: GetUserByEmail :one
SELECT
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
FROM users
WHERE email = $1 AND deleted_at IS NULL;
-- name: GetUserByID :one
SELECT
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
FROM users
WHERE id = $1 AND deleted_at IS NULL;
-- name: EmailExists :one
SELECT EXISTS(
SELECT 1 FROM users
WHERE email = $1 AND deleted_at IS NULL
);
-- name: UpdateLastLogin :exec
UPDATE users
SET last_login_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- ============================================================================
-- Refresh Tokens
-- ============================================================================
-- name: CreateRefreshToken :one
INSERT INTO refresh_tokens (
user_id,
token,
expires_at,
user_agent,
ip_address
) VALUES (
$1,
$2,
$3,
$4,
$5
) RETURNING *;
-- name: GetRefreshToken :one
SELECT * FROM refresh_tokens
WHERE token = $1
AND revoked = FALSE
AND expires_at > CURRENT_TIMESTAMP;
-- name: RevokeRefreshToken :exec
UPDATE refresh_tokens
SET revoked = TRUE, revoked_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: RevokeAllUserTokens :exec
UPDATE refresh_tokens
SET revoked = TRUE, revoked_at = CURRENT_TIMESTAMP
WHERE user_id = $1 AND revoked = FALSE;
-- name: CleanupExpiredTokens :exec
DELETE FROM refresh_tokens
WHERE expires_at < CURRENT_TIMESTAMP
OR (revoked = TRUE AND revoked_at < CURRENT_TIMESTAMP - INTERVAL '30 days');
-- ============================================================================
-- User Sessions
-- ============================================================================
-- name: CreateUserSession :one
INSERT INTO user_sessions (
user_id,
session_token,
refresh_token_id,
expires_at,
user_agent,
ip_address,
device_info
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7
) RETURNING *;
-- name: GetUserSession :one
SELECT * FROM user_sessions
WHERE session_token = $1
AND expires_at > CURRENT_TIMESTAMP;
-- name: UpdateSessionActivity :exec
UPDATE user_sessions
SET last_activity_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: InvalidateUserSession :exec
DELETE FROM user_sessions
WHERE id = $1;
-- name: InvalidateAllUserSessions :exec
DELETE FROM user_sessions
WHERE user_id = $1;
-- name: CleanupExpiredSessions :exec
DELETE FROM user_sessions
WHERE expires_at < CURRENT_TIMESTAMP
OR last_activity_at < CURRENT_TIMESTAMP - INTERVAL '7 days';

View File

@@ -0,0 +1,151 @@
-- Фаза 2B: Геопространственные запросы (ВЫСОКИЙ ПРИОРИТЕТ)
-- PostGIS запросы для поиска заявок по геолокации
-- ============================================================================
-- Поиск заявок рядом с точкой
-- ============================================================================
-- name: FindRequestsNearby :many
SELECT
r.id,
r.title,
r.description,
r.address,
r.city,
r.urgency,
r.status,
r.created_at,
r.desired_completion_date,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
ST_Distance(
r.location,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography
) as distance_meters,
rt.name as request_type_name,
rt.icon as request_type_icon,
(u.first_name || ' ' || u.last_name) as requester_name
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
JOIN users u ON u.id = r.requester_id
WHERE r.deleted_at IS NULL
AND r.status::text = ANY($3::text[])
AND ST_DWithin(
r.location,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography,
$4
)
ORDER BY distance_meters
LIMIT $5 OFFSET $6;
-- ============================================================================
-- Поиск заявок в прямоугольной области (для карты)
-- ============================================================================
-- name: FindRequestsInBounds :many
SELECT
r.id,
r.title,
r.urgency,
r.status,
r.created_at,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
rt.icon as request_type_icon,
rt.name as request_type_name
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
WHERE r.deleted_at IS NULL
AND r.status::text = ANY($1::text[])
AND ST_Within(
r.location::geometry,
ST_MakeEnvelope($2, $3, $4, $5, 4326)
)
ORDER BY r.created_at DESC
LIMIT 200;
-- ============================================================================
-- Подсчет заявок поблизости
-- ============================================================================
-- name: CountRequestsNearby :one
SELECT COUNT(*) FROM requests r
WHERE r.deleted_at IS NULL
AND r.status = $3
AND ST_DWithin(
r.location,
ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography,
$4
);
-- ============================================================================
-- Поиск волонтеров рядом с заявкой
-- ============================================================================
-- name: FindVolunteersNearRequest :many
SELECT
u.id,
(u.first_name || ' ' || u.last_name) as full_name,
u.avatar_url,
u.volunteer_rating,
u.completed_requests_count,
ST_Y(u.location::geometry) as latitude,
ST_X(u.location::geometry) as longitude,
ST_Distance(
u.location,
(SELECT req.location FROM requests req WHERE req.id = $1)
) as distance_meters
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN roles r ON r.id = ur.role_id
WHERE r.name = 'volunteer'
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
AND u.location IS NOT NULL
AND ST_DWithin(
u.location,
(SELECT req.location FROM requests req WHERE req.id = $1),
$2
)
ORDER BY distance_meters
LIMIT $3;
-- ============================================================================
-- Поиск ближайших заявок для волонтера
-- ============================================================================
-- name: FindNearestRequestsForVolunteer :many
SELECT
r.id,
r.title,
r.description,
r.urgency,
r.status,
r.created_at,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
ST_Distance(
r.location,
(SELECT u.location FROM users u WHERE u.id = $1)
) as distance_meters,
rt.name as request_type_name,
rt.icon as request_type_icon
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
WHERE r.deleted_at IS NULL
AND r.status = 'approved'
AND r.assigned_volunteer_id IS NULL
AND ST_DWithin(
r.location,
(SELECT u.location FROM users u WHERE u.id = $1),
$2
)
ORDER BY
CASE r.urgency
WHEN 'urgent' THEN 1
WHEN 'high' THEN 2
WHEN 'medium' THEN 3
ELSE 4
END,
distance_meters
LIMIT $3;

View File

@@ -0,0 +1,102 @@
-- Фаза 1B: RBAC (Role-Based Access Control) (КРИТИЧНО)
-- Запросы для управления ролями и правами доступа
-- ============================================================================
-- Роли
-- ============================================================================
-- name: GetRoleByName :one
SELECT * FROM roles
WHERE name = $1;
-- name: GetRoleByID :one
SELECT * FROM roles
WHERE id = $1;
-- name: ListAllRoles :many
SELECT * FROM roles
ORDER BY name;
-- ============================================================================
-- Пользовательские роли
-- ============================================================================
-- name: GetUserRoles :many
SELECT r.* FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = $1
ORDER BY r.name;
-- name: AssignRoleToUser :one
INSERT INTO user_roles (user_id, role_id, assigned_by)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, role_id) DO NOTHING
RETURNING *;
-- name: RemoveRoleFromUser :exec
DELETE FROM user_roles
WHERE user_id = $1 AND role_id = $2;
-- name: UserHasRole :one
SELECT EXISTS(
SELECT 1 FROM user_roles
WHERE user_id = $1 AND role_id = $2
);
-- name: UserHasRoleByName :one
SELECT EXISTS(
SELECT 1 FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = $1 AND r.name = $2
);
-- ============================================================================
-- Права доступа
-- ============================================================================
-- name: GetUserPermissions :many
SELECT DISTINCT p.name, p.resource, p.action, p.description
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
ORDER BY p.resource, p.action;
-- name: GetPermissionByName :one
SELECT * FROM permissions
WHERE name = $1;
-- name: ListPermissionsByRole :many
SELECT p.* FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
WHERE rp.role_id = $1
ORDER BY p.resource, p.action;
-- name: UserHasPermission :one
SELECT EXISTS(
SELECT 1
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
AND p.name = $2
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
);
-- name: UserHasAnyPermission :one
SELECT EXISTS(
SELECT 1
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
AND p.name = ANY($2::varchar[])
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
);

View File

@@ -0,0 +1,339 @@
-- Фаза 2A: Управление заявками (ВЫСОКИЙ ПРИОРИТЕТ)
-- CRUD операции для заявок на помощь
-- ============================================================================
-- Создание и получение заявок
-- ============================================================================
-- name: CreateRequest :one
INSERT INTO requests (
requester_id,
request_type_id,
title,
description,
location,
address,
city,
desired_completion_date,
urgency,
contact_phone,
contact_notes
) VALUES (
$1,
$2,
$3,
$4,
ST_SetSRID(ST_MakePoint($5, $6), 4326)::geography,
$7,
$8,
$9,
$10,
$11,
$12
) RETURNING
id,
requester_id,
request_type_id,
title,
description,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
desired_completion_date,
urgency,
contact_phone,
contact_notes,
status,
assigned_volunteer_id,
created_at,
updated_at,
deleted_at;
-- name: GetRequestByID :one
SELECT
r.id,
r.requester_id,
r.request_type_id,
r.title,
r.description,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
r.address,
r.city,
r.desired_completion_date,
r.urgency,
r.contact_phone,
r.contact_notes,
r.status,
r.assigned_volunteer_id,
r.created_at,
r.updated_at,
r.deleted_at,
r.completed_at,
rt.name as request_type_name,
rt.icon as request_type_icon,
(u.first_name || ' ' || u.last_name) as requester_name,
u.phone as requester_phone,
u.email as requester_email,
(av.first_name || ' ' || av.last_name) as assigned_volunteer_name,
av.phone as assigned_volunteer_phone
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
JOIN users u ON u.id = r.requester_id
LEFT JOIN users av ON av.id = r.assigned_volunteer_id
WHERE r.id = $1 AND r.deleted_at IS NULL;
-- name: GetRequestsByRequester :many
SELECT
r.id,
r.requester_id,
r.request_type_id,
r.title,
r.description,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
r.address,
r.city,
r.desired_completion_date,
r.urgency,
r.contact_phone,
r.contact_notes,
r.status,
r.assigned_volunteer_id,
r.created_at,
r.updated_at,
r.deleted_at,
rt.name as request_type_name,
rt.icon as request_type_icon
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
WHERE r.requester_id = $1
AND r.deleted_at IS NULL
ORDER BY r.created_at DESC
LIMIT $2 OFFSET $3;
-- name: GetRequestsByStatus :many
SELECT
r.id,
r.requester_id,
r.request_type_id,
r.title,
r.description,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
r.address,
r.city,
r.desired_completion_date,
r.urgency,
r.contact_phone,
r.contact_notes,
r.status,
r.assigned_volunteer_id,
r.created_at,
r.updated_at,
r.deleted_at,
rt.name as request_type_name,
(u.first_name || ' ' || u.last_name) as requester_name
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
JOIN users u ON u.id = r.requester_id
WHERE r.status = $1
AND r.deleted_at IS NULL
ORDER BY r.created_at DESC
LIMIT $2 OFFSET $3;
-- ============================================================================
-- Обновление заявок
-- ============================================================================
-- name: UpdateRequestStatus :exec
UPDATE requests SET
status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND deleted_at IS NULL;
-- name: AssignVolunteerToRequest :exec
UPDATE requests SET
assigned_volunteer_id = $2,
status = 'in_progress',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND deleted_at IS NULL;
-- name: CompleteRequest :exec
UPDATE requests SET
status = 'completed',
completed_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND deleted_at IS NULL;
-- name: CancelRequest :exec
UPDATE requests SET
status = 'cancelled',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND deleted_at IS NULL;
-- name: ModerateRequest :exec
UPDATE requests SET
status = $2,
moderated_by = $3,
moderated_at = CURRENT_TIMESTAMP,
moderation_comment = $4,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- ============================================================================
-- Удаление заявок
-- ============================================================================
-- name: DeleteRequest :exec
UPDATE requests SET
deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND requester_id = $2
AND deleted_at IS NULL;
-- ============================================================================
-- Типы заявок
-- ============================================================================
-- name: ListRequestTypes :many
SELECT * FROM request_types
WHERE is_active = TRUE
ORDER BY name;
-- name: GetRequestTypeByID :one
SELECT * FROM request_types
WHERE id = $1;
-- name: GetRequestTypeByName :one
SELECT * FROM request_types
WHERE name = $1;
-- ============================================================================
-- Статистика
-- ============================================================================
-- name: CountRequestsByRequester :one
SELECT COUNT(*) FROM requests
WHERE requester_id = $1
AND deleted_at IS NULL;
-- name: CountRequestsByStatus :one
SELECT COUNT(*) FROM requests
WHERE status = $1
AND deleted_at IS NULL;
-- ============================================================================
-- Модерация заявок
-- ============================================================================
-- name: GetPendingModerationRequests :many
SELECT
r.id,
r.requester_id,
r.request_type_id,
r.title,
r.description,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
r.address,
r.city,
r.desired_completion_date,
r.urgency,
r.contact_phone,
r.contact_notes,
r.status,
r.created_at,
r.updated_at,
rt.name as request_type_name,
rt.icon as request_type_icon,
(u.first_name || ' ' || u.last_name) as requester_name,
u.email as requester_email,
u.phone as requester_phone
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
JOIN users u ON u.id = r.requester_id
WHERE r.status = 'pending_moderation'
AND r.deleted_at IS NULL
ORDER BY r.created_at ASC
LIMIT $1 OFFSET $2;
-- name: ApproveRequest :exec
UPDATE requests SET
status = 'approved',
moderated_by = $2,
moderated_at = CURRENT_TIMESTAMP,
moderation_comment = sqlc.narg('moderation_comment'),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND status = 'pending_moderation'
AND deleted_at IS NULL;
-- name: RejectRequest :exec
UPDATE requests SET
status = 'rejected',
moderated_by = $2,
moderated_at = CURRENT_TIMESTAMP,
moderation_comment = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
AND status = 'pending_moderation'
AND deleted_at IS NULL;
-- name: GetModeratedRequests :many
SELECT
r.id,
r.requester_id,
r.request_type_id,
r.title,
r.description,
ST_Y(r.location::geometry) as latitude,
ST_X(r.location::geometry) as longitude,
r.address,
r.status,
r.moderated_by,
r.moderated_at,
r.moderation_comment,
r.created_at,
rt.name as request_type_name,
(u.first_name || ' ' || u.last_name) as requester_name,
(m.first_name || ' ' || m.last_name) as moderator_name
FROM requests r
JOIN request_types rt ON rt.id = r.request_type_id
JOIN users u ON u.id = r.requester_id
LEFT JOIN users m ON m.id = r.moderated_by
WHERE r.moderated_by = $1
AND r.deleted_at IS NULL
ORDER BY r.moderated_at DESC
LIMIT $2 OFFSET $3;
-- ============================================================================
-- Аудит действий модераторов
-- ============================================================================
-- name: GetModeratorActionsByRequest :many
SELECT
ma.*,
(u.first_name || ' ' || u.last_name) as moderator_name,
u.email as moderator_email
FROM moderator_actions ma
JOIN users u ON u.id = ma.moderator_id
WHERE ma.target_request_id = $1
ORDER BY ma.created_at DESC;
-- name: GetModeratorActionsByModerator :many
SELECT
ma.*,
r.title as request_title,
r.status as request_status
FROM moderator_actions ma
LEFT JOIN requests r ON r.id = ma.target_request_id
WHERE ma.moderator_id = $1
ORDER BY ma.created_at DESC
LIMIT $2 OFFSET $3;

View File

@@ -0,0 +1,192 @@
-- Фаза 3: Отклики волонтеров и история статусов (СРЕДНИЙ ПРИОРИТЕТ)
-- Запросы для управления откликами волонтеров и историей изменения статусов заявок
-- ============================================================================
-- Отклики волонтеров
-- ============================================================================
-- name: CreateVolunteerResponse :one
INSERT INTO volunteer_responses (
request_id,
volunteer_id,
message
) VALUES (
$1,
$2,
$3
)
ON CONFLICT (request_id, volunteer_id) DO NOTHING
RETURNING *;
-- name: GetResponsesByRequest :many
SELECT
vr.*,
(u.first_name || ' ' || u.last_name) as volunteer_name,
u.avatar_url as volunteer_avatar,
u.volunteer_rating,
u.completed_requests_count,
u.email as volunteer_email,
u.phone as volunteer_phone
FROM volunteer_responses vr
JOIN users u ON u.id = vr.volunteer_id
WHERE vr.request_id = $1
ORDER BY vr.created_at DESC;
-- name: GetResponsesByVolunteer :many
SELECT
vr.*,
r.title as request_title,
r.status as request_status,
(u.first_name || ' ' || u.last_name) as requester_name
FROM volunteer_responses vr
JOIN requests r ON r.id = vr.request_id
JOIN users u ON u.id = r.requester_id
WHERE vr.volunteer_id = $1
ORDER BY vr.created_at DESC
LIMIT $2 OFFSET $3;
-- name: GetResponseByID :one
SELECT
vr.*,
(u.first_name || ' ' || u.last_name) as volunteer_name,
r.title as request_title
FROM volunteer_responses vr
JOIN users u ON u.id = vr.volunteer_id
JOIN requests r ON r.id = vr.request_id
WHERE vr.id = $1;
-- name: AcceptVolunteerResponse :exec
UPDATE volunteer_responses SET
status = 'accepted',
accepted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: RejectVolunteerResponse :exec
UPDATE volunteer_responses SET
status = 'rejected',
rejected_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CountResponsesByRequest :one
SELECT COUNT(*) FROM volunteer_responses
WHERE request_id = $1;
-- name: CountPendingResponsesByVolunteer :one
SELECT COUNT(*) FROM volunteer_responses
WHERE volunteer_id = $1 AND status = 'pending';
-- ============================================================================
-- История изменения статусов заявок
-- ============================================================================
-- name: CreateStatusHistoryEntry :one
INSERT INTO request_status_history (
request_id,
from_status,
to_status,
changed_by,
comment
) VALUES (
$1,
$2,
$3,
$4,
sqlc.narg('comment')
) RETURNING *;
-- name: GetRequestStatusHistory :many
SELECT
rsh.*,
(u.first_name || ' ' || u.last_name) as changed_by_name
FROM request_status_history rsh
JOIN users u ON u.id = rsh.changed_by
WHERE rsh.request_id = $1
ORDER BY rsh.created_at DESC;
-- name: GetLatestStatusChange :one
SELECT
rsh.*,
(u.first_name || ' ' || u.last_name) as changed_by_name
FROM request_status_history rsh
JOIN users u ON u.id = rsh.changed_by
WHERE rsh.request_id = $1
ORDER BY rsh.created_at DESC
LIMIT 1;
-- ============================================================================
-- Рейтинги
-- ============================================================================
-- name: CreateRating :one
INSERT INTO ratings (
volunteer_response_id,
volunteer_id,
requester_id,
request_id,
rating,
comment
) VALUES (
$1,
$2,
$3,
$4,
$5,
sqlc.narg('comment')
) RETURNING *;
-- name: GetRatingByResponseID :one
SELECT * FROM ratings
WHERE volunteer_response_id = $1;
-- name: GetRatingsByVolunteer :many
SELECT
r.*,
req.title as request_title,
(u.first_name || ' ' || u.last_name) as requester_name
FROM ratings r
JOIN requests req ON req.id = r.request_id
JOIN users u ON u.id = r.requester_id
WHERE r.volunteer_id = $1
ORDER BY r.created_at DESC
LIMIT $2 OFFSET $3;
-- name: CalculateVolunteerAverageRating :one
SELECT
COALESCE(AVG(rating), 0) as average_rating,
COUNT(*) as total_ratings
FROM ratings
WHERE volunteer_id = $1;
-- name: UpdateRating :exec
UPDATE ratings SET
rating = $2,
comment = sqlc.narg('comment'),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- ============================================================================
-- Хранимые процедуры
-- ============================================================================
-- name: CallAcceptVolunteerResponse :one
SELECT
r.success::BOOLEAN,
r.message::TEXT,
r.out_request_id::BIGINT,
r.out_volunteer_id::BIGINT
FROM accept_volunteer_response($1, $2) AS r(success, message, out_request_id, out_volunteer_id);
-- name: CallCompleteRequestWithRating :one
SELECT
r.success::BOOLEAN,
r.message::TEXT,
r.out_rating_id::BIGINT
FROM complete_request_with_rating($1, $2, $3, sqlc.narg('comment')) AS r(success, message, out_rating_id);
-- name: CallModerateRequest :one
SELECT
r.success::BOOLEAN,
r.message::TEXT
FROM moderate_request($1, $2, $3, sqlc.narg('comment')) AS r(success, message);

View File

@@ -0,0 +1,137 @@
-- Фаза 1C: Управление профилем (КРИТИЧНО)
-- Запросы для получения и обновления профилей пользователей
-- ============================================================================
-- Профиль пользователя
-- ============================================================================
-- name: GetUserProfile :one
SELECT
id,
email,
phone,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at
FROM users
WHERE id = $1 AND deleted_at IS NULL;
-- name: UpdateUserProfile :exec
UPDATE users SET
phone = COALESCE(sqlc.narg('phone'), phone),
first_name = COALESCE(sqlc.narg('first_name'), first_name),
last_name = COALESCE(sqlc.narg('last_name'), last_name),
avatar_url = COALESCE(sqlc.narg('avatar_url'), avatar_url),
address = COALESCE(sqlc.narg('address'), address),
city = COALESCE(sqlc.narg('city'), city),
updated_at = CURRENT_TIMESTAMP
WHERE id = sqlc.arg('user_id');
-- name: UpdateUserLocation :exec
UPDATE users SET
location = ST_SetSRID(ST_MakePoint($2, $3), 4326)::geography,
address = COALESCE(sqlc.narg('address'), address),
city = COALESCE(sqlc.narg('city'), city),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: VerifyUserEmail :exec
UPDATE users SET
email_verified = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: UpdateUserPassword :exec
UPDATE users SET
password_hash = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: BlockUser :exec
UPDATE users SET
is_blocked = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: UnblockUser :exec
UPDATE users SET
is_blocked = FALSE,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: SoftDeleteUser :exec
UPDATE users SET
deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- ============================================================================
-- Поиск пользователей
-- ============================================================================
-- name: GetUsersByIDs :many
SELECT
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
FROM users
WHERE id = ANY($1::bigint[])
AND deleted_at IS NULL;
-- name: SearchUsersByName :many
SELECT
id,
email,
first_name,
last_name,
avatar_url,
volunteer_rating,
completed_requests_count,
is_verified
FROM users
WHERE (first_name ILIKE '%' || $1 || '%' OR last_name ILIKE '%' || $1 || '%' OR (first_name || ' ' || last_name) ILIKE '%' || $1 || '%')
AND deleted_at IS NULL
AND is_blocked = FALSE
ORDER BY volunteer_rating DESC NULLS LAST
LIMIT $2 OFFSET $3;
-- name: GetVolunteerStatistics :one
SELECT
id,
first_name,
last_name,
volunteer_rating,
completed_requests_count,
created_at as member_since
FROM users
WHERE id = $1
AND deleted_at IS NULL;

View File

@@ -0,0 +1,352 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: rbac.sql
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const AssignRoleToUser = `-- name: AssignRoleToUser :one
INSERT INTO user_roles (user_id, role_id, assigned_by)
VALUES ($1, $2, $3)
ON CONFLICT (user_id, role_id) DO NOTHING
RETURNING id, user_id, role_id, assigned_at, assigned_by
`
type AssignRoleToUserParams struct {
UserID int64 `json:"user_id"`
RoleID int64 `json:"role_id"`
AssignedBy pgtype.Int8 `json:"assigned_by"`
}
func (q *Queries) AssignRoleToUser(ctx context.Context, arg AssignRoleToUserParams) (UserRole, error) {
row := q.db.QueryRow(ctx, AssignRoleToUser, arg.UserID, arg.RoleID, arg.AssignedBy)
var i UserRole
err := row.Scan(
&i.ID,
&i.UserID,
&i.RoleID,
&i.AssignedAt,
&i.AssignedBy,
)
return i, err
}
const GetPermissionByName = `-- name: GetPermissionByName :one
SELECT id, name, resource, action, description, created_at FROM permissions
WHERE name = $1
`
func (q *Queries) GetPermissionByName(ctx context.Context, name string) (Permission, error) {
row := q.db.QueryRow(ctx, GetPermissionByName, name)
var i Permission
err := row.Scan(
&i.ID,
&i.Name,
&i.Resource,
&i.Action,
&i.Description,
&i.CreatedAt,
)
return i, err
}
const GetRoleByID = `-- name: GetRoleByID :one
SELECT id, name, description, created_at FROM roles
WHERE id = $1
`
func (q *Queries) GetRoleByID(ctx context.Context, id int64) (Role, error) {
row := q.db.QueryRow(ctx, GetRoleByID, id)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
)
return i, err
}
const GetRoleByName = `-- name: GetRoleByName :one
SELECT id, name, description, created_at FROM roles
WHERE name = $1
`
// Фаза 1B: RBAC (Role-Based Access Control) (КРИТИЧНО)
// Запросы для управления ролями и правами доступа
// ============================================================================
// Роли
// ============================================================================
func (q *Queries) GetRoleByName(ctx context.Context, name string) (Role, error) {
row := q.db.QueryRow(ctx, GetRoleByName, name)
var i Role
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
)
return i, err
}
const GetUserPermissions = `-- name: GetUserPermissions :many
SELECT DISTINCT p.name, p.resource, p.action, p.description
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
ORDER BY p.resource, p.action
`
type GetUserPermissionsRow struct {
Name string `json:"name"`
Resource string `json:"resource"`
Action string `json:"action"`
Description pgtype.Text `json:"description"`
}
// ============================================================================
// Права доступа
// ============================================================================
func (q *Queries) GetUserPermissions(ctx context.Context, id int64) ([]GetUserPermissionsRow, error) {
rows, err := q.db.Query(ctx, GetUserPermissions, id)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetUserPermissionsRow{}
for rows.Next() {
var i GetUserPermissionsRow
if err := rows.Scan(
&i.Name,
&i.Resource,
&i.Action,
&i.Description,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUserRoles = `-- name: GetUserRoles :many
SELECT r.id, r.name, r.description, r.created_at FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = $1
ORDER BY r.name
`
// ============================================================================
// Пользовательские роли
// ============================================================================
func (q *Queries) GetUserRoles(ctx context.Context, userID int64) ([]Role, error) {
rows, err := q.db.Query(ctx, GetUserRoles, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Role{}
for rows.Next() {
var i Role
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListAllRoles = `-- name: ListAllRoles :many
SELECT id, name, description, created_at FROM roles
ORDER BY name
`
func (q *Queries) ListAllRoles(ctx context.Context) ([]Role, error) {
rows, err := q.db.Query(ctx, ListAllRoles)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Role{}
for rows.Next() {
var i Role
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListPermissionsByRole = `-- name: ListPermissionsByRole :many
SELECT p.id, p.name, p.resource, p.action, p.description, p.created_at FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
WHERE rp.role_id = $1
ORDER BY p.resource, p.action
`
func (q *Queries) ListPermissionsByRole(ctx context.Context, roleID int64) ([]Permission, error) {
rows, err := q.db.Query(ctx, ListPermissionsByRole, roleID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Permission{}
for rows.Next() {
var i Permission
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Resource,
&i.Action,
&i.Description,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RemoveRoleFromUser = `-- name: RemoveRoleFromUser :exec
DELETE FROM user_roles
WHERE user_id = $1 AND role_id = $2
`
type RemoveRoleFromUserParams struct {
UserID int64 `json:"user_id"`
RoleID int64 `json:"role_id"`
}
func (q *Queries) RemoveRoleFromUser(ctx context.Context, arg RemoveRoleFromUserParams) error {
_, err := q.db.Exec(ctx, RemoveRoleFromUser, arg.UserID, arg.RoleID)
return err
}
const UserHasAnyPermission = `-- name: UserHasAnyPermission :one
SELECT EXISTS(
SELECT 1
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
AND p.name = ANY($2::varchar[])
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
)
`
type UserHasAnyPermissionParams struct {
ID int64 `json:"id"`
Column2 []string `json:"column_2"`
}
func (q *Queries) UserHasAnyPermission(ctx context.Context, arg UserHasAnyPermissionParams) (bool, error) {
row := q.db.QueryRow(ctx, UserHasAnyPermission, arg.ID, arg.Column2)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const UserHasPermission = `-- name: UserHasPermission :one
SELECT EXISTS(
SELECT 1
FROM users u
JOIN user_roles ur ON ur.user_id = u.id
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE u.id = $1
AND p.name = $2
AND u.deleted_at IS NULL
AND u.is_blocked = FALSE
)
`
type UserHasPermissionParams struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
func (q *Queries) UserHasPermission(ctx context.Context, arg UserHasPermissionParams) (bool, error) {
row := q.db.QueryRow(ctx, UserHasPermission, arg.ID, arg.Name)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const UserHasRole = `-- name: UserHasRole :one
SELECT EXISTS(
SELECT 1 FROM user_roles
WHERE user_id = $1 AND role_id = $2
)
`
type UserHasRoleParams struct {
UserID int64 `json:"user_id"`
RoleID int64 `json:"role_id"`
}
func (q *Queries) UserHasRole(ctx context.Context, arg UserHasRoleParams) (bool, error) {
row := q.db.QueryRow(ctx, UserHasRole, arg.UserID, arg.RoleID)
var exists bool
err := row.Scan(&exists)
return exists, err
}
const UserHasRoleByName = `-- name: UserHasRoleByName :one
SELECT EXISTS(
SELECT 1 FROM user_roles ur
JOIN roles r ON r.id = ur.role_id
WHERE ur.user_id = $1 AND r.name = $2
)
`
type UserHasRoleByNameParams struct {
UserID int64 `json:"user_id"`
Name string `json:"name"`
}
func (q *Queries) UserHasRoleByName(ctx context.Context, arg UserHasRoleByNameParams) (bool, error) {
row := q.db.QueryRow(ctx, UserHasRoleByName, arg.UserID, arg.Name)
var exists bool
err := row.Scan(&exists)
return exists, err
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,713 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: responses.sql
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const AcceptVolunteerResponse = `-- name: AcceptVolunteerResponse :exec
UPDATE volunteer_responses SET
status = 'accepted',
accepted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) AcceptVolunteerResponse(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, AcceptVolunteerResponse, id)
return err
}
const CalculateVolunteerAverageRating = `-- name: CalculateVolunteerAverageRating :one
SELECT
COALESCE(AVG(rating), 0) as average_rating,
COUNT(*) as total_ratings
FROM ratings
WHERE volunteer_id = $1
`
type CalculateVolunteerAverageRatingRow struct {
AverageRating interface{} `json:"average_rating"`
TotalRatings int64 `json:"total_ratings"`
}
func (q *Queries) CalculateVolunteerAverageRating(ctx context.Context, volunteerID int64) (CalculateVolunteerAverageRatingRow, error) {
row := q.db.QueryRow(ctx, CalculateVolunteerAverageRating, volunteerID)
var i CalculateVolunteerAverageRatingRow
err := row.Scan(&i.AverageRating, &i.TotalRatings)
return i, err
}
const CallAcceptVolunteerResponse = `-- name: CallAcceptVolunteerResponse :one
SELECT
r.success::BOOLEAN,
r.message::TEXT,
r.out_request_id::BIGINT,
r.out_volunteer_id::BIGINT
FROM accept_volunteer_response($1, $2) AS r(success, message, out_request_id, out_volunteer_id)
`
type CallAcceptVolunteerResponseParams struct {
PResponseID int64 `json:"p_response_id"`
PRequesterID int64 `json:"p_requester_id"`
}
type CallAcceptVolunteerResponseRow struct {
Success bool `json:"r_success"`
Message string `json:"r_message"`
RequestID int64 `json:"r_out_request_id"`
VolunteerID int64 `json:"r_out_volunteer_id"`
}
// ============================================================================
// Хранимые процедуры
// ============================================================================
func (q *Queries) CallAcceptVolunteerResponse(ctx context.Context, arg CallAcceptVolunteerResponseParams) (CallAcceptVolunteerResponseRow, error) {
row := q.db.QueryRow(ctx, CallAcceptVolunteerResponse, arg.PResponseID, arg.PRequesterID)
var i CallAcceptVolunteerResponseRow
err := row.Scan(
&i.Success,
&i.Message,
&i.RequestID,
&i.VolunteerID,
)
return i, err
}
const CallCompleteRequestWithRating = `-- name: CallCompleteRequestWithRating :one
SELECT
r.success::BOOLEAN,
r.message::TEXT,
r.out_rating_id::BIGINT
FROM complete_request_with_rating($1, $2, $3, $4) AS r(success, message, out_rating_id)
`
type CallCompleteRequestWithRatingParams struct {
PRequestID int64 `json:"p_request_id"`
PRequesterID int64 `json:"p_requester_id"`
PRating int32 `json:"p_rating"`
Comment pgtype.Text `json:"comment"`
}
type CallCompleteRequestWithRatingRow struct {
Success bool `json:"r_success"`
Message string `json:"r_message"`
RatingID int64 `json:"r_out_rating_id"`
}
func (q *Queries) CallCompleteRequestWithRating(ctx context.Context, arg CallCompleteRequestWithRatingParams) (CallCompleteRequestWithRatingRow, error) {
row := q.db.QueryRow(ctx, CallCompleteRequestWithRating,
arg.PRequestID,
arg.PRequesterID,
arg.PRating,
arg.Comment,
)
var i CallCompleteRequestWithRatingRow
err := row.Scan(&i.Success, &i.Message, &i.RatingID)
return i, err
}
const CallModerateRequest = `-- name: CallModerateRequest :one
SELECT
r.success::BOOLEAN,
r.message::TEXT
FROM moderate_request($1, $2, $3, $4) AS r(success, message)
`
type CallModerateRequestParams struct {
PRequestID int64 `json:"p_request_id"`
PModeratorID int64 `json:"p_moderator_id"`
PAction string `json:"p_action"`
Comment pgtype.Text `json:"comment"`
}
type CallModerateRequestRow struct {
Success bool `json:"r_success"`
Message string `json:"r_message"`
}
func (q *Queries) CallModerateRequest(ctx context.Context, arg CallModerateRequestParams) (CallModerateRequestRow, error) {
row := q.db.QueryRow(ctx, CallModerateRequest,
arg.PRequestID,
arg.PModeratorID,
arg.PAction,
arg.Comment,
)
var i CallModerateRequestRow
err := row.Scan(&i.Success, &i.Message)
return i, err
}
const CountPendingResponsesByVolunteer = `-- name: CountPendingResponsesByVolunteer :one
SELECT COUNT(*) FROM volunteer_responses
WHERE volunteer_id = $1 AND status = 'pending'
`
func (q *Queries) CountPendingResponsesByVolunteer(ctx context.Context, volunteerID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountPendingResponsesByVolunteer, volunteerID)
var count int64
err := row.Scan(&count)
return count, err
}
const CountResponsesByRequest = `-- name: CountResponsesByRequest :one
SELECT COUNT(*) FROM volunteer_responses
WHERE request_id = $1
`
func (q *Queries) CountResponsesByRequest(ctx context.Context, requestID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountResponsesByRequest, requestID)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateRating = `-- name: CreateRating :one
INSERT INTO ratings (
volunteer_response_id,
volunteer_id,
requester_id,
request_id,
rating,
comment
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6
) RETURNING id, volunteer_response_id, volunteer_id, requester_id, request_id, rating, comment, created_at, updated_at
`
type CreateRatingParams struct {
VolunteerResponseID int64 `json:"volunteer_response_id"`
VolunteerID int64 `json:"volunteer_id"`
RequesterID int64 `json:"requester_id"`
RequestID int64 `json:"request_id"`
Rating int32 `json:"rating"`
Comment pgtype.Text `json:"comment"`
}
// ============================================================================
// Рейтинги
// ============================================================================
func (q *Queries) CreateRating(ctx context.Context, arg CreateRatingParams) (Rating, error) {
row := q.db.QueryRow(ctx, CreateRating,
arg.VolunteerResponseID,
arg.VolunteerID,
arg.RequesterID,
arg.RequestID,
arg.Rating,
arg.Comment,
)
var i Rating
err := row.Scan(
&i.ID,
&i.VolunteerResponseID,
&i.VolunteerID,
&i.RequesterID,
&i.RequestID,
&i.Rating,
&i.Comment,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateStatusHistoryEntry = `-- name: CreateStatusHistoryEntry :one
INSERT INTO request_status_history (
request_id,
from_status,
to_status,
changed_by,
comment
) VALUES (
$1,
$2,
$3,
$4,
$5
) RETURNING id, request_id, from_status, to_status, changed_by, comment, created_at
`
type CreateStatusHistoryEntryParams struct {
RequestID int64 `json:"request_id"`
FromStatus NullRequestStatus `json:"from_status"`
ToStatus RequestStatus `json:"to_status"`
ChangedBy int64 `json:"changed_by"`
Comment pgtype.Text `json:"comment"`
}
// ============================================================================
// История изменения статусов заявок
// ============================================================================
func (q *Queries) CreateStatusHistoryEntry(ctx context.Context, arg CreateStatusHistoryEntryParams) (RequestStatusHistory, error) {
row := q.db.QueryRow(ctx, CreateStatusHistoryEntry,
arg.RequestID,
arg.FromStatus,
arg.ToStatus,
arg.ChangedBy,
arg.Comment,
)
var i RequestStatusHistory
err := row.Scan(
&i.ID,
&i.RequestID,
&i.FromStatus,
&i.ToStatus,
&i.ChangedBy,
&i.Comment,
&i.CreatedAt,
)
return i, err
}
const CreateVolunteerResponse = `-- name: CreateVolunteerResponse :one
INSERT INTO volunteer_responses (
request_id,
volunteer_id,
message
) VALUES (
$1,
$2,
$3
)
ON CONFLICT (request_id, volunteer_id) DO NOTHING
RETURNING id, request_id, volunteer_id, status, message, responded_at, accepted_at, rejected_at, created_at, updated_at
`
type CreateVolunteerResponseParams struct {
RequestID int64 `json:"request_id"`
VolunteerID int64 `json:"volunteer_id"`
Message pgtype.Text `json:"message"`
}
// Фаза 3: Отклики волонтеров и история статусов (СРЕДНИЙ ПРИОРИТЕТ)
// Запросы для управления откликами волонтеров и историей изменения статусов заявок
// ============================================================================
// Отклики волонтеров
// ============================================================================
func (q *Queries) CreateVolunteerResponse(ctx context.Context, arg CreateVolunteerResponseParams) (VolunteerResponse, error) {
row := q.db.QueryRow(ctx, CreateVolunteerResponse, arg.RequestID, arg.VolunteerID, arg.Message)
var i VolunteerResponse
err := row.Scan(
&i.ID,
&i.RequestID,
&i.VolunteerID,
&i.Status,
&i.Message,
&i.RespondedAt,
&i.AcceptedAt,
&i.RejectedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetLatestStatusChange = `-- name: GetLatestStatusChange :one
SELECT
rsh.id, rsh.request_id, rsh.from_status, rsh.to_status, rsh.changed_by, rsh.comment, rsh.created_at,
(u.first_name || ' ' || u.last_name) as changed_by_name
FROM request_status_history rsh
JOIN users u ON u.id = rsh.changed_by
WHERE rsh.request_id = $1
ORDER BY rsh.created_at DESC
LIMIT 1
`
type GetLatestStatusChangeRow struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
FromStatus NullRequestStatus `json:"from_status"`
ToStatus RequestStatus `json:"to_status"`
ChangedBy int64 `json:"changed_by"`
Comment pgtype.Text `json:"comment"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ChangedByName interface{} `json:"changed_by_name"`
}
func (q *Queries) GetLatestStatusChange(ctx context.Context, requestID int64) (GetLatestStatusChangeRow, error) {
row := q.db.QueryRow(ctx, GetLatestStatusChange, requestID)
var i GetLatestStatusChangeRow
err := row.Scan(
&i.ID,
&i.RequestID,
&i.FromStatus,
&i.ToStatus,
&i.ChangedBy,
&i.Comment,
&i.CreatedAt,
&i.ChangedByName,
)
return i, err
}
const GetRatingByResponseID = `-- name: GetRatingByResponseID :one
SELECT id, volunteer_response_id, volunteer_id, requester_id, request_id, rating, comment, created_at, updated_at FROM ratings
WHERE volunteer_response_id = $1
`
func (q *Queries) GetRatingByResponseID(ctx context.Context, volunteerResponseID int64) (Rating, error) {
row := q.db.QueryRow(ctx, GetRatingByResponseID, volunteerResponseID)
var i Rating
err := row.Scan(
&i.ID,
&i.VolunteerResponseID,
&i.VolunteerID,
&i.RequesterID,
&i.RequestID,
&i.Rating,
&i.Comment,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetRatingsByVolunteer = `-- name: GetRatingsByVolunteer :many
SELECT
r.id, r.volunteer_response_id, r.volunteer_id, r.requester_id, r.request_id, r.rating, r.comment, r.created_at, r.updated_at,
req.title as request_title,
(u.first_name || ' ' || u.last_name) as requester_name
FROM ratings r
JOIN requests req ON req.id = r.request_id
JOIN users u ON u.id = r.requester_id
WHERE r.volunteer_id = $1
ORDER BY r.created_at DESC
LIMIT $2 OFFSET $3
`
type GetRatingsByVolunteerParams struct {
VolunteerID int64 `json:"volunteer_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetRatingsByVolunteerRow struct {
ID int64 `json:"id"`
VolunteerResponseID int64 `json:"volunteer_response_id"`
VolunteerID int64 `json:"volunteer_id"`
RequesterID int64 `json:"requester_id"`
RequestID int64 `json:"request_id"`
Rating int32 `json:"rating"`
Comment pgtype.Text `json:"comment"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
RequestTitle string `json:"request_title"`
RequesterName interface{} `json:"requester_name"`
}
func (q *Queries) GetRatingsByVolunteer(ctx context.Context, arg GetRatingsByVolunteerParams) ([]GetRatingsByVolunteerRow, error) {
rows, err := q.db.Query(ctx, GetRatingsByVolunteer, arg.VolunteerID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetRatingsByVolunteerRow{}
for rows.Next() {
var i GetRatingsByVolunteerRow
if err := rows.Scan(
&i.ID,
&i.VolunteerResponseID,
&i.VolunteerID,
&i.RequesterID,
&i.RequestID,
&i.Rating,
&i.Comment,
&i.CreatedAt,
&i.UpdatedAt,
&i.RequestTitle,
&i.RequesterName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetRequestStatusHistory = `-- name: GetRequestStatusHistory :many
SELECT
rsh.id, rsh.request_id, rsh.from_status, rsh.to_status, rsh.changed_by, rsh.comment, rsh.created_at,
(u.first_name || ' ' || u.last_name) as changed_by_name
FROM request_status_history rsh
JOIN users u ON u.id = rsh.changed_by
WHERE rsh.request_id = $1
ORDER BY rsh.created_at DESC
`
type GetRequestStatusHistoryRow struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
FromStatus NullRequestStatus `json:"from_status"`
ToStatus RequestStatus `json:"to_status"`
ChangedBy int64 `json:"changed_by"`
Comment pgtype.Text `json:"comment"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
ChangedByName interface{} `json:"changed_by_name"`
}
func (q *Queries) GetRequestStatusHistory(ctx context.Context, requestID int64) ([]GetRequestStatusHistoryRow, error) {
rows, err := q.db.Query(ctx, GetRequestStatusHistory, requestID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetRequestStatusHistoryRow{}
for rows.Next() {
var i GetRequestStatusHistoryRow
if err := rows.Scan(
&i.ID,
&i.RequestID,
&i.FromStatus,
&i.ToStatus,
&i.ChangedBy,
&i.Comment,
&i.CreatedAt,
&i.ChangedByName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetResponseByID = `-- name: GetResponseByID :one
SELECT
vr.id, vr.request_id, vr.volunteer_id, vr.status, vr.message, vr.responded_at, vr.accepted_at, vr.rejected_at, vr.created_at, vr.updated_at,
(u.first_name || ' ' || u.last_name) as volunteer_name,
r.title as request_title
FROM volunteer_responses vr
JOIN users u ON u.id = vr.volunteer_id
JOIN requests r ON r.id = vr.request_id
WHERE vr.id = $1
`
type GetResponseByIDRow struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
VolunteerID int64 `json:"volunteer_id"`
Status NullResponseStatus `json:"status"`
Message pgtype.Text `json:"message"`
RespondedAt pgtype.Timestamptz `json:"responded_at"`
AcceptedAt pgtype.Timestamptz `json:"accepted_at"`
RejectedAt pgtype.Timestamptz `json:"rejected_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
VolunteerName interface{} `json:"volunteer_name"`
RequestTitle string `json:"request_title"`
}
func (q *Queries) GetResponseByID(ctx context.Context, id int64) (GetResponseByIDRow, error) {
row := q.db.QueryRow(ctx, GetResponseByID, id)
var i GetResponseByIDRow
err := row.Scan(
&i.ID,
&i.RequestID,
&i.VolunteerID,
&i.Status,
&i.Message,
&i.RespondedAt,
&i.AcceptedAt,
&i.RejectedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.VolunteerName,
&i.RequestTitle,
)
return i, err
}
const GetResponsesByRequest = `-- name: GetResponsesByRequest :many
SELECT
vr.id, vr.request_id, vr.volunteer_id, vr.status, vr.message, vr.responded_at, vr.accepted_at, vr.rejected_at, vr.created_at, vr.updated_at,
(u.first_name || ' ' || u.last_name) as volunteer_name,
u.avatar_url as volunteer_avatar,
u.volunteer_rating,
u.completed_requests_count,
u.email as volunteer_email,
u.phone as volunteer_phone
FROM volunteer_responses vr
JOIN users u ON u.id = vr.volunteer_id
WHERE vr.request_id = $1
ORDER BY vr.created_at DESC
`
type GetResponsesByRequestRow struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
VolunteerID int64 `json:"volunteer_id"`
Status NullResponseStatus `json:"status"`
Message pgtype.Text `json:"message"`
RespondedAt pgtype.Timestamptz `json:"responded_at"`
AcceptedAt pgtype.Timestamptz `json:"accepted_at"`
RejectedAt pgtype.Timestamptz `json:"rejected_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
VolunteerName interface{} `json:"volunteer_name"`
VolunteerAvatar pgtype.Text `json:"volunteer_avatar"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
VolunteerEmail string `json:"volunteer_email"`
VolunteerPhone pgtype.Text `json:"volunteer_phone"`
}
func (q *Queries) GetResponsesByRequest(ctx context.Context, requestID int64) ([]GetResponsesByRequestRow, error) {
rows, err := q.db.Query(ctx, GetResponsesByRequest, requestID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetResponsesByRequestRow{}
for rows.Next() {
var i GetResponsesByRequestRow
if err := rows.Scan(
&i.ID,
&i.RequestID,
&i.VolunteerID,
&i.Status,
&i.Message,
&i.RespondedAt,
&i.AcceptedAt,
&i.RejectedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.VolunteerName,
&i.VolunteerAvatar,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.VolunteerEmail,
&i.VolunteerPhone,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetResponsesByVolunteer = `-- name: GetResponsesByVolunteer :many
SELECT
vr.id, vr.request_id, vr.volunteer_id, vr.status, vr.message, vr.responded_at, vr.accepted_at, vr.rejected_at, vr.created_at, vr.updated_at,
r.title as request_title,
r.status as request_status,
(u.first_name || ' ' || u.last_name) as requester_name
FROM volunteer_responses vr
JOIN requests r ON r.id = vr.request_id
JOIN users u ON u.id = r.requester_id
WHERE vr.volunteer_id = $1
ORDER BY vr.created_at DESC
LIMIT $2 OFFSET $3
`
type GetResponsesByVolunteerParams struct {
VolunteerID int64 `json:"volunteer_id"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetResponsesByVolunteerRow struct {
ID int64 `json:"id"`
RequestID int64 `json:"request_id"`
VolunteerID int64 `json:"volunteer_id"`
Status NullResponseStatus `json:"status"`
Message pgtype.Text `json:"message"`
RespondedAt pgtype.Timestamptz `json:"responded_at"`
AcceptedAt pgtype.Timestamptz `json:"accepted_at"`
RejectedAt pgtype.Timestamptz `json:"rejected_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
RequestTitle string `json:"request_title"`
RequestStatus NullRequestStatus `json:"request_status"`
RequesterName interface{} `json:"requester_name"`
}
func (q *Queries) GetResponsesByVolunteer(ctx context.Context, arg GetResponsesByVolunteerParams) ([]GetResponsesByVolunteerRow, error) {
rows, err := q.db.Query(ctx, GetResponsesByVolunteer, arg.VolunteerID, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetResponsesByVolunteerRow{}
for rows.Next() {
var i GetResponsesByVolunteerRow
if err := rows.Scan(
&i.ID,
&i.RequestID,
&i.VolunteerID,
&i.Status,
&i.Message,
&i.RespondedAt,
&i.AcceptedAt,
&i.RejectedAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.RequestTitle,
&i.RequestStatus,
&i.RequesterName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RejectVolunteerResponse = `-- name: RejectVolunteerResponse :exec
UPDATE volunteer_responses SET
status = 'rejected',
rejected_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) RejectVolunteerResponse(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, RejectVolunteerResponse, id)
return err
}
const UpdateRating = `-- name: UpdateRating :exec
UPDATE ratings SET
rating = $2,
comment = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateRatingParams struct {
ID int64 `json:"id"`
Rating int32 `json:"rating"`
Comment pgtype.Text `json:"comment"`
}
func (q *Queries) UpdateRating(ctx context.Context, arg UpdateRatingParams) error {
_, err := q.db.Exec(ctx, UpdateRating, arg.ID, arg.Rating, arg.Comment)
return err
}

View File

@@ -0,0 +1,413 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: users.sql
package database
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const BlockUser = `-- name: BlockUser :exec
UPDATE users SET
is_blocked = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) BlockUser(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, BlockUser, id)
return err
}
const GetUserProfile = `-- name: GetUserProfile :one
SELECT
id,
email,
phone,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at
FROM users
WHERE id = $1 AND deleted_at IS NULL
`
type GetUserProfileRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
IsBlocked pgtype.Bool `json:"is_blocked"`
EmailVerified pgtype.Bool `json:"email_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
}
// Фаза 1C: Управление профилем (КРИТИЧНО)
// Запросы для получения и обновления профилей пользователей
// ============================================================================
// Профиль пользователя
// ============================================================================
func (q *Queries) GetUserProfile(ctx context.Context, id int64) (GetUserProfileRow, error) {
row := q.db.QueryRow(ctx, GetUserProfile, id)
var i GetUserProfileRow
err := row.Scan(
&i.ID,
&i.Email,
&i.Phone,
&i.FirstName,
&i.LastName,
&i.AvatarUrl,
&i.Latitude,
&i.Longitude,
&i.Address,
&i.City,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.IsVerified,
&i.IsBlocked,
&i.EmailVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastLoginAt,
)
return i, err
}
const GetUsersByIDs = `-- name: GetUsersByIDs :many
SELECT
id,
email,
phone,
password_hash,
first_name,
last_name,
avatar_url,
ST_Y(location::geometry) as latitude,
ST_X(location::geometry) as longitude,
address,
city,
volunteer_rating,
completed_requests_count,
is_verified,
is_blocked,
email_verified,
created_at,
updated_at,
last_login_at,
deleted_at
FROM users
WHERE id = ANY($1::bigint[])
AND deleted_at IS NULL
`
type GetUsersByIDsRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
Phone pgtype.Text `json:"phone"`
PasswordHash string `json:"password_hash"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Latitude interface{} `json:"latitude"`
Longitude interface{} `json:"longitude"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
IsBlocked pgtype.Bool `json:"is_blocked"`
EmailVerified pgtype.Bool `json:"email_verified"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
LastLoginAt pgtype.Timestamptz `json:"last_login_at"`
DeletedAt pgtype.Timestamptz `json:"deleted_at"`
}
// ============================================================================
// Поиск пользователей
// ============================================================================
func (q *Queries) GetUsersByIDs(ctx context.Context, dollar_1 []int64) ([]GetUsersByIDsRow, error) {
rows, err := q.db.Query(ctx, GetUsersByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
items := []GetUsersByIDsRow{}
for rows.Next() {
var i GetUsersByIDsRow
if err := rows.Scan(
&i.ID,
&i.Email,
&i.Phone,
&i.PasswordHash,
&i.FirstName,
&i.LastName,
&i.AvatarUrl,
&i.Latitude,
&i.Longitude,
&i.Address,
&i.City,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.IsVerified,
&i.IsBlocked,
&i.EmailVerified,
&i.CreatedAt,
&i.UpdatedAt,
&i.LastLoginAt,
&i.DeletedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetVolunteerStatistics = `-- name: GetVolunteerStatistics :one
SELECT
id,
first_name,
last_name,
volunteer_rating,
completed_requests_count,
created_at as member_since
FROM users
WHERE id = $1
AND deleted_at IS NULL
`
type GetVolunteerStatisticsRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
MemberSince pgtype.Timestamptz `json:"member_since"`
}
func (q *Queries) GetVolunteerStatistics(ctx context.Context, id int64) (GetVolunteerStatisticsRow, error) {
row := q.db.QueryRow(ctx, GetVolunteerStatistics, id)
var i GetVolunteerStatisticsRow
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.MemberSince,
)
return i, err
}
const SearchUsersByName = `-- name: SearchUsersByName :many
SELECT
id,
email,
first_name,
last_name,
avatar_url,
volunteer_rating,
completed_requests_count,
is_verified
FROM users
WHERE (first_name ILIKE '%' || $1 || '%' OR last_name ILIKE '%' || $1 || '%' OR (first_name || ' ' || last_name) ILIKE '%' || $1 || '%')
AND deleted_at IS NULL
AND is_blocked = FALSE
ORDER BY volunteer_rating DESC NULLS LAST
LIMIT $2 OFFSET $3
`
type SearchUsersByNameParams struct {
Column1 pgtype.Text `json:"column_1"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type SearchUsersByNameRow struct {
ID int64 `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
VolunteerRating pgtype.Numeric `json:"volunteer_rating"`
CompletedRequestsCount pgtype.Int4 `json:"completed_requests_count"`
IsVerified pgtype.Bool `json:"is_verified"`
}
func (q *Queries) SearchUsersByName(ctx context.Context, arg SearchUsersByNameParams) ([]SearchUsersByNameRow, error) {
rows, err := q.db.Query(ctx, SearchUsersByName, arg.Column1, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SearchUsersByNameRow{}
for rows.Next() {
var i SearchUsersByNameRow
if err := rows.Scan(
&i.ID,
&i.Email,
&i.FirstName,
&i.LastName,
&i.AvatarUrl,
&i.VolunteerRating,
&i.CompletedRequestsCount,
&i.IsVerified,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SoftDeleteUser = `-- name: SoftDeleteUser :exec
UPDATE users SET
deleted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) SoftDeleteUser(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, SoftDeleteUser, id)
return err
}
const UnblockUser = `-- name: UnblockUser :exec
UPDATE users SET
is_blocked = FALSE,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) UnblockUser(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, UnblockUser, id)
return err
}
const UpdateUserLocation = `-- name: UpdateUserLocation :exec
UPDATE users SET
location = ST_SetSRID(ST_MakePoint($2, $3), 4326)::geography,
address = COALESCE($4, address),
city = COALESCE($5, city),
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateUserLocationParams struct {
ID int64 `json:"id"`
StMakepoint interface{} `json:"st_makepoint"`
StMakepoint_2 interface{} `json:"st_makepoint_2"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
}
func (q *Queries) UpdateUserLocation(ctx context.Context, arg UpdateUserLocationParams) error {
_, err := q.db.Exec(ctx, UpdateUserLocation,
arg.ID,
arg.StMakepoint,
arg.StMakepoint_2,
arg.Address,
arg.City,
)
return err
}
const UpdateUserPassword = `-- name: UpdateUserPassword :exec
UPDATE users SET
password_hash = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateUserPasswordParams struct {
ID int64 `json:"id"`
PasswordHash string `json:"password_hash"`
}
func (q *Queries) UpdateUserPassword(ctx context.Context, arg UpdateUserPasswordParams) error {
_, err := q.db.Exec(ctx, UpdateUserPassword, arg.ID, arg.PasswordHash)
return err
}
const UpdateUserProfile = `-- name: UpdateUserProfile :exec
UPDATE users SET
phone = COALESCE($1, phone),
first_name = COALESCE($2, first_name),
last_name = COALESCE($3, last_name),
avatar_url = COALESCE($4, avatar_url),
address = COALESCE($5, address),
city = COALESCE($6, city),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
`
type UpdateUserProfileParams struct {
Phone pgtype.Text `json:"phone"`
FirstName pgtype.Text `json:"first_name"`
LastName pgtype.Text `json:"last_name"`
AvatarUrl pgtype.Text `json:"avatar_url"`
Address pgtype.Text `json:"address"`
City pgtype.Text `json:"city"`
UserID int64 `json:"user_id"`
}
func (q *Queries) UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error {
_, err := q.db.Exec(ctx, UpdateUserProfile,
arg.Phone,
arg.FirstName,
arg.LastName,
arg.AvatarUrl,
arg.Address,
arg.City,
arg.UserID,
)
return err
}
const VerifyUserEmail = `-- name: VerifyUserEmail :exec
UPDATE users SET
email_verified = TRUE,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) VerifyUserEmail(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, VerifyUserEmail, id)
return err
}