initial commit
This commit is contained in:
548
internal/database/auth.sql.go
Normal file
548
internal/database/auth.sql.go
Normal 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
32
internal/database/db.go
Normal 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,
|
||||
}
|
||||
}
|
||||
43
internal/database/geography.go
Normal file
43
internal/database/geography.go
Normal 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,
|
||||
}
|
||||
}
|
||||
412
internal/database/geospatial.sql.go
Normal file
412
internal/database/geospatial.sql.go
Normal 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
530
internal/database/models.go
Normal 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"`
|
||||
}
|
||||
185
internal/database/querier.go
Normal file
185
internal/database/querier.go
Normal 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)
|
||||
194
internal/database/queries/auth.sql
Normal file
194
internal/database/queries/auth.sql
Normal 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';
|
||||
151
internal/database/queries/geospatial.sql
Normal file
151
internal/database/queries/geospatial.sql
Normal 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;
|
||||
102
internal/database/queries/rbac.sql
Normal file
102
internal/database/queries/rbac.sql
Normal 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
|
||||
);
|
||||
339
internal/database/queries/requests.sql
Normal file
339
internal/database/queries/requests.sql
Normal 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;
|
||||
192
internal/database/queries/responses.sql
Normal file
192
internal/database/queries/responses.sql
Normal 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);
|
||||
137
internal/database/queries/users.sql
Normal file
137
internal/database/queries/users.sql
Normal 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;
|
||||
352
internal/database/rbac.sql.go
Normal file
352
internal/database/rbac.sql.go
Normal 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
|
||||
}
|
||||
1030
internal/database/requests.sql.go
Normal file
1030
internal/database/requests.sql.go
Normal file
File diff suppressed because it is too large
Load Diff
713
internal/database/responses.sql.go
Normal file
713
internal/database/responses.sql.go
Normal 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
|
||||
}
|
||||
413
internal/database/users.sql.go
Normal file
413
internal/database/users.sql.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user