first commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerPort string
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPassword string
|
||||
DBName string
|
||||
}
|
||||
|
||||
func LoadConfig() *Config {
|
||||
viper.SetConfigFile(".env")
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Fatalf("Ошибка чтения конфигурации: %v", err)
|
||||
}
|
||||
return &Config{
|
||||
ServerPort: viper.GetString("SERVER_PORT"),
|
||||
DBHost: viper.GetString("DB_HOST"),
|
||||
DBPort: viper.GetString("DB_PORT"),
|
||||
DBUser: viper.GetString("DB_USER"),
|
||||
DBPassword: viper.GetString("DB_PASSWORD"),
|
||||
DBName: viper.GetString("DB_NAME"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"effective_mobile_test_go_api/internal/model"
|
||||
"effective_mobile_test_go_api/internal/service"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/gorilla/mux"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
log *zap.Logger
|
||||
service *service.SubscriptionService
|
||||
}
|
||||
|
||||
func NewHandler(log *zap.Logger, service *service.SubscriptionService) *Handler {
|
||||
return &Handler{log: log, service: service}
|
||||
}
|
||||
|
||||
type CreateSubscriptionRequest struct {
|
||||
ServiceName string `json:"service_name" example:"Yandex Plus"`
|
||||
Price int `json:"price" example:"400"`
|
||||
UserID string `json:"user_id" example:"60601fee-2bf1-4721-ae6f-7636e79a0cba"`
|
||||
StartDate string `json:"start_date" example:"07-2025"`
|
||||
EndDate *string `json:"end_date,omitempty" example:"12-2025"`
|
||||
}
|
||||
|
||||
// CreateSubscription godoc
|
||||
// @Summary Создать подписку
|
||||
// @Tags subscriptions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param subscription body CreateSubscriptionRequest true "Создание подписки"
|
||||
// @Success 201 {object} model.Subscription
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /subscriptions [post]
|
||||
func (h *Handler) CreateSubscription(w http.ResponseWriter, r *http.Request) {
|
||||
var req CreateSubscriptionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
h.log.Error("Ошибка декодирования", zap.Error(err))
|
||||
http.Error(w, "Ошибка запроса", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userUUID, err := uuid.Parse(req.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный uuid пользователя", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
startDate, err := time.Parse("01-2006", req.StartDate)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный формат start_date", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var endDate *time.Time
|
||||
if req.EndDate != nil {
|
||||
ed, err := time.Parse("01-2006", *req.EndDate)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный формат даты окончания", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
endDate = &ed
|
||||
}
|
||||
sub := &model.Subscription{
|
||||
ServiceName: req.ServiceName,
|
||||
Price: req.Price,
|
||||
UserID: userUUID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
}
|
||||
if err := h.service.Create(r.Context(), sub); err != nil {
|
||||
h.log.Error("Подписчик не создан", zap.Error(err))
|
||||
http.Error(w, "Ошибка создания", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(sub)
|
||||
}
|
||||
|
||||
// GetSubscription godoc
|
||||
// @Summary Получить подписку по ID
|
||||
// @Tags subscriptions
|
||||
// @Produce json
|
||||
// @Param id path int true "ID подписки"
|
||||
// @Success 200 {object} model.Subscription
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 404 {object} map[string]string
|
||||
// @Router /subscriptions/{id} [get]
|
||||
func (h *Handler) GetSubscription(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный идентификатор", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sub, err := h.service.GetByID(r.Context(), id)
|
||||
if err != nil {
|
||||
http.Error(w, "Подписчик не найден", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(sub)
|
||||
}
|
||||
|
||||
// UpdateSubscription godoc
|
||||
// @Summary Обновить подписку по ID
|
||||
// @Tags subscriptions
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "ID подписки"
|
||||
// @Param subscription body CreateSubscriptionRequest true "Обновление подписки"
|
||||
// @Success 200 {object} model.Subscription
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /subscriptions/{id} [put]
|
||||
func (h *Handler) UpdateSubscription(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный идентификатор", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var req CreateSubscriptionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Неверный запрос", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userUUID, err := uuid.Parse(req.UserID)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный uuid пользователя", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
startDate, err := time.Parse("01-2006", req.StartDate)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный формат даты начала", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
var endDate *time.Time
|
||||
if req.EndDate != nil {
|
||||
ed, err := time.Parse("01-2006", *req.EndDate)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный формат даты окончания", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
endDate = &ed
|
||||
}
|
||||
sub := &model.Subscription{
|
||||
ID: id,
|
||||
ServiceName: req.ServiceName,
|
||||
Price: req.Price,
|
||||
UserID: userUUID,
|
||||
StartDate: startDate,
|
||||
EndDate: endDate,
|
||||
}
|
||||
if err := h.service.Update(r.Context(), sub); err != nil {
|
||||
http.Error(w, "Не удалось обновить", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(sub)
|
||||
}
|
||||
|
||||
// DeleteSubscription godoc
|
||||
// @Summary Удалить подписку по ID
|
||||
// @Tags subscriptions
|
||||
// @Param id path int true "ID подписки"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /subscriptions/{id} [delete]
|
||||
func (h *Handler) DeleteSubscription(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := mux.Vars(r)["id"]
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный идентификатор", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.service.Delete(r.Context(), id); err != nil {
|
||||
http.Error(w, "Не удалось удалить", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListSubscriptions godoc
|
||||
// @Summary Список подписок
|
||||
// @Tags subscriptions
|
||||
// @Produce json
|
||||
// @Success 200 {array} model.Subscription
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /subscriptions [get]
|
||||
func (h *Handler) ListSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
subs, err := h.service.List(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, "Не удалось включить в список", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(subs)
|
||||
}
|
||||
|
||||
// SumSubscriptions godoc
|
||||
// @Summary Суммарная стоимость подписок за период с фильтрацией
|
||||
// @Tags subscriptions
|
||||
// @Produce json
|
||||
// @Param user_id query string false "UUID пользователя"
|
||||
// @Param service_name query string false "Название сервиса"
|
||||
// @Param start query string true "Начало периода, формат MM-YYYY" example("07-2025")
|
||||
// @Param end query string true "Конец периода, формат MM-YYYY" example("08-2025")
|
||||
// @Success 200 {object} map[string]int
|
||||
// @Failure 400 {object} map[string]string
|
||||
// @Failure 500 {object} map[string]string
|
||||
// @Router /subscriptions/sum [get]
|
||||
func (h *Handler) SumSubscriptions(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
var userID *uuid.UUID
|
||||
if u := q.Get("user_id"); u != "" {
|
||||
id, err := uuid.Parse(u)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверный идентификатор пользователя", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userID = &id
|
||||
}
|
||||
serviceName := q.Get("service_name")
|
||||
startStr := q.Get("start")
|
||||
endStr := q.Get("end")
|
||||
if startStr == "" || endStr == "" {
|
||||
http.Error(w, "требуются начальные и конечные параметры запроса", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
startDate, err := time.Parse("01-2006", startStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверная дата начала", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
endDate, err := time.Parse("01-2006", endStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Неверная дата окончания", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
sum, err := h.service.SumPrice(r.Context(), userID, serviceName, startDate, endDate)
|
||||
if err != nil {
|
||||
http.Error(w, "Не удалось рассчитать сумму", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
resp := map[string]int{"total_price": sum}
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Subscription struct {
|
||||
ID int `json:"id"`
|
||||
ServiceName string `json:"service_name"`
|
||||
Price int `json:"price"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
StartDate time.Time `json:"start_date"`
|
||||
EndDate *time.Time `json:"end_date,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"effective_mobile_test_go_api/internal/model"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SubscriptionRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSubscriptionRepo(db *sql.DB) *SubscriptionRepo {
|
||||
return &SubscriptionRepo{db: db}
|
||||
}
|
||||
|
||||
func (r *SubscriptionRepo) Create(ctx context.Context, s *model.Subscription) error {
|
||||
query := `INSERT INTO subscriptions (service_name, price, user_id, start_date, end_date) VALUES ($1, $2, $3, $4, $5) RETURNING id`
|
||||
var id int
|
||||
err := r.db.QueryRowContext(ctx, query, s.ServiceName, s.Price, s.UserID, s.StartDate, s.EndDate).Scan(&id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SubscriptionRepo) GetByID(ctx context.Context, id int) (*model.Subscription, error) {
|
||||
query := `SELECT id, service_name, price, user_id, start_date, end_date FROM subscriptions WHERE id = $1`
|
||||
s := &model.Subscription{}
|
||||
err := r.db.QueryRowContext(ctx, query, id).Scan(&s.ID, &s.ServiceName, &s.Price, &s.UserID, &s.StartDate, &s.EndDate)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, errors.New("подписка не найдена")
|
||||
}
|
||||
return s, err
|
||||
}
|
||||
|
||||
func (r *SubscriptionRepo) Update(ctx context.Context, s *model.Subscription) error {
|
||||
query := `UPDATE subscriptions SET service_name=$1, price=$2, user_id=$3, start_date=$4, end_date=$5 WHERE id=$6`
|
||||
res, err := r.db.ExecContext(ctx, query, s.ServiceName, s.Price, s.UserID, s.StartDate, s.EndDate, s.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil || rowsAffected == 0 {
|
||||
return errors.New("подписка не найдена или не обновлена")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SubscriptionRepo) Delete(ctx context.Context, id int) error {
|
||||
query := `DELETE FROM subscriptions WHERE id = $1`
|
||||
res, err := r.db.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rowsAffected, err := res.RowsAffected()
|
||||
if err != nil || rowsAffected == 0 {
|
||||
return errors.New("подписка не найдена или не удалена")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SubscriptionRepo) List(ctx context.Context) ([]*model.Subscription, error) {
|
||||
query := `SELECT id, service_name, price, user_id, start_date, end_date FROM subscriptions`
|
||||
rows, err := r.db.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var subscriptions []*model.Subscription
|
||||
for rows.Next() {
|
||||
s := &model.Subscription{}
|
||||
if err := rows.Scan(&s.ID, &s.ServiceName, &s.Price, &s.UserID, &s.StartDate, &s.EndDate); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
subscriptions = append(subscriptions, s)
|
||||
}
|
||||
return subscriptions, nil
|
||||
}
|
||||
|
||||
func (r *SubscriptionRepo) SumPrice(ctx context.Context, userID *uuid.UUID, serviceName string, startPeriod, endPeriod time.Time) (int, error) {
|
||||
baseQuery := `SELECT COALESCE(SUM(price), 0) FROM subscriptions WHERE start_date >= $1 AND start_date <= $2`
|
||||
args := []interface{}{startPeriod, endPeriod}
|
||||
i := 3
|
||||
if userID != nil {
|
||||
baseQuery += ` AND user_id = $` + strconv.Itoa(i)
|
||||
args = append(args, *userID)
|
||||
i++
|
||||
}
|
||||
if serviceName != "" {
|
||||
baseQuery += ` AND service_name = $` + strconv.Itoa(i)
|
||||
args = append(args, serviceName)
|
||||
}
|
||||
var sum int
|
||||
err := r.db.QueryRowContext(ctx, baseQuery, args...).Scan(&sum)
|
||||
return sum, err
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"effective_mobile_test_go_api/internal/model"
|
||||
"effective_mobile_test_go_api/internal/repo"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type SubscriptionService struct {
|
||||
repo *repo.SubscriptionRepo
|
||||
}
|
||||
|
||||
func NewSubscriptionService(r *repo.SubscriptionRepo) *SubscriptionService {
|
||||
return &SubscriptionService{repo: r}
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) Create(ctx context.Context, sub *model.Subscription) error {
|
||||
return s.repo.Create(ctx, sub)
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) GetByID(ctx context.Context, id int) (*model.Subscription, error) {
|
||||
return s.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) Update(ctx context.Context, sub *model.Subscription) error {
|
||||
return s.repo.Update(ctx, sub)
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) Delete(ctx context.Context, id int) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) List(ctx context.Context) ([]*model.Subscription, error) {
|
||||
return s.repo.List(ctx)
|
||||
}
|
||||
|
||||
func (s *SubscriptionService) SumPrice(ctx context.Context, userID *uuid.UUID, serviceName string, startPeriod, endPeriod time.Time) (int, error) {
|
||||
return s.repo.SumPrice(ctx, userID, serviceName, startPeriod, endPeriod)
|
||||
}
|
||||
Reference in New Issue
Block a user